Build a Todo App with React and Context API

Build a Todo App with React and Context API

Build a Todo App with React and Context API

The classic Todo app built the right way — with clean state management, persistence, and filtering.

Features

  • Add, complete, and delete todos
  • Filter by All / Active / Completed
  • Persisted to localStorage
  • Global state via Context API + useReducer
  • Drag to reorder (react-beautiful-dnd)

Step 1 — Todo Reducer

// context/todoReducer.js
export function todoReducer(state, action) {
    switch (action.type) {
        case 'ADD':
            return [...state, { id: crypto.randomUUID(), text: action.text, done: false }];
        case 'TOGGLE':
            return state.map(t => t.id === action.id ? { ...t, done: !t.done } : t);
        case 'DELETE':
            return state.filter(t => t.id !== action.id);
        case 'CLEAR_COMPLETED':
            return state.filter(t => !t.done);
        default:
            return state;
    }
}

Step 2 — Context with localStorage Sync

// context/TodoContext.jsx
const TodoContext = createContext(null);

export function TodoProvider({ children }) {
    const [todos, dispatch] = useReducer(
        todoReducer,
        [],
        () => JSON.parse(localStorage.getItem('todos') ?? '[]')
    );

    useEffect(() => {
        localStorage.setItem('todos', JSON.stringify(todos));
    }, [todos]);

    return (
        <TodoContext.Provider value={{ todos, dispatch }}>
            {children}
        </TodoContext.Provider>
    );
}

export const useTodos = () => useContext(TodoContext);

Step 3 — TodoList with Filter

function TodoList() {
    const { todos, dispatch } = useTodos();
    const [filter, setFilter] = useState('all');

    const visible = todos.filter(t =>
        filter === 'active'    ? !t.done :
        filter === 'completed' ?  t.done : true
    );

    return (
        <div>
            {visible.map(todo => (
                <div key={todo.id} className={todo.done ? 'done' : ''}>
                    <span onClick={() => dispatch({ type: 'TOGGLE', id: todo.id })}>
                        {todo.text}
                    </span>
                    <button onClick={() => dispatch({ type: 'DELETE', id: todo.id })}>✕</button>
                </div>
            ))}
            <div>
                {['all', 'active', 'completed'].map(f => (
                    <button key={f} onClick={() => setFilter(f)}
                        style={{ fontWeight: filter === f ? 'bold' : 'normal' }}>
                        {f}
                    </button>
                ))}
            </div>
        </div>
    );
}
All Comments