Back to Machine Coding
Machine Coding
medium
mid

How would you build a To Do list with an input, add button, and per item delete?

The 'hello world' of React interviews. State: an array of {id, text} todos plus controlled input. Add appends, delete filters by id. Watch the details: stable keys (not index), controlled input, trim/empty validation, form submit for Enter support.

4 min read·~15 min to think through

The todo list is the React interview baseline — the interviewer is grading the details, not whether it works.

The implementation

jsx
function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [text, setText] = useState("");

  const addTodo = (e) => {
    e.preventDefault();                       // it's a form → Enter works too
    const trimmed = text.trim();
    if (!trimmed) return;                     // reject empty/whitespace
    setTodos((prev) => [...prev, { id: crypto.randomUUID(), text: trimmed }]);
    setText("");                              // clear the input
  };

  const removeTodo = (id) =>
    setTodos((prev) => prev.filter((t) => t.id !== id));

  return (
    <div>
      <form onSubmit={addTodo}>
        <input
          value={text}
          onChange={(e) => setText(e.target.value)}
          placeholder="Add a todo"
        />
        <button type="submit">Add</button>
      </form>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>                  {/* stable id, NOT index */}
            {todo.text}
            <button onClick={() => removeTodo(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

What's actually being graded

  • Stable keyskey={todo.id}, not key={index}. With index keys, deleting an item makes React reconcile wrong — input state and focus jump to the wrong row.
  • Controlled inputvalue + onChange; React owns the input state.
  • Immutable updates[...prev, new] and .filter(), never push/splice on state.
  • Functional state updatersetTodos(prev => ...) avoids stale-state bugs.
  • Validationtrim() and reject empty input.
  • A <form> with onSubmit — so pressing Enter adds the todo, not just clicking. e.preventDefault() stops the page reload.
  • Clear the input after adding.

Likely follow-ups to be ready for

Toggle complete (add a done boolean, line-through styling), edit in place, filter (all/active/done), persist to localStorage, and lift state / use a reducer when it grows.

The framing

"State is an array of {id, text} plus the controlled input string. Add appends immutably with a functional updater; delete filters by id. The details that matter: a real id for the key — never the index — a <form onSubmit> so Enter works, trim-and-reject-empty validation, and clearing the input after adding. Those details are the actual test."

Follow-up questions

  • Why use todo.id instead of the array index as the key?
  • How would you add a 'mark complete' toggle?
  • How would you persist todos across page reloads?
  • When would you switch from useState to useReducer here?

Common mistakes

  • Using the array index as the key — breaks on delete/reorder.
  • Mutating state with push/splice instead of returning a new array.
  • Uncontrolled input, or forgetting to clear it after adding.
  • Using a button click instead of a form, so Enter doesn't work.
  • Not trimming/validating — empty todos get added.

Performance considerations

  • Trivial at interview scale. If the list grew large, you'd memoize rows (React.memo) and stabilize the delete callback so unchanged rows don't re-render; for thousands of items, virtualize.

Edge cases

  • Adding an empty or whitespace-only string.
  • Duplicate todo text — allowed, but ids must still be unique.
  • Rapidly adding many todos — functional updater prevents lost updates.
  • Very long todo text overflowing the layout.

Real-world examples

  • The canonical React tutorial app — every list-with-CRUD UI is this pattern.
  • Task managers, shopping lists, checklist features.

Senior engineer discussion

Seniors nail the details — stable keys, immutable functional updates, controlled input, form submit, validation — without being told, then proactively discuss scaling: useReducer, memoized rows, localStorage persistence, and virtualization.

Related questions