Skip to the chapter
←ReactGGroundwork⚛React
React/Beginner/B11
11 / 57 written chapters
/

The cover
  1. B1Setup & JSX
  2. B2Thinking in React
  3. B3Components
  4. B4Props
  5. B5useState
  6. B6Events & conditionals
  7. B7Lists & keys
  8. B8Forms
  9. B9useEffect
  10. B10Lifting state & styling
  11. B11Guided project
  12. ★Cheat page
  1. I1useRef
  2. I2Context
  3. I3useReducer
  4. I4Custom hooks
  5. I5Effects in depth
  6. I6Routing
  7. I7State architecture
  8. I8Memoisation
  9. I9Boundaries & portals
  10. I10Data fetching
  11. I11Forms at scale
  12. I12State libraries
  13. I13Auth
  14. I14TanStack Query
  15. I15Testing
  16. I16TypeScript
  17. I17Animation
  18. I18E2E testing
  19. I19i18n
  20. I20File uploads
  21. I21Real-time UI
  22. I22shadcn/ui
  23. ★Cheat page
  1. A1Fiber
  2. A2Concurrent
  3. A3Suspense
  4. A4Server Components
  5. A5Server Actions
  6. A6use()
  7. A7Rendering strategies
  8. A8Effect timing
  9. A9External stores
  10. A10Refs in depth
  11. A11Component patterns
  12. A12Performance
  13. A13Accessibility
  14. A14Design systems
  15. A15Advanced TypeScript
  16. A16Architecture at scale
  17. A17Custom renderers
  18. A18Migrating & upgrading
  19. A19Production debugging
  20. A20Review & judgement
  21. A21State machines
  22. A22Feature flags
  23. A23Interview bank
  24. ★Cheat page

⚛B11 · Part 1 · Beginner

Guided project: a task board

Every beginner chapter, in one app built end to end.

4 min read11 sections5 exercises
11 of 57

In this chapter

  • What you are building
  • Step 1 — the model, before any JSX
  • Step 2 — static render from hardcoded data
  • Step 3 — state, and where it lives
  • Step 4 — adding, moving, deleting

and 6 more

about 4 min
11On this page
  1. What you are building
  2. Step 1 — the model, before any JSX
  3. Step 2 — static render from hardcoded data
  4. Step 3 — state, and where it lives
  5. Step 4 — adding, moving, deleting
  6. Step 5 — the form
  7. Step 6 — moving without drag and drop
  8. Step 7 — persistence
  9. Step 8 — the states you would otherwise forget
  10. Break it on purpose
  11. If you have time left

What you are building

A board with three columns, cards you can add, move and delete, a filter, and state that survives a refresh. It is also the most common machine-coding brief there is — if you can build this in ninety minutes you can pass that round.

Build in order. Each step ends with something that runs, so you always know which change broke it.

Step 1 — the model, before any JSX

const COLUMNS = ["todo", "doing", "done"];

// tasks flat and keyed by id — not nested inside columns
const initial = {
  "a1": { id: "a1", title: "Write the brief", column: "todo" },
  "b2": { id: "b2", title: "Review PR",       column: "doing" },
};
This decision is the whole round

Nesting tasks inside columns — { todo: [...], doing: [...] } — makes a move a splice out of one array and an insert into another, and "which column is this card in" a scan of every column. Flat with a column field makes a move one field change. Say why you chose it out loud; the reasoning scores higher than the result.

Step 2 — static render from hardcoded data

function Board({ tasks }) {
  return (
    <div className="board">
      {COLUMNS.map((col) => (
        <Column
          key={col}
          name={col}
          tasks={Object.values(tasks).filter((t) => t.column === col)}
        />
      ))}
    </div>
  );
}

function Column({ name, tasks }) {
  return (
    <section>
      <h2>{name} <span>{tasks.length}</span></h2>
      <ul>{tasks.map((t) => <Card key={t.id} task={t} />)}</ul>
    </section>
  );
}

No state yet. This proves the component boundaries and gives you something on screen inside ten minutes — which is what Thinking in React is about.

Step 3 — state, and where it lives

function App() {
  const [tasks, setTasks] = useState(initial);
  const [filter, setFilter] = useState("");

  const visible = useMemo(() => {
    const q = filter.trim().toLowerCase();
    return Object.values(tasks).filter((t) => !q || t.title.toLowerCase().includes(q));
  }, [tasks, filter]);

  return (
    <>
      <input value={filter} onChange={(e) => setFilter(e.target.value)} placeholder="Filter" />
      <Board tasks={visible} onMove={move} onDelete={remove} />
    </>
  );
}

Two pieces of state. visible is derived — computed during render, never stored. The counts per column are derived from that.

Step 4 — adding, moving, deleting

function add(title, column) {
  const id = crypto.randomUUID();
  setTasks((t) => ({ ...t, [id]: { id, title, column } }));
}

function move(id, column) {
  setTasks((t) => ({ ...t, [id]: { ...t[id], column } }));   // one field
}

function remove(id) {
  setTasks((t) => {
    const { [id]: _gone, ...rest } = t;                       // destructure it out
    return rest;
  });
}

All three use the updater form, so two changes in one tick cannot overwrite each other. And all three return a new object — mutating tasks gives React the same reference and it skips the render.

Step 5 — the form

function NewTask({ onAdd }) {
  const [title, setTitle] = useState("");
  const id = useId();

  function handleSubmit(e) {
    e.preventDefault();
    const clean = title.trim();
    if (!clean) return;
    onAdd(clean, "todo");
    setTitle("");
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor={id}>New task</label>
      <input id={id} value={title} onChange={(e) => setTitle(e.target.value)} />
      <button>Add</button>
    </form>
  );
}

A real <form> means Enter submits. useId means the label works even if the component appears twice.

Step 6 — moving without drag and drop

<button
  onClick={() => onMove(task.id, prevColumn)}
  aria-label={"Move " + task.title + " left"}
>←</button>

<button
  onClick={() => onMove(task.id, nextColumn)}
  aria-label={"Move " + task.title + " right"}
>→</button>

Buttons first, drag later. They work with a keyboard, they are testable, and they take two minutes — whereas drag and drop is the thing that eats your last half hour. In an interview, say you are doing buttons first and will add drag if there is time. That reads as judgement, not as a shortcut.

Step 7 — persistence

const KEY = "board:v1";

useEffect(() => {
  try { localStorage.setItem(KEY, JSON.stringify(tasks)); } catch {}
}, [tasks]);

const [tasks, setTasks] = useState(() => {          // lazy — runs once
  try {
    const raw = localStorage.getItem(KEY);
    const parsed = raw ? JSON.parse(raw) : null;
    return parsed && typeof parsed === "object" ? parsed : initial;
  } catch {
    return initial;
  }
});

The initialiser is a function, so the parse happens on the first render only. Both accesses are wrapped — localStorage throws in private mode and when the quota is full. And whatever is in there was written by an older version of your own code, so check the shape. The :v1 is how you change that shape later without breaking anyone.

Step 8 — the states you would otherwise forget

{visible.length === 0 && filter && <p>Nothing matches "{filter}".</p>}
{Object.keys(tasks).length === 0 && <p>No tasks yet. Add one above.</p>}

Two different empties — nothing at all, and nothing matching a filter — and they need different messages. Interviewers watching a machine round are counting these; handling them unprompted is one of the cheapest signals you can give.

Break it on purpose

TryWhat it teaches
Use the array index as the card keyMove a card and watch state follow the wrong one
Replace the updater form with setTasks({ ...tasks, ... })It works — until two updates land in one tick
Mutate: tasks[id].column = "done"; setTasks(tasks)Nothing re-renders. Same reference.
Define Card inside ColumnEvery card remounts each render; focus and state vanish
Store visible in state instead of deriving itThe filter and the list drift apart

If you have time left

  1. Undo the last delete — keep the removed task and a timer.
  2. Drag and drop with the HTML drag events, or a library. Now the fractional order field earns itself.
  3. Edit in place on double-click. Watch what happens to focus when you swap the element.
  4. Two tabs — they disagree, and the storage event is the fix.
  5. Replace storage with an API. Now you meet loading, errors, and the race where two saves land out of order.
Why this project

It contains the four things every React app has: state, a way to change it, a way to render it, and somewhere to keep it. It is also close enough to the standard machine-coding brief that building it once is direct interview preparation — and the flat data model is the decision that round is actually testing.

05Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 10 testsMove a task — one field, one updatebeginner
  • 10 testsDelete by destructuring it outbeginner
  • 9 testsA small todo appintermediate
  • 9 testsA small kanban boardintermediate
  • 5 testsFilter the same list three ways, without three copies of itintermediate
All 46 for this level →
B11

Finished reading?

0 of 57 chapters read in this series

→Next stop

  1. B10Previous · B10Lifting state up & basic styling
  2. B11you are here
  3. Next · I1useRefA box that survives renders and does not cause them.3 min read

01On this page

11 sections · 0%

  1. 01What you are building
  2. 02Step 1 — the model, before any JSX
  3. 03Step 2 — static render from hardcoded data
  4. 04Step 3 — state, and where it lives
  5. 05Step 4 — adding, moving, deleting
  6. 06Step 5 — the form
  7. 07Step 6 — moving without drag and drop
  8. 08Step 7 — persistence
  9. 09Step 8 — the states you would otherwise forget
  10. 10Break it on purpose
  11. 11If you have time left

[] chapters · t top