Command Palette

Search for a command to run...

Hectal
PHASE 27Advanced ~26 min· topic 2 of 6

Topic 27.2

Build the Components

In one line

Milestone 1: build the board as a static version first, components drawn from data with props only and no state, using composition so a Column can hold any content, and style it with plain CSS.

Think of it like this

A dolls' house before the furniture moves. First you build the rooms, walls and painted furniture so you can see the whole house. Only then do you add the bits that open and slide. A static version is the house with nothing moving yet: you can see every room and check the layout before adding behaviour.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Static version
The UI drawn from fixed data with props only, before any state or behaviour is added.
Props
The inputs a parent passes to a child component, like task or title.
Composition
Building components by putting other components or JSX inside them, through children or slot props.
children
The special prop holding whatever JSX you write between a component's opening and closing tags.
Slot
A prop that takes JSX, like actions={<button>+</button>}, so the parent decides what goes in that spot.
Single responsibility
Each component has one clear job, which makes it easy to name, test and reuse.
key
A stable, unique id on each item of a list so React can match items between renders.
Region
An area of the page with a name, like a <section> labelled by its heading. Screen readers can jump between regions.

Step by step

01Start from the leaves

Build the smallest pieces first: PriorityBadge and TaskCard. They take one prop each and contain no logic you'd need to test.

src/components/TaskCard.jsxwhole filejsx
import PriorityBadge from "./PriorityBadge.jsx";

export default function TaskCard({ task }) {
  return (
    <li className="task">
      <span className="title">{task.title}</span>
      <PriorityBadge priority={task.priority} />
    </li>
  );
}

02A Column that takes children

Column draws the frame and shows Nothing here yet. when it is empty. It renders children for the content, so it never needs to know what a task is.

src/components/Column.jsxwhole filejsx
export default function Column({ id, title, count, children }) {
  const headingId = "col-" + id + "-heading";
  return (
    <section className="column" aria-labelledby={headingId}>
      <h2 id={headingId}>{title} <span className="count">({count})</span></h2>
      {count === 0 ? <p className="empty">Nothing here yet.</p> : <ul>{children}</ul>}
    </section>
  );
}

03The Board puts tasks into columns

Board loops over COLUMNS, filters the tasks for each one and passes the cards in as children. The filter is a derived value: computed during render from the tasks prop.

The Board puts tasks into columnsdiagram
Rendering diagram…

04Check the static version

Before adding state, check three things: the page matches the mockup, the console has no warnings (keys!), and the structure reads well without CSS (headings, lists, regions). Open the browser's accessibility tree (DevTools, Elements, Accessibility) and confirm each column is a named region.

05Real files: one component per file

In the preview everything is one file with // ---- src/... ---- markers. In the project each marker is a file with export default, and the imports join them.

src/App.jsxwhole filejsx
import Board from "./components/Board.jsx";
import { SEED_TASKS } from "./data/seed.js";
import "./App.css";

export default function App() {
  return (
    <main>
      <h1>Task board</h1>
      <Board tasks={SEED_TASKS} />
    </main>
  );
}

Try it yourself

  1. 1

    Change the data, not the components

    In milestone 1, move Save to localStorage to "doing" and delete Plan the board from SEED_TASKS. Predict the counts and the empty message, then check. You changed no component.

  2. 2

    Add a column

    Add { id: "review", title: "Review" } between Doing and Done in COLUMNS. What else must change for it to look right? (Only the CSS grid: repeat(4, ...).)

  3. 3

    Use the actions slot

    In the composition example, give the Doing column actions={<button>Start one</button>}. Predict the console line for Doing.

Code & diagrams

Milestone 1: the static board New tab

The whole app as of milestone 1, in one file. Each `// ---- src/... ----` comment marks where a real file starts. No state yet: change `SEED_TASKS` and the board follows.

Preview

Press Run to see the page.

Console

Nothing logged yet.
Composition: one Column, any content New tab

The parent decides what goes into each column: cards, nothing, or buttons in the `actions` slot. `Column` never changes.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

To do: 2 cards, 1 buttons
Doing: 0 cards, 0 buttons
Done: 1 cards, 1 buttons
Milestone 1 as real filestext
src/
├── main.jsx                 createRoot(...).render(<StrictMode><App /></StrictMode>)
├── App.jsx                  <main><h1/><Board tasks={SEED_TASKS} /></main>
├── App.css                  .board grid, .column, .task, .badge
├── data/seed.js             COLUMNS, SEED_TASKS
└── components/
    ├── Board.jsx            columns from data, tasks filtered per column
    ├── Column.jsx           frame + children + empty state
    ├── TaskCard.jsx         one task
    └── PriorityBadge.jsx    one coloured label

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Forget the key on the cards

In milestone 1, change <TaskCard key={t.id} task={t} /> to <TaskCard task={t} />.

terminal
$ Console
── what you'll see ──
Each child in a list should have a unique "key" prop.
 
Check the render method of `Column`. It was passed a child from Board. See https://react.dev/link/warning-keys for more information.

Myth vs fact

Myth

A static version is wasted work you'll throw away.

Fact

Almost all of it stays. Milestones 2 to 5 add state and behaviour to these same components; the markup, classes and structure barely change.

Myth

Column should import TaskCard so the parent has less to do.

Fact

Then Column only works for tasks and needs task data passed through it. With children it stays a pure layout component the Board fills.

Myth

Accessibility can wait until the app works.

Fact

Semantic structure (sections, headings, lists, buttons) is free when you write it first and painful to retrofit. The tests in 27.5 depend on it.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Composition through children also helps performance: if a stateful wrapper renders children it received from its parent, those children don't re-render when only the wrapper's state changes, because they are the same elements (Topic 16.1).

  • ▸

    Keep components that only draw (Column, TaskCard, PriorityBadge) separate from ones that decide (Board, later the store hooks). Drawing components are trivial to test and to show in a tool such as Storybook.

  • ▸

    aria-labelledby pointing at the heading's id gives the region an accessible name without repeating text. When ids must be unique across many instances of a component, generate them with useId() (React 18, Topic 18.3) instead of building them from data.

Remember this

  1. 1

    Build a static version first (Topic 2.7). It uses only props (the inputs a parent passes to a child) and no state. Each component is a pure function from data to UI: give Board the seed tasks and it draws the columns and cards. Because nothing changes yet, it is easy to check the layout, the HTML structure and the accessibility of the markup in isolation.

  2. 2

    Draw the UI from data, never by hand. The three columns come from a COLUMNS array and the cards from tasks.filter(...) and map(...) (Topic 3.1 and 3.2). Every card has key={task.id} (Topic 3.3): a stable id from the data, never the array index, because cards will move between columns and change order.

  3. 3

    Single responsibility: each component does one job. TaskCard shows one task. PriorityBadge shows a priority. Column draws one column's frame: heading, count and an empty message. Board decides which tasks go into which column. App lays out the page. Small components are easy to name, test and reuse.

  4. 4

    **Composition with children** (Topic 2.5). Column doesn't import TaskCard: it renders whatever it receives as children, plus an optional actions prop (a slot, a prop that takes JSX). The same Column can hold cards, an empty state or a form. This keeps Column reusable and avoids passing task data through components that don't need it (prop drilling, Topic 9.1).

  5. 5

    Semantic HTML from day one (Topic 18.1): each column is a <section> labelled by its heading with aria-labelledby, so screen readers list it as a named region; cards are <li> items in a <ul>, so a screen reader says "list, 2 items". Getting this right now is far cheaper than adding it later, and Testing Library queries in 27.5 rely on it (getByRole("region", { name: "To do" })).

  6. 6

    Styling (Phase 11): one plain CSS file with class names (.board, .column, .task) is enough for this app. CSS grid lays out the three columns; the priority badge picks its colour from a class that matches the data (badge high). In a real project you might use CSS Modules or Tailwind instead; the components don't change.

Explain it without notes

01

Why build a static version of the UI first?

02

How does composition with children make Column reusable?

03

Why must the card key be the task id and not the index?

Practice

01

Add a count badge to Column that turns red when the column has more than 3 cards (a WIP limit).

02

Write a Board that shows an <h2> "No tasks yet" when tasks is empty, instead of three empty columns.

Trade-offs

  • ↔

    Many small components are easy to test and reuse but add files and props to follow; split when a part has its own job or is repeated, not for every div.

  • ↔

    Plain CSS is simplest for one app; CSS Modules or Tailwind help when many people work on many components and class names start to clash (Phase 11).

Done when you can

  • I can build a static version of an app from data with props only.

  • I can split a screen into single-responsibility components.

  • I can use children and slot props to compose components.

  • I give list items stable keys from the data.

  • I write semantic, labelled structure from the first version.