Build a todo list app with React Hooks and Context
Performant, brilliant, and highly maintainable

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
):
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
:
There’s nothing special; pass in a state and return it.
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
:
Create TodoApp.js
and TodoList.js
:
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
:
useInputState.js
We could customize a hook to write less code. Create src/hooks/useInputState.js
:
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.
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.
Now, create TodoForm.js
and TodoFormStyles.js
:
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
:
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
- 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!