Back to Machine Coding
Machine Coding
easy
mid

How would you build a To Do app with add, edit, delete, and filters?

The full todo app: array of {id, text, done}, add/edit/delete/toggle done, filter all/active/completed. Stable id keys, immutable updates, controlled inputs in a form, edit-in-place mode, derived (not stored) filtered list, empty states, localStorage persistence. useReducer once it grows.

4 min read·~25 min to think through

The full todo app — CRUD plus filtering — tests whether you handle list state cleanly and catch all the details.

State

jsx
const [todos, setTodos] = useState([]);       // [{ id, text, done }]
const [filter, setFilter] = useState("all");  // all | active | completed
const [editingId, setEditingId] = useState(null);

The operations — immutable, keyed by id

jsx
const add = (text) =>
  setTodos((t) => [...t, { id: crypto.randomUUID(), text: text.trim(), done: false }]);

const toggle = (id) =>
  setTodos((t) => t.map((todo) => todo.id === id ? { ...todo, done: !todo.done } : todo));

const edit = (id, text) =>
  setTodos((t) => t.map((todo) => todo.id === id ? { ...todo, text } : todo));

const remove = (id) =>
  setTodos((t) => t.filter((todo) => todo.id !== id));

Filtering — derived, not stored

jsx
const visible = useMemo(() => {
  if (filter === "active")    return todos.filter((t) => !t.done);
  if (filter === "completed") return todos.filter((t) => t.done);
  return todos;
}, [todos, filter]);

Never keep a separate filteredTodos state — it desyncs from todos. Compute it.

Edit-in-place

editingId flags which row is editing; that row renders an input instead of text. Enter/blur saves, Escape cancels, empty text could delete-or-revert.

The full checklist of details

  • Stable id keys — never the array index (delete/reorder corrupts rows, inputs, focus).
  • Immutable functional updatesmap/filter/spread, setTodos(prev => ...).
  • Controlled add input inside a <form onSubmit> so Enter works; clear it after adding.
  • Validationtrim(), reject empty.
  • Derived filtered list, memoized.
  • Empty states — "no todos yet" vs "no todos match this filter."
  • PersistencelocalStorage via a useLocalStorage hook so it survives refresh.
  • Optional polish: item count, "clear completed," toggle-all.

Scaling

useReducer once the operations multiply (the reducer centralizes the logic cleanly); React.memo rows with stable callbacks so toggling one doesn't re-render all; virtualization for huge lists.

The framing

"Todos as an array of {id, text, done}; add/edit/delete/toggle all immutable and keyed by a stable id — never the index. The filter is derived via useMemo, not stored state that can desync. Edit-in-place is an editingId swapping the row to an input. The detail checklist that's actually being graded: stable keys, immutable functional updates, a controlled input in a real form so Enter works, trim validation, both empty states, and localStorage persistence. As it grows I'd move to useReducer and memoize the rows."

Follow-up questions

  • Why must the filtered list be derived, not stored?
  • Why is the array index a bad key here?
  • How would you implement edit-in-place cleanly?
  • When and why would you switch to useReducer?

Common mistakes

  • Array index as key.
  • Storing filteredTodos separately — desyncs.
  • Mutating the array (push/splice).
  • Button instead of a form — Enter doesn't add.
  • No empty states, no validation, no persistence.

Performance considerations

  • Interview scale is trivial. For scale: memoized rows + stable callbacks so toggling one todo doesn't re-render the list, memoized derived filter, virtualization past a few hundred items.

Edge cases

  • Editing a todo to empty text.
  • Deleting the todo currently being edited.
  • Filter showing zero results.
  • Duplicate text (allowed; ids still unique).
  • Large lists.

Real-world examples

  • The canonical React interview / tutorial app (TodoMVC).
  • Checklist and task features in real products.

Senior engineer discussion

Seniors hit every detail — stable keys, immutable functional updates, form semantics, derived filtering, empty states, persistence — and discuss scaling via useReducer, memoized rows, and virtualization.

Related questions