Home/Blog/Programming/React Hooks tutorial: Build a to-do list with React Hooks
Home/Blog/Programming/React Hooks tutorial: Build a to-do list with React Hooks

React Hooks tutorial: Build a to-do list with React Hooks

12 min read
Feb 26, 2025
content
Overview of React Hooks
Benefits of using hooks
The class component in React
Functional components in React
React to-do list
Step 1: Create a React application
Step 2: Set up the App component
Step 3: Create the Header component
Step 4: Add the mock data
Step 5: Read the list of to-do items and display them
The basic syntax for the useState() hook
Step 6: Toggle task completion
Step 7: Clear completed tasks
Step 8: Add new tasks with a form
Complete application
What to learn next
Continue learning the useState hook

Become a Software Engineer in Months, Not Years

From your first line of code, to your first day on the job — Educative has you covered. Join 2M+ developers learning in-demand programming skills.

Key takeaways:

  • The useState hook allows functional components to manage the local state efficiently, eliminating the need for class components and making code simpler, more readable, and modular.

  • Structuring your application into reusable components like Header, ToDoList, ToDo, and ToDoForm enhances code maintainability and scalability, making managing and extending your to-do list application easier.

  • React’s optimized rerendering ensures that only the necessary parts of the DOM update when the state changes, providing a smooth and responsive user experience in your to-do list app.

  • Implementing features like task completion toggling and clearing completed tasks enhance user engagement and functionality, making your to-do list application more practical and user-friendly.

With the release of React 16.8 in 2019, React Hooks finally became available for use in production applications. This allows React developers to make functional components stateful. Instead of using a class component to hold stateful logic, we can use functional components.

Have you ever struggled with managing state in React class components or found your code cluttered with life cycle methods? React Hooks simplifies state management and streamlines the component-building process. They are a powerful tool; we’ll start by building a to-do list to better understand them, specifically focusing on the useState hook.

Note: It is assumed you already know at least the basics of React. If you’re new to learning React, that’s okay. Check out our React tutorial for beginners before continuing here.

Overview of React Hooks#

In React, hooks allow you to hook into React state and life cycle features from function components. This allows you to use React without classes.

When you take an initial look at the React Hooks documentation, you’ll see that there are several hooks that we can use for our applications. You can even create your own. Some of the popular ones include:

  • useState: Adds state to functional components.

  • useEffect: Performs side effects in function components.

  • useContext: Accepts a context object and returns the current context value.

  • useCallback: Returns a memoized callback.

Benefits of using hooks#

React Hooks offers several advantages that can improve the structure and maintainability of our application. Here are some of the key benefits:

  • Hooks enable you to manage state and side effects in functional components, eliminating the need for class components.

  • Stateful logic implemented with hooks is easier to isolate and test, as it doesn’t depend on the React life cycle or class-based structure.

  • You can share logic between components without using props or higher-order components.

  • Hooks separate logic based on its purpose (e.g., state, side effects) rather than splitting it by life cycle methods, leading to cleaner and more maintainable code.

The only hook we’ll need for this to-do list project is the useState hook. This hook replaces the need for a state object in a class component structure.

The class component in React #

When looking at older React legacy code, you will see something like the following:

The class component structure describes an instance of an App object with a state that is an array of movies. We render that array of movies by mapping over the state object and returning every movie within its own <div> element.

Functional components in React #

Stateful functional components are very similar in that they hold state but are much simpler. Look at the following example:

The useState hook is deconstructed into an array with two items in it:

  • The variable that holds our state (movies)

  • A method that is used to update that state if you need to (setMovies)

The useState hook creates and initializes a local state, movies, within the App component and ensures that the App component will rerender whenever setMovies is called. This way, any changes to movies will immediately appear in the component’s output.

Now that you have the basic idea behind the useState React Hook, let’s see how to use it when creating a to-do list application.

Explore the useState ook by implementing it in a real-world use case in this project, Build a Task Manager Using React.

React to-do list#

Our goal is to create a to-do list UI with the following components:

  1. Header: Labels the to-do list.

  2. To-do items list: Displays each to-do item. We’ll also create two additional capabilities for the list:

    1. Ability to strike through a task that indicates completion.

    2. Remove all completed tasks from the list with a button click.

  3. Form: Adds new to-do task item to the list.

The final layout of the application that we’ll create will look something like this:

Final layout
Final layout

Let’s start with a step-by-step guide on creating a to-do list in React using the useState hook.

Step 1: Create a React application#

The first step is to create a React application. You can use either Yarn or npm to set up your React project:

# Using Yarn
yarn create react-app todo-list
# Using npm
npx create-react-app todo-list
Create a React application

Navigate into the todo-list folder and start the application. Your project should now be running on http://localhost:3000.

cd todo-list
yarn start
# or
npm start
Start the application

Step 2: Set up the App component#

Navigate to the App.js file and clear the existing content inside the <div> tags. We won’t need any of the prepopulated codes. The App.js file should look something like this:

Step 3: Create the Header component#

A header enhances the application’s UI by clearly indicating its purpose. We’ll create a reusable Header component.

Create a new file, Header.js, in the src directory. Inside this file, create a functional component, Header, that returns JSX to render the header section. The JSX should display a header identifying your application’s name. Finally, export your Header component and import it to App.js.

Step 4: Add the mock data#

To ensure our app behaves as expected before connecting to a real API, we’ll use mock data to simulate tasks and test our application. This helps us focus on the core functionality without worrying about backend integration.

For this, create a file, data.json, in the src directory. Populate the data.json file with sample tasks using the JSON data given below:

[
{ "id": 1, "task": "Give dog a bath", "complete": true },
{ "id": 2, "task": "Do laundry", "complete": true },
{ "id": 3, "task": "Vacuum floor", "complete": false },
{ "id": 4, "task": "Feed cat", "complete": true },
{ "id": 5, "task": "Change light bulbs", "complete": false },
{ "id": 6, "task": "Go to Store", "complete": true },
{ "id": 7, "task": "Fill gas tank", "complete": true },
{ "id": 8, "task": "Change linens", "complete": false },
{ "id": 9, "task": "Rake leaves", "complete": true },
{ "id": 10, "task": "Bake Cookies", "complete": false },
{ "id": 11, "task": "Take nap", "complete": true },
{ "id": 12, "task": "Read book", "complete": true },
{ "id": 13, "task": "Exercise", "complete": false },
{ "id": 14, "task": "Give dog a bath", "complete": false },
{ "id": 15, "task": "Do laundry", "complete": false },
{ "id": 16, "task": "Vacuum floor", "complete": false },
{ "id": 17, "task": "Feed cat", "complete": true },
{ "id": 18, "task": "Change light bulbs", "complete": false },
{ "id": 19, "task": "Go to Store", "complete": false },
{ "id": 20, "task": "Fill gas tank", "complete": false }
]

Each task is an object with id, task, and complete properties.

  • id: It is a unique identifier for each task, crucial for React’s list rendering.

  • task: It is the description of the to-do item.

  • complete: It is a boolean indicating whether the task is completed.

You also need to import the mock data from the data.json file in the App.js file.

Step 5: Read the list of to-do items and display them#

Now that we have our data and header set up, we’ll create components to display the list of tasks. The first thing to do here is initializing a new state in the App component using the useState hook. This state will store our mock data for the component.

The basic syntax for the useState() hook#

import { useState } from 'react'; // import the hook
const [ variable, setVariable ] = useState(initialState);
The syntax for useState hook

We’ll create a new state, toDoList, and initialize it with the imported mock data. It will hold the current list of tasks, and setToDoList will be used to update this state.

Now, we need to map over the toDoList state to render each to-do item in our application. We’ll create two components to display the list of tasks.

Create two new files, ToDoList.js and ToDo.js in the src directory. The ToDoList.js file will have a component—ToDoList—that will serve as the container that holds all of our todos, and the ToDo.js file will have a component—ToDo— that will render an individual to-do item in our To-do list.

Let’s first look at the ToDoList.js file. In this file, we have the ToDoList component that receives toDoList as a prop from the parent component—App.js. It then iterates over the toDoList array and renders a ToDo component for each task. It also assigns a unique key to each ToDo component using the task’s id to help React optimize rendering.

Now, let’s look at the ToDo.js file. In this file, we have the ToDo component, which receives a single todo object as a prop and renders the task property of the todo object.

Now that both the components are set, we must import and use the ToDoList component in the App.js file.

Run the code above, and you should now see the “To Do List” header followed by a list of tasks, each displayed within its own row.

The Road to React: The One with Hooks

Cover
The Road to React: The One with Hooks

This is a relaunch of my existing course, The Road to Learn React. A lot has changed in React since I first created this course, and so here I am to give you all the information you need to work with modern React. (If you’re looking for content on legacy React, the old course is still available as well.) In this course you will take a deep dive into React fundamentals, covering all new React concepts including Hooks. I do address some legacy features in case you’re working with an older codebase, but the majority of this course will focus on working with modern React. You will learn how to style your app, techniques for maintaining your app, and some more advanced concepts like performance optimization. Throughout the course, you will gain hands-on experience by building a Hacker News app, and by the end of this course, you will be prepared to build your own applications and have something to showcase in your portfolio.

25hrs
Beginner
74 Playgrounds
13 Quizzes

Step 6: Toggle task completion#

We’ll allow users to mark tasks as complete or incomplete by clicking on them. Completed tasks will be visually distinguished with a strike-through.

We’ll update the ToDo.js file to conditionally apply a CSS class based on the task’s completion status. We add the attribute className that conditionally applies the strike class if todo.complete is true, otherwise applies no class.

const ToDo = ({todo}) => {
return (
<div className={todo.complete ? "strike" : ""}>
{todo.task}
</div>
);
};

Note: Anything in between curly braces when using JSX signals that we are using JavaScript functions.

In the styles.css file, we add the strike class which applies a line-through to indicate completion, and changes the text color to gray for better visibility.

.strike {
text-decoration: line-through;
color: gray;
}

If you were to look at your React application, you would see some tasks with a line indicating that a project or task has been completed.

Next, we have to create a function, handleToggle, that will toggle the complete status of a task. As our state resides there, we’ll implement this function in the App.js file.

Practice using multiple states in a component with this project, Build an Image Sharing App with MERN Stack.

The handleToggle function accepts an id, maps through the toDoList, and toggles the complete status of the matching task, marking that task as complete or incomplete. We’ll use setToDoList to update the state with the modified task list, triggering a rerender.

const handleToggle = (id) => {
const updatedList = toDoList.map((task) =>
task.id === id ? { ...task, complete: !task.complete } : task
);
setToDoList(updatedList);
};

Note: setToDoList(updatedList) is analogous to this.setState({ toDoList: updatedList }) would have been used if we had worked with the state in class components.

We now need to pass handleToggle function to ToDoList and ToDo components. Once done, click any task to toggle its completion status. Completed tasks should display with a strike-through and gray color, while incomplete tasks remain normal. If you double-click the task, it will be marked as incomplete, depicting the change in the toggle state.

Step 7: Clear completed tasks#

What will we do with all those crossed-off, completed tasks? Let’s remove them from the list. We’ll create a button with an onClick handler that filters out all of the completed items.

This functionality is similar to the toggle functionality we just did. We’ll create a function, handleFilter that will filter out tasks where complete is true, effectively removing all completed tasks from the list. It will then update the toDoList state with the filtered list, triggering a rerender.

const handleFilter = () => {
const filteredList = toDoList.filter((task) => !task.complete);
setToDoList(filteredList);
};

We’ll then pass the handleFilter function to the ToDoList component.

Note: The JavaScript filter method returns a new array, so we are not in danger of mutating the state and can proceed without copying the array before we play with it.

Then, in the ToDoList component, we’ll add a button labeled “Clear Completed” below the list of tasks and set an onClick to fire the handleFilter function.

To test the functionality, mark a few tasks as complete by clicking on them. Then click the “Clear Completed” button to remove all completed tasks from the list.

Step 8: Add new tasks with a form#

The final step in our list is to create a form component for adding tasks to our to-do list. We’ll create a new file, ToDoForm.js, in the src directory for this.

In this file, a ToDoForm component will create a basic form allowing a user to input a task name and click a button to add the new task to the list. For a form to work correctly, we must keep track of the changes as we go, so logically, we have to handle what happens as the input changes.

The ToDoForm component will have the following:

  1. A state, userInput, will track any input the user types into their form.

const [ userInput, setUserInput ] = useState('');
  1. A function, handleChange, will handle the local state’s changes. When a user types in the input box, the state will reflect the most recent input.

const handleChange = (e) => {
setUserInput(e.target.value);
};
  1. A function, handleSubmit, is the default form submission behavior. It also checks if the input is not empty or has white space. It then calls an addTask function passed via props to add the new task (discussed later). Finally, it resets the input field to an empty string after submission.

const handleSubmit = (e) => {
e.preventDefault();
if (userInput.trim()) {
addTask(userInput);
setUserInput('');
}
};

Note: Remember to use e.preventDefault() when we use forms because we don’t want the default action to occur. In this case, it would reload the page, and everything changed will return to how it was initially rendered.

  1. A form container with an input field should have a value associated with it that matches the name of your state variable. It will also have a button that will call the relevant event handler to create the task.

<form onSubmit={handleSubmit} className="todo-form">
<input
type="text"
value={userInput}
onChange={handleChange}
placeholder="Enter task..."
className="todo-input"
/>
<button type="submit" className="add-button">
Add Task
</button>
</form>

With the ToDoForm component setup, we need to create the addTask function in the App component since that is where our toDoList state is. We need to be able to set the new array on state using setToDoList, and we can only do that when the addTask function has access to that state.

const addTask = (userInput) => {
const maxId = toDoList.reduce((max, task) => (task.id > max ? task.id : max), 0);
const newTask = {
id: maxId + 1,
task: userInput,
complete: false,
};
setToDoList([...toDoList, newTask]);
};
The addTask function

This function takes in userInput that we gathered from our form component’s current state. It then creates a new task object with a unique id and the task description from user input and sets complete to false. It uses the spread operator (...) to add the new task to the existing toDoList array, ensuring immutability.

Lastly, we’ll import the ToDoForm component in the App component and pass the addTask function as a prop.

Complete application#

Here is the complete to-do application with the functionality of creating and adding a new task to the to-do list.

What to learn next#

Congrats! You’ve now made a to-do list using the useState hooks. If you found this fairly straightforward, play around with the code and try to implement more functionality.

Here are some extra things you can do to give you some ideas:

  • Add the ability to create a due date for each task or a priority rating.

  • Give the ability to sort the list by the due date or priority.

  • Create a backend so your to-do list can persist.

  • Create a frontend interface for your own custom React app.

  • Style application using React-Bootstrap or CSS-in-JS.

  • Employ the Context API using the useContext hook instead of local state and props.

Continue learning the useState hook#

Explore these projects for hands-on practice on the useState hook to better understand managing state within functional components.

Frequently Asked Questions

How do I add persistence to my to-do list?

To add persistence, set up a backend using Node.js and a database like MongoDB or Firebase. This allows you to save your to-do list data, ensuring tasks persist across sessions and devices. Alternatively, you can use browser storage options like localStorage to store tasks locally on the user’s device for simpler needs.

Can useState hold complex data types?

How does useState differ from this.setState in class components?

How do I ensure unique keys when mapping lists?

Can I use multiple hooks in a single component?


Written By:
Hamna Waseem
Join 2.5 million developers at
Explore the catalog

Free Resources