Published on 1.8.2019
React Hooks have revolutionized how we write components in React. Gone are the days of class components for managing state and side effects — now we can achieve all of that and more with the power of hooks like useState
and useEffect
. In this tutorial, we’ll walk through how to build a simple Todo app using React Hooks, and we’ll add local storage support so that the tasks persist across page reloads.
By the end of this tutorial, you’ll understand how to:
- Use the
useState
hook to manage state in your app. - Use the
useEffect
hook to sync your state with local storage. - Build a simple CRUD (Create, Read, Update, Delete) app with React.
Let’s get started!
Step 1: Setting Up the Project
To begin, create a new React project (if you don’t already have one):
npx create-react-app todo-hooks
cd todo-hooks
npm start
Now, you’re ready to start building your Todo app!
Step 2: Creating the Todo App Structure
In the src
folder, open the App.js
file and start by building the basic layout for the app. We’ll create a list of todos, an input field to add new todos, and buttons to handle completing and deleting tasks.
App.js
import React, { useState, useEffect } from 'react';
import './App.css';
const App = () => {
// State for storing the todo items
const [todos, setTodos] = useState([]);
// State for the current input value
const [input, setInput] = useState('');
// useEffect to load todos from localStorage
useEffect(() => {
const savedTodos = JSON.parse(localStorage.getItem('todos'));
if (savedTodos) {
setTodos(savedTodos);
}
}, []);
// useEffect to save todos to localStorage
useEffect(() => {
if (todos.length) {
localStorage.setItem('todos', JSON.stringify(todos));
}
}, [todos]);
// Function to add a new todo
const addTodo = (e) => {
e.preventDefault();
if (!input.trim()) return; // Prevent adding empty todos
setTodos([...todos, { text: input, completed: false }]);
setInput(''); // Clear the input after adding the todo
};
// Function to toggle the completion status of a todo
const toggleComplete = (index) => {
const updatedTodos = [...todos];
updatedTodos[index].completed = !updatedTodos[index].completed;
setTodos(updatedTodos);
};
// Function to delete a todo
const deleteTodo = (index) => {
const updatedTodos = todos.filter((_, i) => i !== index);
setTodos(updatedTodos);
};
return (
<div className="App">
<h1>Todo App with Hooks</h1>
<form onSubmit={addTodo}>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Add a new task"
/>
<button type="submit">Add Todo</button>
</form>
<ul>
{todos.map((todo, index) => (
<li key={index} className={todo.completed ? 'completed' : ''}>
<span onClick={() => toggleComplete(index)}>{todo.text}</span>
<button onClick={() => deleteTodo(index)}>Delete</button>
</li>
))}
</ul>
</div>
);
};
export default App;
Explanation:
- State Management with
useState
:todos
: An array to hold the todo items.input
: A string for the value of the input field.
- Using
useEffect
for Local Storage:- The first
useEffect
loads the todos fromlocalStorage
when the component mounts (this only runs once). - The second
useEffect
updateslocalStorage
whenever thetodos
state changes.
- The first
- Handling Todo Operations:
addTodo
: Adds a new todo to thetodos
array. It also clears the input field after the todo is added.toggleComplete
: Toggles thecompleted
property of a todo, changing its style when clicked.deleteTodo
: Removes a todo from the list by filtering out the item at the specified index.
Step 3: Styling the Todo List
You can add some basic styles in App.css
to make the app look a bit more polished.
App.css
.App {
font-family: Arial, sans-serif;
width: 300px;
margin: 0 auto;
padding: 20px;
background-color: #f4f4f4;
border-radius: 8px;
}
input {
padding: 10px;
margin-right: 10px;
width: 200px;
border-radius: 4px;
border: 1px solid #ccc;
}
button {
padding: 10px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
ul {
list-style-type: none;
padding: 0;
}
li {
padding: 10px;
margin-bottom: 8px;
background-color: #fff;
border: 1px solid #ddd;
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 4px;
}
.completed {
text-decoration: line-through;
color: #bbb;
}
span {
cursor: pointer;
}
li button {
background-color: #f44336;
color: white;
border-radius: 4px;
padding: 5px 10px;
cursor: pointer;
}
li button:hover {
background-color: #e53935;
}
Explanation of Styles:
input
andbutton
: Styled to make the input field and buttons look nicer.li
: Each todo item is displayed in aflex
container with a delete button.completed
: Todos that are marked as completed get a line-through style and a lighter color.
Step 4: Testing the App
At this point, your app is functional, and you can try adding, completing, and deleting todos. The todos will persist even after refreshing the page, thanks to localStorage
.
How It Works:
- When the page loads, the todos are fetched from
localStorage
and displayed. - When a new todo is added, it’s saved in both the state and
localStorage
. - When a todo is toggled as completed or deleted, the state updates and so does
localStorage
.
Conclusion
You’ve just built a simple Todo app using React Hooks, leveraging useState
for state management and useEffect
for side effects like syncing with local storage. This pattern can be applied to more complex CRUD applications, and you can easily extend this app by adding features like:
- Editing todos
- Sorting or filtering todos
- Adding due dates or priorities
This approach using React Hooks provides a clean, easy-to-understand way of managing state and side effects in your applications.
Happy coding!