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)
- Data model — what is a Todo?
- State ownership — single list in parent vs. reducer
- Persistence — when to read/write storage
- Component tree — presentational vs. container
- 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>(oraria-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
- Add tags or projects
- Optimistic sync to a mock API
- Undo delete (toast + timeout)
- Virtualize 10k rows
Ship a clean MVP in 30–40 minutes, then earn extension points.