Skip to the solution
EasyMachine Coding14 min read

Build a Todo List

230 views

The problem

Question presented to candidate: "Let's start simple. Build me a todo list in the browser: I type a task and press Enter or click Add, it appears in the list, I can tick it off, rename it in place and delete it. Show how many are left, and let me filter between all, active and completed. No libraries, just the framework."

Requirements:

  • Add a task with a text field plus Enter or an Add button; trim whitespace and reject blank or duplicate (case-insensitive) text with a visible message.
  • Each row: a checkbox to mark it complete, an Edit control that swaps the text for an input (Enter or Save commits, Escape or Cancel throws the edit away), and a Delete button.
  • A live "N items left" count with correct singular and plural.
  • Filters: All / Active / Completed, with the active filter visibly selected and an empty-state message when nothing matches.
  • Works with the keyboard and announces errors to screen readers.

What a strong answer should cover:

  • One array of { id, text, done } objects as the single source of truth; the visible list and the remaining count are derived from it during render, not stored separately.
  • Controlled inputs: the text field's value comes from state and every keystroke goes through a change handler.
  • Immutable updates (map, filter, spread) so the framework sees a new array, plus stable unique keys (an id, never the array index) for list rows.
  • A real <form> with a submit handler so Enter works for free, and preventDefault to stop the page reload.
  • Edit mode as a small piece of per-row or lifted state, with focus moved into the edit box and Escape to cancel.
  • Accessible names on icon buttons, aria-pressed on filter toggles, and an aria-live region for validation errors.

Clarifying questions expected:

  • "Should tasks survive a page reload, or is in-memory state fine?"
  • "Are duplicate task names allowed, and what should happen when I save an edit as blank?"
  • "Is the order fixed (newest last), or do you want sorting or drag to reorder?"

Code / implementation expected: Yes. A working component in the chosen framework (React, Vue or Angular), about 100-150 lines, with add, toggle, inline edit, delete, filters and the remaining count.

  • React
  • Lists
  • Forms

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

14 min read · 9 sections

React solution

Every file of the working solution. Open it in the playground to run and change it.

ReactComplete solution
import { useRef, useState } from "react";
import TodoItem from "./src/components/TodoItem";

const FILTERS = {
  all: () => true,
  active: (todo) => !todo.done,
  completed: (todo) => todo.done,
};

export default function App() {
  const [todos, setTodos] = useState([
    { id: 1, text: "Buy milk", done: false },
    { id: 2, text: "Write tests", done: true },
  ]);
  const [draft, setDraft] = useState("");
  const [filter, setFilter] = useState("all");
  const [error, setError] = useState("");
  const nextId = useRef(3);

  // Derived values: computed during render, never stored in state.
  const visible = todos.filter(FILTERS[filter]);
  const remaining = todos.filter((t) => !t.done).length;

  function validate(text, ignoreId) {
    if (!text) return "Type a task first.";
    const key = text.toLowerCase();
    if (todos.some((t) => t.id !== ignoreId && t.text.toLowerCase() === key))
      return "“" + text + "” is already on the list.";
    return "";
  }

  function handleSubmit(event) {
    event.preventDefault();
    const text = draft.trim();
    const problem = validate(text);
    setError(problem);
    if (problem) return;
    const todo = { id: nextId.current++, text, done: false };
    setTodos((prev) => [...prev, todo]);
    setDraft("");
  }

  function toggle(id) {
    setError("");
    setTodos((prev) =>
      prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t)),
    );
  }

  function remove(id) {
    setError("");
    setTodos((prev) => prev.filter((t) => t.id !== id));
  }

  // Returns true when the new text was
  // accepted, so the row can leave edit mode.
  function rename(id, raw) {
    const text = raw.trim();
    const problem = validate(text, id);
    setError(problem);
    if (problem) return false;
    setTodos((prev) => prev.map((t) => (t.id === id ? { ...t, text } : t)));
    return true;
  }

  return (
    <main className="todo">
      <h1>Todo List</h1>

      <form data-testid="add-form" onSubmit={handleSubmit} className="row">
        <input
          data-testid="new-todo"
          aria-label="New task"
          placeholder="What needs doing?"
          value={draft}
          onChange={(e) => {
            setDraft(e.target.value);
            setError("");
          }}
          aria-invalid={error ? "true" : "false"}
          aria-describedby="todo-error"
        />
        <button data-testid="add" type="submit">
          Add
        </button>
      </form>
      <p
        id="todo-error"
        data-testid="error"
        className="error"
        aria-live="polite"
      >
        {error}
      </p>

      <div className="filters" role="group" aria-label="Filter tasks">
        {Object.keys(FILTERS).map((name) => (
          <button
            key={name}
            type="button"
            data-filter={name}
            aria-pressed={filter === name}
            onClick={() => setFilter(name)}
          >
            {name[0].toUpperCase() + name.slice(1)}
          </button>
        ))}
      </div>

      {visible.length === 0 ? (
        <p data-testid="empty" className="empty">
          No {filter === "all" ? "" : filter + " "}tasks.
        </p>
      ) : (
        <ul className="list">
          {visible.map((todo) => (
            <TodoItem
              key={todo.id}
              todo={todo}
              onToggle={() => toggle(todo.id)}
              onDelete={() => remove(todo.id)}
              onRename={(text) => rename(todo.id, text)}
              onCancel={() => setError("")}
            />
          ))}
        </ul>
      )}

      <p data-testid="count" className="count">
        {remaining} {remaining === 1 ? "item" : "items"} left
      </p>
    </main>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 23 of 107 in Machine coding

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextEasy

Build an Accordion component

Continue