Skip to main content

Todo App (Context + Reducer)

Difficulty: Core

Problem Statement​

Build add + toggle todo flows shared across components using Context and reducer.

Live Demo​

Try the running app below β€” this is the target behavior for the challenge.

Todo App (Context + Reducer) β€” Live Demo

    What Interviewers Expect​

    • Pure reducer for ADD/TOGGLE actions.
    • Context exposes actions, not raw dispatch (preferred).
    • Form and list components stay thin.

    Step-by-Step Implementation​

    Step 1 β€” Reducer actions​

    Define action types and pure state transitions.

    case 'ADD': return [...state, { id: Date.now(), title: action.title, done: false }];

    Step 2 β€” Provider value​

    Expose addTodo and toggleTodo helpers.

    const value = { todos, addTodo, toggleTodo };

    Step 3 β€” Consumer components​

    Form dispatches add; list dispatches toggle.

    const { todos, addTodo, toggleTodo } = useContext(TodoCtx);

    Complete Implementation​

    Copy-paste ready single-file solution. Use this as your reference after attempting the challenge yourself, or paste it into CodeSandbox / StackBlitz to run locally.

    App.jsx
    import React, {createContext, useContext, useMemo, useReducer, useState} from 'react';

    const TodoCtx = createContext(null);

    const todoReducer = (state, action) => {
    switch (action.type) {
    case 'ADD':
    return [...state, {id: Date.now(), title: action.title, done: false}];
    case 'TOGGLE':
    return state.map((todo) =>
    todo.id === action.id ? {...todo, done: !todo.done} : todo,
    );
    default:
    return state;
    }
    };

    function TodoAppInner() {
    const {todos, addTodo, toggleTodo} = useContext(TodoCtx);
    const [text, setText] = useState('');

    return (
    <div>
    <div style={{display: 'flex', gap: '0.75rem', marginBottom: '0.75rem'}}>
    <input
    value={text}
    onChange={(e) => setText(e.target.value)}
    placeholder="New todo"
    />
    <button
    type="button"
    onClick={() => {
    if (text.trim()) {
    addTodo(text.trim());
    setText('');
    }
    }}
    >
    Add
    </button>
    </div>

    <ul style={{listStyle: 'none', padding: 0}}>
    {todos.map((todo) => (
    <li key={todo.id} style={{display: 'flex', gap: '0.5rem', marginBottom: '0.35rem'}}>
    <input
    type="checkbox"
    checked={todo.done}
    onChange={() => toggleTodo(todo.id)}
    />
    <span style={{textDecoration: todo.done ? 'line-through' : 'none'}}>
    {todo.title}
    </span>
    </li>
    ))}
    </ul>
    </div>
    );
    }

    export default function App() {
    const [todos, dispatch] = useReducer(todoReducer, []);

    const value = useMemo(
    () => ({
    todos,
    addTodo: (title) => dispatch({type: 'ADD', title}),
    toggleTodo: (id) => dispatch({type: 'TOGGLE', id}),
    }),
    [todos],
    );

    return (
    <TodoCtx.Provider value={value}>
    <TodoAppInner />
    </TodoCtx.Provider>
    );
    }

    Final Checklist​

    • UI works for primary flow
    • Edge cases handled (empty/disabled/loading where relevant)
    • State updates are immutable
    • Components are readable and reasonably split
    • You can explain trade-offs out loud in an interview

    Next Challenge​

    Continue to the next item in the sidebar when you're comfortable implementing this from scratch in 30–45 minutes.