Build a todo list app with React Hooks and Context

Preface

With the popularization of React Hooks, more people are building their new React apps without using class components and redux.

In this tutorial, we’re going to build a todo list app (watch the demo) with React Hooks, Context, and Reducer. Also, we’ll have a glance at how to use JSS, write customized hooks, persist our data in the browser. Finally, we’ll deploy our app to the GitHub Pages.

You could find the source code here.

There’re several advantages to use stateless function components over stateful class components:

  • less code (probably less bug)
  • easier to read
  • preventing high order components wrapping hell

If you don’t know what React Hooks are, please refer to the official document.

Without further ado, let’s build our hooks-based todo list app.

Get Started

First, initialize our app and change your directory:

create-react-app react-hooks-todolist
cd react-hooks-todolist/

Delete useless files:

src/App.css
src/logo.svg

Clean up our App.js a bit.

Run our app with npm start and make sure everything is okay.

File Structure

Let’s have an overview of what our app will look like after we finish.

We put components under src/components, styling files under src/styles, and customized hooks under src/hooks. In addition, we have folders, constants, contexts, helpers, and reducers in a conventional way.

.
├── App.test.js
├── components
│ ├── App.js
│ ├── EditTodoForm.js
│ ├── Todo.js
│ ├── TodoApp.js
│ ├── TodoForm.js
│ └── TodoList.js
├── constants
│ └── actions.js
├── contexts
│ └── todos.context.js
├── helpers
│ └── sizes.js
├── hooks
│ ├── useInputState.js
│ ├── useLocalStorageReducer.js
│ └── useToggleState.js
├── index.css
├── index.js
├── reducers
│ └── todos.reducer.js
├── serviceWorker.js
└── styles
├── AppStyles.js
├── EditTodoFormStyles.js
├── TodoFormStyles.js
└── TodoStyles.js

Styling (Use JSS over CSS)

We’ll use Material-UI’s styling solution (CSS-in-JS) instead of traditional CSS. It might seem a little overkill to use JSS in a todo list app, but it’s a good practice when you will scale your app someday. This way, you could cleanly separate components’ logic and styles.

Install the dependencies.

npm install --save @material-ui/core
npm install --save @material-ui/styles

I’ll show you how to create and style our components by App.js and AppStyles.js. We export JavaScript objects for styling and import them in our component files, respectively.

For other components, the logic is as same as this example. Also, I omit the -Styles suffix files later in this tutorial, but you could find the source code in my repository.

Create src/styles/AppStyles.js and src/components/App.js (remove src/App.js):

A CSS-in-JS solution
The App.js boilerplate

Import the styles in App.js and use const classes = useStyles() to get the value in the function.

For example, you used to type at line 9:

<header className="header">

However, we do not use CSS in our app, and we have to write className={classes.header} because classes is now a JS object.

Make sure to change the importing in your src/index.js to

import App from './components/App';

Design the blueprint (context and reducer)

Before moving on, let’s take a look at what we need. Traditionally, we have to keep states and define methods in our central parent component (TodoApp in this case.) In this tutorial, we’ll use Context and Hooks to simplify the logic and centralize the states and methods.

We need a context file to centralize the props. Also, a reducer could reduce the complexity a lot.

Let’s create src/reducers/todos.reducer.js and src/contexts/todos.context.js:

A centralized place to track the state of todos

There’s nothing special; pass in a state and return it.

A conventional way to create a context

We have to createContext and export it, so we could import this context and use it later.

As for theuseReducer, it takes two parameters, the todosReducer we just created and initializerArg (default value). It’ll return an array of the state (todos) and a method to update that state, which we are still not using in this commit.

Import TodoApp in App.js:

Add TodoApp in App.js.

Create TodoApp.js and TodoList.js:

All subcomponents of TodoList can consume the context of TodosProvider.
A TodoList that consumes todos in the context

In TodoList, useContext(TodosContext) provides the todos in the TodosContext for us.

Now, you should see some dummy todos listed in your browser.

Create the TodoForm

Import TodoForm in TodoApp.js:

Add TodoForm in TodoApp.js.

useInputState.js

We could customize a hook to write less code. Create src/hooks/useInputState.js:

A customized hook that handles form binding

We create a reusable hook that requires an initialValue, in which it generates a general state, value, and a general method,setValue. If we want to use the input hook later on (you’ll see it in this tutorial), we don’t bother to write duplicate code.

TodoForm should have the ability to add a todo, so we require our reducer to handle this action for us. Create src/constants/actions.js to centralize all actions’ constants.

Modify todos.reducer.js as the following. Here, we use uuid to generate a unique id for each todo.

Use `uuid to generate a unique id for each todo.

Also, we have to modify the todos.context.js. DispatchContext provides dispatch action, which is returned from useReducer(todosReducer, defaultTodos), for children components.

Add DispatchContext and the returned value (dispatch) of useReducer(todosReducer, defaultTodos).

Now, create TodoForm.js and TodoFormStyles.js:

With the helper hook, we make our `TodoForm` both tiny and cute.

Create the Todo component

Add Font Awesome CSS link in public/index.html:

Add constants in actions and actions of removing a todo and toggling a todo in todos.reducer.js:

Import Todo in TodoList:

Finally, create Todo.js and TodoStyles.js:

We use memo to make Todo a pure component, preventing unnecessary re-rendering. The dispatch in Todo works similarly to TodoForm; they both consume the context provided by DispatchContext.

Add the EditTodoForm

Now, we need another helper hook to toggle showing a EditTodoForm or showing a Todo.

Create src/hooks/useToggleState.js:

In our Todo.js, add the following lines:

We also need the action of editing a todo. Add constants in src/constants/actions.js and add the following lines in src/reducers/todos.reducer.js:

Finally, we can create EditTodoForm.js and EditTodoFormStyles.js:

You see! We again use the customized hook useInputState, which was created for TodoForm. Centralization of a single hook allows us to avoid writing duplicate code and reduce the chance to have bugs.

Persist our data in the browser

Open the console in the browser (⌘ + ⌥ + I in macOS), type window.localStorage, and press enter. You’ll find that there’s nothing in the localStoarge, and that’s the place in which we could store our data.

Create src/hooks/useLocalStorageReducer.js:

We hook our reducer by giving it the third parameter.

Pass in a parameter key to the hook to acknowledge our reducer that we want to store our data with a specific key.

Therefore, instead of directly using the defaultValue, the reducer will try to parse window.localStorage.getItem(key) first, and only if it fails will it use the defaultValue.

Also, useEffect will update the value of window.localStorage.key when either key or state is updated.

Modify todos.context.js:

We no longer have to use the default useReducer. Instead, we could now use the hook reducer we just created. Furthermore, we have to specify the key we want to use. I’ll use 'todos' here; feel free to use any key name.

Now, try to modify some todos or delete them; then, open the console in the browser, type window.localStorage, and press enter again. You’ll find that there’re data in our localStoarge . Refresh the page again, the todos are still there!

Type window.localStorage.clear(), then refresh the page, and you’ll find that the default todos are back.

Make our app responsive

Now, the app looks ugly in some screen sizes. We could create a helper function sizes to address the problem.

Create helpers/sizes.js:

A helper function to make the app responsive

Add the following lines in AppStyles.js:

Remember to adjust App.js,

We are done! You can see our app is now responsive to different screen sizes. Of course, you could adjust the value if you’d like.

Deploy our app to GitHub Pages

  1. Install the dependency:
npm install -save gh-pages --save-dev

2. Add the home page and the following scripts in package.json

  "private": true,
+ "homepage": "https://{yourname}.github.io/react-hooks-todolist",
"dependencies": {
... },
"scripts": {
+ "predeploy": "npm run build",
+ "deploy": "gh-pages -d build",
"start": "react-scripts start",
...

3. Run npm run deploy to deploy the app.

It might take about 10 to 20 minutes for GitHub to host your page. Be patient, and grab some food. You’ll see your page in https://{yourname}.github.io/react-hooks-todolist soon! 😃

Conclusion

In this tutorial, we learned the following

  • using JSS to style our app,
  • making reusable hooks,
  • centralizing our props and methods (dispatch) by React Context,
  • making our app responsive in a clever way,
  • persisting our data in the browser, and
  • deploying our app to GitHub Pages.

This is my first time writing a tutorial and I hope you guys enjoy it. Thank you!

References

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store