Back to Machine Coding
Machine Coding
easy
mid

How would you approach building a To Do app in a 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).

5 min read·~40 min to think through

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

ts
<App>
<TodoInput onAdd />
<TodoList todos onToggle onDelete onEdit />
  │   └ <TodoItem todo onToggle onDelete onEdit />
<FilterBar filter onChange counts />

2. State shape — one source of truth

jsx
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

jsx
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

jsx
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

jsx
{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-label on the new-todo input.
  • Filter controls as a <fieldset>/radio group or buttons with aria-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).

Senior engineer discussion

Seniors signal it through small decisions: stable ids, immutable updates, controlled inputs, accessibility from the start, and not over-engineering with Redux/state machines for a 5-component app.

Related questions