ESC

Type to search the knowledge base.

Todo App

Machine-coding brief for a production-minded todo list — requirements, architecture, state, a11y, and extensions.

beginner3 min read
  • machine-coding
  • react
  • state
  • accessibility

Problem statement

Build a Todo application in the browser. Candidates usually get React + TypeScript (or vanilla JS). Interviewers score product sense, state design, accessibility, and code structure — not pixel-perfect design.

Requirements

Must have

  • Add a todo from an input + submit control
  • Mark complete / incomplete
  • Delete a todo
  • Persist across refresh (localStorage)
  • Empty and loading/error-free edge states
  • Keyboard usable end-to-end

Should have

  • Filter: All / Active / Completed
  • Items remaining count
  • Clear completed
  • Prevent empty titles; trim whitespace

Nice to have

  • Inline edit (double-click or Edit button)
  • Drag reorder
  • Due dates

Planning (5 minutes out loud)

  1. Data model — what is a Todo?
  2. State ownership — single list in parent vs. reducer
  3. Persistence — when to read/write storage
  4. Component tree — presentational vs. container
  5. A11y — labels, list semantics, focus after delete

Architecture

TodoApp
├── TodoInput          // controlled input + form
├── TodoFilters        // All | Active | Completed
├── TodoList
│   └── TodoItem       // checkbox, label, delete, optional edit
└── TodoFooter         // count + clear completed

Data model

type TodoId = string;

type Todo = {
  id: TodoId;
  title: string;
  completed: boolean;
  createdAt: number;
};

type Filter = 'all' | 'active' | 'completed';

Use crypto.randomUUID() for ids when available.

Folder structure

src/
  features/todos/
    TodoApp.tsx
    TodoInput.tsx
    TodoList.tsx
    TodoItem.tsx
    TodoFilters.tsx
    types.ts
    storage.ts
    useTodos.ts

Implementation sketch

Storage

const KEY = 'fb.todos.v1';

export function loadTodos(): Todo[] {
  try {
    const raw = localStorage.getItem(KEY);
    if (!raw) return [];
    const parsed = JSON.parse(raw);
    return Array.isArray(parsed) ? parsed : [];
  } catch {
    return [];
  }
}

export function saveTodos(todos: Todo[]) {
  localStorage.setItem(KEY, JSON.stringify(todos));
}

State hook

function useTodos() {
  const [todos, setTodos] = useState<Todo[]>(() => loadTodos());
  const [filter, setFilter] = useState<Filter>('all');

  useEffect(() => {
    saveTodos(todos);
  }, [todos]);

  const visible = todos.filter((t) => {
    if (filter === 'active') return !t.completed;
    if (filter === 'completed') return t.completed;
    return true;
  });

  function add(title: string) {
    const trimmed = title.trim();
    if (!trimmed) return;
    setTodos((prev) => [
      { id: crypto.randomUUID(), title: trimmed, completed: false, createdAt: Date.now() },
      ...prev,
    ]);
  }

  // toggle, remove, clearCompleted...

  return { todos, visible, filter, setFilter, add /* ... */ };
}

Accessibility essentials

  • <form> around the input; submit on Enter
  • <ul> / <li> for the list
  • Checkbox has an associated <label> (or aria-label)
  • Delete button has an accessible name, e.g. aria-label="Delete buy milk"
  • After deleting the focused item, move focus to a sensible neighbor or the input

Interview expectations

Signal What good looks like
Requirements You clarify filters/persist before coding
State One source of truth; no duplicated lists
UX Empty state, disabled clear when none completed
A11y Keyboard + labels without prompting
Code Small components; typed model; no god file
Time MVP first, then polish

Extensions they may ask live

  1. Add tags or projects
  2. Optimistic sync to a mock API
  3. Undo delete (toast + timeout)
  4. Virtualize 10k rows

Ship a clean MVP in 30–40 minutes, then earn extension points.