Build: Todo App
Introduction
A Todo app is the smallest UI challenge that still tests real product architecture: state shape, immutable updates, rendering, event delegation, filters, and persistence boundaries.
A strong answer does not scatter DOM mutations across handlers. It centralizes state transitions in a reducer-like function and uses one render path so every action produces a predictable UI.
Why This Matters
Todo apps are deceptively useful in interviews because they reveal how you structure frontend code under time pressure. Interviewers watch for stable ids, event delegation, empty states, XSS-safe rendering, and whether state is the source of truth.
Theory
State model
Each todo should have a stable id, a text, and a completed flag. The app state also tracks the active filter and the next id. Avoid using array indexes as ids because removing or reordering items breaks event targeting.
Render strategy
For a framework-free implementation, generate HTML from state in one render function and attach one delegated click listener to the root. Event delegation is simpler than registering a listener on every item after every render.
Reducer mindset
Actions such as add, toggle, remove, clear completed, and set filter are state transitions. Keeping them in dispatch(action) makes the app easy to test without the DOM. Rendering becomes a side effect after state changes.
Visual Diagrams
user event | v dispatch action | v update state | v render from state | v DOM reflects current state
State is the source of truth; the DOM is a projection of state.
Code Examples
Complete Todo app with delegated events
The render function deliberately builds strings with concatenation. In production, a framework or DOM node construction can replace this rendering layer while preserving the state model.
Playground
Press Run to execute the code and see output here.
Output Prediction
Predict the output #1
1function reduceTodos(state, action) {2 if (action.type === 'add') {3 return {4 nextId: state.nextId + 1,5 items: state.items.concat({ id: state.nextId, text: action.text, completed: false })6 };7 }8 9 if (action.type === 'toggle') {10 return {11 nextId: state.nextId,12 items: state.items.map(function (item) {13 if (item.id !== action.id) {14 return item;15 }16 17 return { id: item.id, text: item.text, completed: !item.completed };18 })19 };20 }21 22 return state;23}24 25var state = { nextId: 1, items: [] };26state = reduceTodos(state, { type: 'add', text: 'Ship content' });27state = reduceTodos(state, { type: 'toggle', id: 1 });28console.log(state.items[0].text + ':' + state.items[0].completed);Coding Exercises
Implement a framework-free Todo app
MediumBuild createTodoApp(root).
Requirements:
- Add todos from a form.
- Toggle completion by stable id.
- Remove todos.
- Filter all, active, and completed.
- Clear completed todos.
- Render empty and remaining-count states.
- Escape user text before inserting HTML.
- Use event delegation and return
{ dispatch, getState }.
Constraints:
- Do not use a framework.
- Do not use array indexes as ids.
- Keep state as the source of truth.
Interview Questions
1Why should you avoid array indexes as todo ids?
Indexes change when items are removed, filtered, or reordered. If the UI stores an index in the DOM, a later click may target the wrong item. Stable ids keep event handling correct across list changes.
Follow-ups
- How would you persist todos?
- How would you avoid rerendering the whole list?
2Why use event delegation?
One root listener can handle events for current and future child elements. This avoids reattaching per-item listeners after every render and makes cleanup simpler.
Quiz
1. What is the main reason to escape todo text before writing `innerHTML`?
Summary
- Keep Todo state as the source of truth.
- Use stable ids, not array indexes.
- Centralize transitions in a dispatch function and render from state.
- Escape user text before string-based rendering.
Cheat Sheet
Todo app checklist
- State:
{ items, filter, nextId }. - Item:
{ id, text, completed }. - Actions: add, toggle, remove, clear completed, set filter.
- Event delegation on root.
- Stable ids in
data-id. - Escape text if using
innerHTML. - Render empty state and remaining count.