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.