Build a Todo App machine-coding round
Show clean component decomposition (App → List → Item, plus Input + Filter), a single source of truth for state, immutable updates, keys on list items, controlled inputs, basic accessibility, and persistence to localStorage. Then layer features (edit-in-place, filter all/active/completed, count).
A todo app is the classic machine-coding round because it touches state management, list rendering, immutability, accessibility, and persistence in ~50 lines. They're judging structure and habits.
1. Component decomposition
<App>
├ <TodoInput onAdd />
├ <TodoList todos onToggle onDelete onEdit />
│ └ <TodoItem todo onToggle onDelete onEdit />
├ <FilterBar filter onChange counts />2. State shape — one source of truth
function App() {
const [todos, setTodos] = useState(() => loadFromLS() || []);
const [filter, setFilter] = useState("all");
useEffect(() => saveToLS(todos), [todos]);
const visible = todos.filter(t =>
filter === "all" ? true : filter === "active" ? !t.done : t.done
);
// ...
}Each todo: { id: crypto.randomUUID(), text, done, createdAt }. Stable id (not array index) is critical for keys.
3. Immutable updates
const add = (text) => setTodos(ts => [...ts, { id: crypto.randomUUID(), text, done: false }]);
const toggle = (id) => setTodos(ts => ts.map(t => t.id === id ? { ...t, done: !t.done } : t));
const remove = (id) => setTodos(ts => ts.filter(t => t.id !== id));
const edit = (id, text) => setTodos(ts => ts.map(t => t.id === id ? { ...t, text } : t));4. Controlled input
function TodoInput({ onAdd }) {
const [text, setText] = useState("");
return (
<form onSubmit={e => { e.preventDefault(); if (text.trim()) { onAdd(text.trim()); setText(""); } }}>
<input value={text} onChange={e => setText(e.target.value)} aria-label="Add todo" />
<button type="submit">Add</button>
</form>
);
}5. Keys on list items — use the stable id
{visible.map(t => <TodoItem key={t.id} todo={t} ... />)}6. Edit in place
Click text → input with the current text, Enter to save, Escape to cancel, blur to save. Keep local edit state in the item; commit via callback.
7. Persistence
Read localStorage lazily via the useState initializer; write in a useEffect on todos.
8. Accessibility
<input type="checkbox">with a label per todo.aria-labelon the new-todo input.- Filter controls as a
<fieldset>/radio group or buttons witharia-pressed. - Keyboard: Enter to add/save, Escape to cancel edit.
9. The polish (if there's time)
- Counts: "3 items left".
- Clear completed.
- Toggle all.
- Optimistic ordering on add.
- Maybe react-query for syncing if there's a backend.
What the interviewer is grading
- Component decomposition — small, focused components.
- State location — single source of truth in App, not duplicated.
- Immutability — no
push/mutation. - Stable keys — id, not index.
- Controlled inputs.
- Accessibility basics.
- Clean, readable code — naming, no premature abstraction.
- Persistence if asked.
Interview framing
"I'd decompose into App / Input / List / Item / FilterBar, with state living once in App. Each todo has a stable id (used as the key), and updates are immutable via map/filter/spread. The new-todo input is controlled, with Enter to submit and trim. Persist to localStorage via an effect, hydrate lazily. Add edit-in-place with local input state in the item and Enter/Escape handling. Then I'd layer filters, counts, and accessibility — labelled checkboxes, keyboard nav."
Follow-up questions
- •Why use a stable id as the key instead of the array index?
- •How would you add an undo button for delete?
- •How would you persist across devices instead of just localStorage?
Common mistakes
- •Using array index as a React key.
- •Mutating todos with push/splice.
- •Putting state in every component instead of lifting it.
- •Skipping accessibility (no labels, no keyboard support).
- •Forgetting trim() and empty-submit guard.
Performance considerations
- •Memoize TodoItem if items get heavy; otherwise React's reconciliation is fine for small lists. For very large lists, virtualize. Stable callbacks via `useCallback` if memoizing children.
Edge cases
- •Empty text on submit.
- •Editing to empty — delete or revert?
- •Very long lists — virtualize if hundreds of items.
- •Concurrent edit across tabs — storage event sync.
Real-world examples
- •TodoMVC reference implementations.
- •Linear-style task lists (with backend sync, optimistic updates, keyboard nav).