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.