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
taskortitle. - Composition
- Building components by putting other components or JSX inside them, through
childrenor 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.
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.
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.
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.
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
Change the data, not the components
In milestone 1, move
Save to localStorageto"doing"and deletePlan the boardfromSEED_TASKS. Predict the counts and the empty message, then check. You changed no component. - 2
Add a column
Add
{ id: "review", title: "Review" }between Doing and Done inCOLUMNS. What else must change for it to look right? (Only the CSS grid:repeat(4, ...).) - 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
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
Console
Nothing logged yet.The parent decides what goes into each column: cards, nothing, or buttons in the `actions` slot. `Column` never changes.
Preview
Console
Nothing logged yet.Expected console output
To do: 2 cards, 1 buttons
Doing: 0 cards, 0 buttons
Done: 1 cards, 1 buttonssrc/
├── 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 labelBreak 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} />.
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
childrenalso helps performance: if a stateful wrapper renderschildrenit 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-labelledbypointing at the heading'sidgives the region an accessible name without repeating text. When ids must be unique across many instances of a component, generate them withuseId()(React 18, Topic 18.3) instead of building them from data.
Remember this
- 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
Boardthe 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
Draw the UI from data, never by hand. The three columns come from a
COLUMNSarray and the cards fromtasks.filter(...)andmap(...)(Topic 3.1 and 3.2). Every card haskey={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
Single responsibility: each component does one job.
TaskCardshows one task.PriorityBadgeshows a priority.Columndraws one column's frame: heading, count and an empty message.Boarddecides which tasks go into which column.Applays out the page. Small components are easy to name, test and reuse. - 4
**Composition with
children** (Topic 2.5).Columndoesn't importTaskCard: it renders whatever it receives aschildren, plus an optionalactionsprop (a slot, a prop that takes JSX). The same Column can hold cards, an empty state or a form. This keepsColumnreusable and avoids passing task data through components that don't need it (prop drilling, Topic 9.1). - 5
Semantic HTML from day one (Topic 18.1): each column is a
<section>labelled by its heading witharia-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
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
Why build a static version of the UI first?
How does composition with children make Column reusable?
Why must the card key be the task id and not the index?
Practice
Add a count badge to Column that turns red when the column has more than 3 cards (a WIP limit).
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
childrenand slot props to compose components.I give list items stable keys from the data.
I write semantic, labelled structure from the first version.