Mini Project: Todo App

Time to put it all together. You'll build a fully functional Todo app using everything you've learned: components, props, state, events, lists, and forms.

What You'll Build

  • Add new todos with a text input
  • Mark todos as complete/incomplete
  • Delete todos
  • Filter by All / Active / Completed
  • Show a count of remaining tasks
?

Before looking at the solution, try building it yourself! Start with the state shape, then the UI, then the handlers.

Step 1 — Plan the State

Think about what data your app needs before writing any JSX:

state-plan.js
// Before writing JSX, plan your state:
const state = {
  todos: [
    // Each todo is an object with a stable ID
    { id: 1, text: "Learn React", completed: true },
    { id: 2, text: "Build something", completed: false },
  ],
  inputText: "",    // controlled input value
  filter: "all",   // "all" | "active" | "completed"
};

// The operations we need:
// addTodo(text)        -> create new todo, clear input
// toggleTodo(id)       -> flip completed boolean
// deleteTodo(id)       -> remove from array
// setFilter(filter)    -> change which todos show

Step 2 — The Complete App

TodoApp.jsx
import { useState } from 'react';

// Unique ID generator
let nextId = 1;
const uid = () => nextId++;

function TodoApp() {
  const [todos, setTodos] = useState([
    { id: uid(), text: 'Learn React components', completed: true },
    { id: uid(), text: 'Understand useState', completed: true },
    { id: uid(), text: 'Build a todo app', completed: false },
  ]);
  const [input, setInput] = useState('');
  const [filter, setFilter] = useState('all');

  function addTodo(e) {
    e.preventDefault();
    const text = input.trim();
    if (!text) return;
    setTodos([...todos, { id: uid(), text, completed: false }]);
    setInput('');
  }

  function toggleTodo(id) {
    setTodos(todos.map(t =>
      t.id === id ? { ...t, completed: !t.completed } : t
    ));
  }

  function deleteTodo(id) {
    setTodos(todos.filter(t => t.id !== id));
  }

  const visible = todos.filter(t => {
    if (filter === 'active') return !t.completed;
    if (filter === 'completed') return t.completed;
    return true;
  });

  const remaining = todos.filter(t => !t.completed).length;

  return (
    <div className="todo-app" style={{ maxWidth: 480, margin: '2rem auto', padding: '0 1rem' }}>
      <h1>Todos</h1>

      {/* Input form */}
      <form onSubmit={addTodo} style={{ display: 'flex', gap: 8, marginBottom: '1rem' }}>
        <input
          value={input}
          onChange={e => setInput(e.target.value)}
          placeholder="What needs to be done?"
          style={{ flex: 1, padding: '0.5rem 0.75rem', borderRadius: 6, border: '1px solid #444', background: '#1a1a2e', color: '#e6edf3' }}
        />
        <button type="submit" style={{ padding: '0.5rem 1rem', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer' }}>
          Add
        </button>
      </form>

      {/* Filter tabs */}
      <div style={{ display: 'flex', gap: 4, marginBottom: '1rem' }}>
        {['all', 'active', 'completed'].map(f => (
          <button key={f} onClick={() => setFilter(f)}
            style={{
              padding: '4px 12px', borderRadius: 4, border: '1px solid #444',
              background: filter === f ? '#3b82f6' : 'transparent',
              color: filter === f ? '#fff' : '#8b949e',
              cursor: 'pointer', fontSize: '0.85rem', textTransform: 'capitalize'
            }}>
            {f}
          </button>
        ))}
      </div>

      {/* Todo list */}
      {visible.length === 0 ? (
        <p style={{ color: '#8b949e', textAlign: 'center', padding: '2rem' }}>
          {filter === 'completed' ? 'Nothing completed yet.' : 'Nothing to do!'}
        </p>
      ) : (
        <ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
          {visible.map(todo => (
            <li key={todo.id} style={{
              display: 'flex', alignItems: 'center', gap: 10,
              padding: '0.75rem 1rem', borderBottom: '1px solid #30363d'
            }}>
              <input type="checkbox" checked={todo.completed}
                onChange={() => toggleTodo(todo.id)}
                style={{ width: 16, height: 16, cursor: 'pointer', accentColor: '#10b981' }}
              />
              <span style={{
                flex: 1,
                textDecoration: todo.completed ? 'line-through' : 'none',
                color: todo.completed ? '#484f58' : '#e6edf3'
              }}>
                {todo.text}
              </span>
              <button onClick={() => deleteTodo(todo.id)}
                style={{ background: 'none', border: 'none', color: '#484f58', cursor: 'pointer', fontSize: '1rem' }}
                title="Delete">
                ?
              </button>
            </li>
          ))}
        </ul>
      )}

      {/* Footer */}
      <div style={{ marginTop: '0.75rem', fontSize: '0.8rem', color: '#8b949e', display: 'flex', justifyContent: 'space-between' }}>
        <span>{remaining} item{remaining !== 1 ? 's' : ''} left</span>
        {todos.some(t => t.completed) && (
          <button onClick={() => setTodos(todos.filter(t => !t.completed))}
            style={{ background: 'none', border: 'none', color: '#8b949e', cursor: 'pointer', fontSize: '0.8rem' }}>
            Clear completed
          </button>
        )}
      </div>
    </div>
  );
}

export default TodoApp;

Step 3 — Understanding the Pattern

patterns.js
// Key React patterns used in the Todo App:

// 1. Immutable state updates
// Never mutate — always create new arrays/objects
setTodos([...todos, newTodo]);                     // add
setTodos(todos.filter(t => t.id !== id));          // remove
setTodos(todos.map(t => t.id === id ? {...t, completed: !t.completed} : t)); // update

// 2. Derived state (don't store what you can compute)
// BAD: store a separate filteredTodos in state
// GOOD: compute it from todos + filter during render
const visible = todos.filter(t => filter === 'all' || t.status === filter);

// 3. Controlled form with clear-on-submit
function addTodo(e) {
  e.preventDefault();
  if (!input.trim()) return; // guard against empty
  setTodos([...todos, { id: uid(), text: input.trim(), completed: false }]);
  setInput(''); // clear the input
}

// 4. Lifting state up
// All state lives in the top component.
// Children receive data as props and call handlers to request changes.
// Children never modify data directly.

What's Next?

You've mastered the fundamentals of React. From here, explore:

  • React Router — multiple pages in a single-page app
  • useContext — share state without prop drilling
  • useReducer — manage complex state transitions
  • Custom Hooks — extract and reuse stateful logic
  • Next.js — React with server-side rendering and file-based routing
  • TanStack Query — powerful data fetching and caching
??

Congratulations on completing React Fundamentals! You now have the core skills to build real React applications.