Introduction
The backstory
I want to develop a simple app to practice my command of front-end and back-end concepts in conjunction with React. A To Do App is very common for these types of projects.
The Problem
Attempting to challenge myself, I’m making use of a few different technologies including some of the latest features available from React. Working with state, in particular, can be challenging. React’s Context API is meant to simplify this aspect of the app. Using Context, state can be collected into one provider and made available to components at any stage of the app. Firebase provides a way to manage users and store data with a rich javascript API. And Tailwind CSS is a flexible and non-intrusive CSS framework that allows for attractive styling.
The Solution
A simple To-Do App that allows a user to login and view, create, update, and delete their To-Dos. Each To-Do list can have keywords that are used as flags and group together multiple lists.
My To Do app is not anything radically different than similar apps. It allows a user to create an account and log in. Once logged in, users can create a new To Do list, add tasks to the list, view To Do lists and tasks, update To Do list titles and the text of tasks, and delete To Do lists and/or tasks. Each To Do list can also have tags associated with it, which can be used to group different lists together.
Making use of Firebase Auth will provide an easy solution for user sign up and authentication. Firestore will allow for content to be saved to a data file and retrieved when requested. React will act as the user interface for user interactions and rendering content. The app makes use of the React Hooks libraries, including the Context API to maintain state throughout use of the app. Finally, Tailwind CSS is good for handling the styling of UI layout and components.
The app supports user browsing to a handful of different pages. The react-router-dom package adds this functionality to the app.
Technologies Used
Being a To Do List app, the technologies used were fairly standard. For the user interface, I employed React with Tailwind CSS. Navigation and routing was handled with React Router Dom. The app connects to a Firebase database. And finally, state is managed by React’s Context API.
- React
- Tailwind CSS
- React Router Dom
- Firebase
- Context API
Requirements and Considerations
The list below reflects a preliminary assessment of the likely system requirements for the project. It’s anticipated that most of the preliminary requirements will be incorporated in the final requirements although possibly with some modifications. Additional requirements will be added that are necessary for the system to function properly. The listed requirements are meant to be as comprehensive as possible to reduce the amount of additional requirements added later.
- Users will be able to sign-up and login to the App
- Users will be able to sign-out of the App
- Users will be able to view a list of options from the Home page
- Users will be able to view their Profile and update fields
- Users will be able to create To-Do Lists, each with a title
- Users will be able to view, update, and delete their lists
- Users will be able to add tasks to their lists
- Users will be able to view, update, and delete tasks from their lists
- Users will be able to add one or more Keywords to their lists
- Users will be able to view lists grouped by keywords
- The App will include an About page describing the App
UX Design
Since this app was simple in scope and a common type of app for developers to practice their skills, much of the UX design was uncomplicated. Things like the header, navigation, and footer were all fairly typical - left aligned logo and right aligned navigation options. The footer was also a fairly typical design, which included three center-aligned columns of navigational links, although not yet added into my wireframes.
To Do Detail Page
The major area for UX design was the detail and edit pages for a To Do item. Following a somewhat typical design for to-do lists, the chosen design included a card layout with a heading text for the list title, a list of to-do items that included a checkbox for each. When a checkbox was checked, the list item would show a strike through text and a lighter color. The card also included buttons for editing and deleting. An element not always included in to-do lists is an additional row of keywords for each list. This was added in a separate section of the card.
To Do Edit Page
On the page to edit a To Do list, the wireframe included a pre-populated input field that gives the ability to edit either the list title or any of the to do task items. There is also the ability to delete a to do item and/or keywords. Finally, there are blank inputs to add a new to do task item or keyword. Finally, there’s a delete button that will delete the entire list including list tasks and the list itself.
Component-Based Visual Design
The visual design of the app makes use of Tailwind CSS version 1.8.5. Employing this CSS framework with React enabled straight forward component design. Several different components were created using this approach, including:
- Page headings
- General use headings
- Inputs
- Buttons
These components were then combined to make more complex components, including:
- To Do Card
- Task List
- Keyword List
Although the scale of this particular application is small, employing a component-based design system can add efficiency to a larger application since the design of the individual components allows for consistency in the user experience.
Information Architecture
The app includes only a handful of top-level pages, including a home page, an about page, and sign-up/login pages. For an authenticated user additional pages include a profile page, a To Do Lists page, and a keyword page.
The most complex structure of the site is the To Do lists, which includes a /lists
page that displays all of the users’ lists. Each list card has an button to edit the list. When clicked the user will be directed to a /lists/:id
page that displays information for the list matching the id value.
Database Design
The app makes use of Firebase for it’s database. Specifically, the site’s data is contained in a NoSQL Cloud Firestore database that includes three collections for users, tasks, and todos.
The decision to include keywords as an array list within the todos collection was based on simplifying the queries to Firestore by finding keyword matches against a list of keywords in a user’s todos held in state.
Development
Although this application was small in scale, there were some interesting challenges that made it a good way to exercise my coding muscles. The challenges can be broken down into different sections:
- Authentication
- Firebase Security
- Handling Display and Organization of User Todos and Todo Tasks
- Handling Display and Edits for Tasks and Keywords in a Single Component
- Using React’s Context API to handle Firebase Interactions and State Management
Authentication
An essential part of the app is allowing users to create accounts and login to the application to view their To Do lists. Thankfully Firebase has authentication processes and functions to make this process easier. Two functions, createUserWithEmailAndPassword
and signInWithEmailAndPassword
, are available to check against Firebase and return data as a return value for apps to handle and direct the application forward. An essential piece is creating a listener that can wait for changes in authentication state from Firebase through the onAuthStateChanged
function and call a function to update React state. The app can then depend on React state management to provide data as query arguments to retrieve more data from Firebase like todos and tasks.
useEffect(() => {
const unsubscribe = addAuthListener((user) => {
const data = { user, isLoading: false };
updateAuth(data);
});
return unsubscribe;
}, []);
const addAuthListener = (callback) => {
const onChange = (user) => {
if (user) {
callback(user.uid);
} else {
callback(null);
}
};
return firebase.auth().onAuthStateChanged(onChange);
};
const updateAuth = (user) => {
dispatch({ type: UPDATE_AUTH, payload: user });
};
The code above is from the AuthState.js
file that handles application-wide state. Using React Hooks and the useEffect hook, the addAuthListener
function is setup on the component mounting and an unsubscribe function is returned is run when the component will unmount. In the meantime, changes to the Firebase authentication state will result in either data to be sent back from Firebase with authenticated user information or null, which is triggered on a signOut
function call.
Firebase Security
A key part of using Firebase is taking advantage of the built-in security rules to control access to data. Cloud Firestore will block reads and writes by default. It’s necessary to update the Rules on Cloud Firestore to allow an app to read and write using the database. The rules below allow an authenticated user to read and write to all the documents in the databases. The next rule updates the first rule by allowing an authenticated user update documents in the users collection only if the usersId in the collection document matches the authenticated user.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow write: if request.auth.uid != null;
allow read: if request.auth.uid != null;
}
match /users/{userId} {
allow write: if request.auth.uid == userId;
}
}
}
Handling the Display and Organization of User Todos and Todo Tasks
A major challenge of the app is displaying a user’s todo lists and the tasks associated with each todo list. Because of the way that NoSQL databases like Firebase work, there’s not a hard coded relationship between one table to another. In place of a hard coded relationship between tables, NoSQL uses logical relationships to associate data in one collection with another collection. To make it easy to maintain the correct association between todo lists and task items, each todo document has a userId
field and each task document has a todoId
and userId
field.
When a user navigates to the To Do Lists page, the app loads todos and tasks based on the user’s id. This is done by running a side effect through the useEffect hook to query Firebase and set data to an app-wide state using React Context API.
useEffect(() => {
const getTodos = async () => {
if (user) {
await getTodosByUserId(user);
setIsSpinning(false);
}
};
getTodos();
}, [user, getTodosByUserId]);
useEffect(() => {
const getTasks = async () => {
if (user) {
await getTasksByUserId(user);
}
};
getTasks();
}, [user, getTasksByUserId]);
Handling Display and Edits for Tasks and Keywords in a Single Component
A key benefit of using React and a component-based UI system is that the components can be flexible and conform to the context of the app to display different html or data. An example is making using of a local state boolean value called editTitleTask
which indicates whether or not the user had clicked on a button to edit the title of a task. The user is either shown a pre-filled input field with an update button or the text of the title.
{
editTitleTask ? (
<div className='flex w-full'>
<form className='flex items-center w-full' onSubmit={handleEditUpdate}>
<Input
id='taskTitleId'
type='text'
placeholder='Enter a title'
name='title'
value={editTitle}
onChange={handleTitleChange}
/>{' '}
<Button type='submit' size='small'>
Update
</Button>
</form>
</div>
) : (
<label
htmlFor={id}
className={`flex-initial ml-2 ${
completed ? 'line-through text-gray-500' : 'text-black'
}`}
>
{title}
</label>
);
}
Using React Context API to handle Firebase Interactions and State Management
This particular app doesn’t have a lot of different pages or overly complex logic, however, it is still complex enough to make handling state by passing props up and down component trees very unappealing. A good solution to handle state app-wide is React Context API. This was done for auth, todos, tasks, and user state management. The way context works in this app is by sending a payload object with a dispatch function and the useReducer
hook, which will handle setting state. The great thing about Context API is that code for state management can be setup away from components and brought in through the useContext
hook as needed.
const getTodosByUserId = useCallback(
async (uid) => {
try {
const todosSnapshot = await firebase
.firestore()
.collection('todos')
.orderBy('createdAt', 'asc')
.where('userId', '==', uid)
.get();
const todos = todosSnapshot.docs.map((todo) => ({
...todo.data(),
id: todo.id,
}));
dispatch({ type: GET_TODOS_BY_USER_ID, payload: todos });
} catch (error) {
console.log(error);
dispatch({ type: TODOS_ERROR, payload: error.message });
}
},
[dispatch]
);
Conclusion
This To Do app was a good experience for implementing several key application features that other, and often more complex, apps use all the time. Features like authentication, signup and login are used in most apps. Working with Firebase and React makes implementing an authentication process fairly straight-forward.
Other features, like routing with react-router-dom
and updating navigation styles based on routing urls helps improve the user experience. Working in conjunction with auth state, another feature is creating protected routes to make some routes available depending on authentication state.
The app offers a fully response design by making use of Tailwind CSS to handle styling for desktop, tablet, or mobile device screens. The flexbox model for layout is used through Tailwind CSS to handle positioning and layout adjustments for different screen sizes.
Finally, features like seamless user feedback after performing actions like updates or checking on checkboxes or typing titles of new tasks and pressing return do not require page reloads. This is a key feature of React that is implemented in the app.
Some things could have been done better in this app, given more time to add features. These features include things like more robust validation on forms, an option to edit a user profile, and administrative interface for global updates, and animated UI elements.