Building a realtime to-do app using React Hooks
You will need Create React App installed on your machine.
Note: To try out React Hooks, you will need the alpha build of React (at time of publication)
Introduction
In this article, we will build a todo application using functional components and Hooks to manage state, here’s a display of what we will have at the end of this tutorial:
In a previous article, we introduced React Hooks and looked at some ways to use the useState()
and useEffect()
methods. If you aren’t already familiar with these methods and their uses, please refer to this article.
Let’s get started.
Prerequisites
To follow along with this tutorial, you’ll need the following tool create-react-app installed.
To get the most out of this tutorial, you need knowledge of JavaScript and the React framework. If you want to play around with React Hooks, you will need the alpha build of React as this feature is still in alpha (as at the time of writing this article).
Setup
Let’s create a new React application using the create-react-app CLI tool:
$ npx create-react-app react-todo-hooks
$ cd react-todo-hooks
$ npm install --save react@16.7.0-alpha.2 react-dom@16.7.0-alpha.2
$ npm start
We run the command on the third line because we want to install specific versions of
react
andreact-dom
(currently in alpha) in order to tap into React Hooks
Running the last command will start the development server on port 3000 and open up a new page on our web browser:
We will create a components
folder in the src
directory and add two files within it:
Todo.js
- This is where all of our functional components will go.Todo.css
- This is where the styles for the application will go.
Open the Todo.css
file and paste in the following CSS:
/* File: src/components/Todo.css */
body {
background: rgb(255, 173, 65);
}
.todo-container {
background: rgb(41, 33, 33);
width: 40vw;
margin: 10em auto;
border-radius: 15px;
padding: 20px 10px;
color: white;
border: 3px solid rgb(36, 110, 194);
}
.task {
border: 1px solid white;
border-radius: 5px;
padding: 0.5em;
margin: 0.5em;
}
.task button{
background: rgb(12, 124, 251);
border-radius: 5px;
margin: 0px 5px;
padding: 3px 5px;
border: none;
cursor: pointer;
color: white;
float: right;
}
.header {
margin: 0.5em;
font-size: 2em;
text-align: center;
}
.create-task input[type=text] {
margin: 2.5em 2em;
width: 80%;
outline: none;
border: none;
padding: 0.7em;
}
Now we want to create two functional components in the Todo.js
file:
// Todo.js
import React, { useState } from 'react';
import './Todo.css';
function Task({ task }) {
return (
<div
className="task"
style={{ textDecoration: task.completed ? "line-through" : "" }}
>
{task.title}
</div>
);
}
function Todo() {
const [tasks, setTasks] = useState([
{
title: "Grab some Pizza",
completed: true
},
{
title: "Do your workout",
completed: true
},
{
title: "Hangout with friends",
completed: false
}
]);
return (
<div className="todo-container">
<div className="header">TODO - ITEMS</div>
<div className="tasks">
{tasks.map((task, index) => (
<Task
task={task}
index={index}
key={index}
/>
))}
</div>
</div>
);
}
export default Todo;
At the beginning of this snippet, we pulled in useState
from the React library because we need it to manage the state within our functional components. Next, the Task component returns some JSX to define what each task element will look like.
In the Todo component, the useState
function returns an array with two elements. The first item being the current state value for the tasks and the second being a function that can be used to update the tasks:
const [tasks, setTasks] = useState([
{
title: "Grab some Pizza",
completed: true
},
{
title: "Do your workout",
completed: true
},
{
title: "Hangout with friends",
completed: false
}
]);
We finally return some JSX within the Todo component and nest the Task component.
Running the application
For us to see what we’ve done so far, we have to update the index.js
file so that it knows where our Todo component is and how to render it to the DOM. Open the index.js
file and update it with the following snippet:
// File: index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import Todo from './components/Todo';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(<Todo />, document.getElementById('root'));
serviceWorker.unregister();
Now we can save the file and start the development server (if it isn’t already running):
We get three hard-coded tasks, two of which are complete and one that isn’t. In the next section, we will work towards making the application interactive and able to receive input from the user.
Creating a new task
Our application currently works with hard-coded data and has no way to receive input in realtime, we will change that now. Let’s create a new functional component and call it CreateTask
:
// Todo.js
// [...]
function CreateTask({ addTask }) {
const [value, setValue] = useState("");
const handleSubmit = e => {
e.preventDefault();
if (!value) return;
addTask(value);
setValue("");
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
className="input"
value={value}
placeholder="Add a new task"
onChange={e => setValue(e.target.value)}
/>
</form>
);
}
// [..]
Using useState
, this component registers a state — value — and a function for updating it — setValue. The handleSubmit
handler will prevent the default action that would normally be taken on the form and add a new Task using the latest value that is in the input field.
The CreateTask
component receives a prop addTask
, which is basically the function that adds a new task to the tasks state on the Todo component. We want to define this function and also update the JSX of the Todo component so it includes the CreateTask
component. Let’s completely replace the code for the Todo component with this one:
// File: Todo.js
// [...]
function Todo() {
const [tasks, setTasks] = useState([
{
title: "Grab some Pizza",
completed: true
},
{
title: "Do your workout",
completed: true
},
{
title: "Hangout with friends",
completed: false
}
]);
const addTask = title => {
const newTasks = [...tasks, { title, completed: false }];
setTasks(newTasks);
};
return (
<div className="todo-container">
<div className="header">TODO - ITEMS</div>
<div className="tasks">
{tasks.map((task, index) => (
<Task
task={task}
index={index}
key={index}
/>
))}
</div>
<div className="create-task" >
<CreateTask addTask={addTask} />
</div>
</div>
);
}
// [..]
We’ve included the addTask method here:
const addTask = title => {
const newTasks = [...tasks, { title, completed: false }];
setTasks(newTasks);
};
We can now save our changes and start the development server again (if it isn’t already running):
Now we have a nice input box where we can put in new values to create new tasks for the Todo application.
Completing a task
At this point, we need to be able to indicate that we have completed a task. Our tasks object in the Todo component already makes that possible as there is a completed
key-value pair. What we need now is an interactive way for the user to set a task as completed without hard-coding the data.
The first thing we will do here is to update the Task component to receive a new prop and include a Complete
button:
// Todo.js
// [...]
function Task({ task, index, completeTask }) {
return (
<div
className="task"
style={{ textDecoration: task.completed ? "line-through" : "" }}
>
{task.title}
<button onClick={() => completeTask(index)}>Complete</button>
</div>
);
}
// [..]
Then we will also update the Todo component to define the completeTask
method and pass it down as a prop to the Task component in the JSX:
// File: Todo.js
// [...]
function Todo() {
const [tasks, setTasks] = useState([
{
title: "Grab some Pizza",
completed: true
},
{
title: "Do your workout",
completed: true
},
{
title: "Hangout with friends",
completed: false
}
]);
const addTask = title => {
const newTasks = [...tasks, { title, completed: false }];
setTasks(newTasks);
};
const completeTask = index => {
const newTasks = [...tasks];
newTasks[index].completed = true;
setTasks(newTasks);
};
return (
<div className="todo-container">
<div className="header">TODO - ITEMS</div>
<div className="tasks">
{tasks.map((task, index) => (
<Task
task={task}
index={index}
completeTask={completeTask}
key={index}
/>
))}
</div>
<div className="create-task" >
<CreateTask addTask={addTask} />
</div>
</div>
);
}
// [...]
We can now start the development server and see what new features have been added:
Now we can click on a complete button to indicate that we have finished executing a task!
Removing a task
Another wonderful feature to include to the Todo application is an option to completely remove a task whether it has been completed or not. We can do this in similar steps like the ones we used in creating the complete feature.
Let’s start by updating the Task component to receive a removeTask
prop and include an “X” button that deletes a task on click:
// File: Todo.js
// [...]
function Task({ task, index, completeTask, removeTask }) {
return (
<div
className="task"
style={{ textDecoration: task.completed ? "line-through" : "" }}
>
{task.title}
<button style={{ background: "red" }} onClick={() => removeTask(index)}>x</button>
<button onClick={() => completeTask(index)}>Complete</button>
</div>
);
}
// [...]
Now we can update the Todo component to register the removeTask
method and pass it down as a prop to the Task component in the JSX:
// File: Todo.js
// [...]
function Todo() {
const [tasks, setTasks] = useState([
{
title: "Grab some Pizza",
completed: true
},
{
title: "Do your workout",
completed: true
},
{
title: "Hangout with friends",
completed: false
}
]);
const addTask = title => {
const newTasks = [...tasks, { title, completed: false }];
setTasks(newTasks);
};
const completeTask = index => {
const newTasks = [...tasks];
newTasks[index].completed = true;
setTasks(newTasks);
};
const removeTask = index => {
const newTasks = [...tasks];
newTasks.splice(index, 1);
setTasks(newTasks);
};
return (
<div className="todo-container">
<div className="header">TODO - ITEMS</div>
<div className="tasks">
{tasks.map((task, index) => (
<Task
task={task}
index={index}
completeTask={completeTask}
removeTask={removeTask}
key={index}
/>
))}
</div>
<div className="create-task" >
<CreateTask addTask={addTask} />
</div>
</div>
);
}
// [...]
We can now test out the new functionality:
Great, we have a fully functional Todo application that is built off functional components only. We will add an additional feature in the next section.
Using useEffect to monitor the number of uncompleted tasks remaining
In this section, we will use the useEffect
state Hook to update the number of pending tasks whenever the DOM is re-rendered. You can learn more about the useEffect
hook here.
First of all, we need to pull in useEffect
from the react library:
import React, { useState, useEffect } from 'react';
Then we will register a new state Hook for the pending tasks in the Todo component:
const [tasksRemaining, setTasksRemaining] = useState(0);
We will also add an effect hook to update the state of tasksRemaining
when the DOM re-renders:
useEffect(() => { setTasksRemaining(tasks.filter(task => !task.completed).length) });
Finally, we will update the JSX in the Todo component to reactively display the number of pending tasks. Here’s what the Todo component should look like:
// File: Todo.js
// [...]
function Todo() {
const [tasksRemaining, setTasksRemaining] = useState(0);
const [tasks, setTasks] = useState([
{
title: "Grab some Pizza",
completed: true
},
{
title: "Do your workout",
completed: true
},
{
title: "Hangout with friends",
completed: false
}
]);
useEffect(() => {
setTasksRemaining(tasks.filter(task => !task.completed).length)
});
const addTask = title => {
const newTasks = [...tasks, { title, completed: false }];
setTasks(newTasks);
};
const completeTask = index => {
const newTasks = [...tasks];
newTasks[index].completed = true;
setTasks(newTasks);
};
const removeTask = index => {
const newTasks = [...tasks];
newTasks.splice(index, 1);
setTasks(newTasks);
};
return (
<div className="todo-container">
<div className="header">Pending tasks ({tasksRemaining})</div>
<div className="tasks">
{tasks.map((task, index) => (
<Task
task={task}
index={index}
completeTask={completeTask}
removeTask={removeTask}
key={index}
/>
))}
</div>
<div className="create-task" >
<CreateTask addTask={addTask} />
</div>
</div>
);
}
// [...]
We can test that the application displays the pending tasks correctly:
Conclusion
In this tutorial, we have learned how we can create a simple todo application using React Hooks. Hooks are a very welcome feature to React and it allows new levels of modularization that was not previously possible in React.
The source code to the application built in this article is on GitHub.
8 February 2019
by Neo Ighodaro