React Project: Todo App
This React project ties the whole tutorial together. You will build a todo app that adds, completes, deletes and filters tasks, and saves them in localStorage so they survive a page reload.
Create a fresh Vite project and replace src/App.jsx with the code below.
The TodoItem Component
TodoItem only displays data and reports events through callback props. It owns no state.
function TodoItem({ todo, onToggle, onDelete }) {
return (
<li>
<label style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
<input type="checkbox" checked={todo.done} onChange={() => onToggle(todo.id)} />
{todo.text}
</label>
<button onClick={() => onDelete(todo.id)} aria-label="Delete">✕</button>
</li>
);
}☐ Buy milk ✕
The App Component
import { useState, useEffect } from 'react';
const FILTERS = {
all: () => true,
active: (t) => !t.done,
done: (t) => t.done,
};
export default function App() {
const [todos, setTodos] = useState(() =>
JSON.parse(localStorage.getItem('todos') ?? '[]')
);
const [text, setText] = useState('');
const [filter, setFilter] = useState('all');
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
function addTodo(e) {
e.preventDefault();
if (!text.trim()) return;
setTodos([...todos, { id: crypto.randomUUID(), text: text.trim(), done: false }]);
setText('');
}
const toggle = (id) =>
setTodos(todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
const remove = (id) => setTodos(todos.filter((t) => t.id !== id));
const visible = todos.filter(FILTERS[filter]);
const left = todos.filter((t) => !t.done).length;
return (
<main>
<h1>Todos</h1>
<form onSubmit={addTodo}>
<input value={text} onChange={(e) => setText(e.target.value)} placeholder="What needs doing?" />
<button>Add</button>
</form>
<ul>
{visible.map((t) => (
<TodoItem key={t.id} todo={t} onToggle={toggle} onDelete={remove} />
))}
</ul>
<p>{left} item{left === 1 ? '' : 's'} left</p>
{Object.keys(FILTERS).map((f) => (
<button key={f} disabled={filter === f} onClick={() => setFilter(f)}>{f}</button>
))}
</main>
);
}Todos [What needs doing?] [Add] ☑ Learn JSX ✕ ☐ Learn hooks ✕ 1 item left [all] [active] [done]
How It Works
- todos, text and filter are state; visible and left are derived values computed during render.
- useState with a function reads localStorage only once, on the first render.
- useEffect saves todos every time the array changes.
- crypto.randomUUID() gives each todo a stable, unique key.
- Updates create new arrays with spread, map and filter instead of mutating.
Exercises
- Add an "Clear completed" button that removes all done todos.
- Let users edit a todo by double-clicking its text.
- Move todos into a useReducer with add, toggle, delete and clear actions.
- Extract the storage logic into a useLocalStorage custom hook.
- Add React Router with /active and /done URLs instead of filter buttons.
Frequently asked questions
What are good React projects for beginners?
A todo list, a weather app using a public API, a quiz game, a notes app with localStorage, and a personal portfolio are classic beginner projects that cover state, effects and lists.
How do I save React state to localStorage?
Initialize state from localStorage with a lazy function in useState, then use an effect that writes the state back with JSON.stringify whenever it changes.
How do I deploy a React Vite app?
Run npm run build and upload the dist folder to a static host like Netlify, Vercel or GitHub Pages. Many hosts can build directly from your Git repository.