Back to Machine Coding
Machine Coding
easy
mid

How would you build a simple task management interface?

A CRUD task UI: add/edit/delete/toggle tasks, filter by status, maybe priority/due date. State is an array of {id, title, done, ...}; immutable updates; stable id keys. Watch for: controlled inputs, derived (not stored) filtered list, edit-in-place mode, and persistence.

4 min read·~20 min to think through

A task management UI is the CRUD-list pattern with a few extra dimensions (status, filtering, edit-in-place). The interviewer grades correctness fundamentals.

State shape

jsx
const [tasks, setTasks] = useState([]);   // [{ id, title, done, priority, dueDate }]
const [filter, setFilter] = useState("all"); // all | active | done
const [editingId, setEditingId] = useState(null);

The operations — all immutable, keyed by id

jsx
const addTask = (title) =>
  setTasks((t) => [...t, { id: crypto.randomUUID(), title, done: false }]);

const toggleTask = (id) =>
  setTasks((t) => t.map((task) => task.id === id ? { ...task, done: !task.done } : task));

const editTask = (id, title) =>
  setTasks((t) => t.map((task) => task.id === id ? { ...task, title } : task));

const deleteTask = (id) =>
  setTasks((t) => t.filter((task) => task.id !== id));

Filtering — derived, NOT stored

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

The filtered list is derived state — compute it with useMemo, never keep a separate filteredTasks useState that can desync from tasks.

Edit-in-place

Track editingId; the row renders an input instead of text when task.id === editingId; save commits the edit and clears editingId, Escape cancels.

What's being graded

  • Stable id keys, never the array index — or toggling/deleting corrupts rows.
  • Immutable updatesmap/filter/spread, functional updaters; never push/mutate.
  • Controlled inputs for add and edit; a real <form onSubmit> so Enter works.
  • Derived filtered list, not stored.
  • Validation — trim, reject empty titles.
  • Empty states — "no tasks," "no tasks match this filter."
  • PersistencelocalStorage (a useLocalStorage hook) so tasks survive refresh.

Scaling up (follow-ups)

useReducer once operations multiply; memoized task rows + stable callbacks for long lists; virtualization for very long lists; sorting by priority/due date; optimistic updates if it's server-backed.

The framing

"It's the CRUD-list pattern: tasks as an array of {id, title, done, ...}, with add/toggle/edit/delete done immutably and keyed by a stable id — never the index. The filter is derived state via useMemo, not a separate useState that can desync. Edit-in-place is an editingId toggling the row between text and input. The fundamentals being graded are stable keys, immutable functional updates, controlled inputs in a real form, and the empty states. I'd persist to localStorage, and reach for useReducer plus memoized rows as it grows."

Follow-up questions

  • Why is the filtered list derived state, not stored state?
  • Why use a stable id instead of the array index as the key?
  • How would you implement edit-in-place?
  • When would you switch to useReducer?

Common mistakes

  • Array index as key — corrupts rows on toggle/delete.
  • Storing filteredTasks in separate state that desyncs.
  • Mutating the tasks array with push/splice.
  • No empty states; no validation on the add input.
  • Not persisting — refresh loses everything.

Performance considerations

  • Interview scale is trivial. For long lists: memoize rows (React.memo) with stable callbacks so toggling one task doesn't re-render all; virtualize beyond a few hundred; memoize the derived filtered list.

Edge cases

  • Empty title submitted.
  • Deleting the task currently being edited.
  • Filter showing zero results.
  • Rapidly toggling many tasks (functional updater).
  • Very long task list.

Real-world examples

  • Todoist/Trello-style task UIs; checklist features in larger apps.
  • The canonical CRUD interview exercise.

Senior engineer discussion

Seniors nail stable keys, immutable updates, controlled inputs, and derived filtering without prompting, handle empty states and persistence, and discuss scaling via useReducer, memoized rows, and virtualization.

Related questions