Every lesson so far has answered a question you were handed. This one starts where real
work starts: with a picture and nothing else.
We are building a reading list. It shows books, lets you filter by text, lets you hide the
ones you have finished, lets you mark a book as read, and shows a count. Sketch it:
The mock-up, with step one already done to it.
There is a procedure for turning that into React. Five steps, always the same, and worth
following even when it feels laborious - it is what stops you writing state you do not
need.
Step 1: break the picture into components
Draw boxes around anything that is one thing. A component should do one job, the same way
a function should.
App - the whole page
Filters - the text box and the checkbox
BookList - the list
BookRow - one book
Summary - the count at the bottom
The hierarchy falls out of the drawing: Filters, BookList and Summary sit
inside App, and BookRow sits inside BookList.
Do not agonise. If a box turns out to be wrong, moving JSX between components is the
cheapest change you can make.
Step 2: build it static, with no state at all
Write the whole thing with data flowing down as props and nothing interactive. No
useState, no handlers.
const books = [
{ id: 1, title: "The Shipping Forecast", author: "Kate Bell", read: false },
{ id: 2, title: "Deep Work", author: "Cal Newport", read: true },
{ id: 3, title: "The Art of Doing Science", author: "Richard Hamming", read: false },
];
function BookRow({ book }) {
return (
<li>
<input type="checkbox" checked={book.read} readOnly />
<strong>{book.title}</strong> <span>{book.author}</span>
</li>
);
}
function BookList({ books }) {
if (books.length === 0) return <p>Nothing matches.</p>;
return (
<ul>
{books.map((book) => (
<BookRow key={book.id} book={book} />
))}
</ul>
);
}
This half is deliberately dull, and doing it first is the whole trick. You are building
the part that cannot go wrong while your attention is fresh, so that when you get to state
you are thinking about one thing rather than two.
(readOnly is there because a checked with no onChange is an input React controls but
nothing can change - it would warn otherwise. It goes away in step 5.)
Step 3: find the minimal state
Now list everything the app deals with, and cross out anything you can work out from
something else.
- The list of books - state. Nothing else can produce it.
- The filter text - state. It comes from the user and nothing else knows it.
- Whether to hide read books - state. Same.
- The filtered list - not state. It is books + filter + checkbox.
- The unread count - not state. It is a
.filter().length away.
- Whether each book is read - not state on its own. It is a field on the book.
Three pieces. Everything else is arithmetic.
This is lesson 9’s rule applied deliberately rather than by accident, and it is the step
that decides whether the app stays easy to reason about. The test is the same one: could
I work this out from something else I already have?
Step 4: decide where each piece lives
For each piece of state, find every component that reads it and put the state in their
nearest common parent.
- Filter text: read by
Filters (to display it) and needed for filtering, which happens
where the list is decided. Common parent: App.
- Hide-read: same.
App.
- The books: read by
BookList and changed by BookRow. Common parent: App.
All three end up in App. That is normal for something this size, and not a sign you have
done it wrong. In a larger app you would push each one down as far as it will go.
Step 5: wire it up
Now add state, pass functions down, and derive everything else during render.
import { useState } from "react";
const initialBooks = [
{ id: 1, title: "The Shipping Forecast", author: "Kate Bell", read: false },
{ id: 2, title: "Deep Work", author: "Cal Newport", read: true },
{ id: 3, title: "The Art of Doing Science", author: "Richard Hamming", read: false },
];
function Filters({ query, onQueryChange, hideRead, onHideReadChange }) {
return (
<div>
<input
value={query}
onChange={(e) => onQueryChange(e.target.value)}
placeholder="Filter"
/>
<label>
<input
type="checkbox"
checked={hideRead}
onChange={(e) => onHideReadChange(e.target.checked)}
/>
Hide read
</label>
</div>
);
}
function BookRow({ book, onToggleRead }) {
return (
<li>
<input
type="checkbox"
checked={book.read}
onChange={() => onToggleRead(book.id)}
/>
<strong>{book.title}</strong> <span>{book.author}</span>
</li>
);
}
function BookList({ books, onToggleRead }) {
if (books.length === 0) return <p>Nothing matches.</p>;
return (
<ul>
{books.map((book) => (
<BookRow key={book.id} book={book} onToggleRead={onToggleRead} />
))}
</ul>
);
}
function Summary({ unread, total }) {
return (
<p>
{unread} of {total} unread
</p>
);
}
function App() {
const [books, setBooks] = useState(initialBooks);
const [query, setQuery] = useState("");
const [hideRead, setHideRead] = useState(false);
const visible = books.filter((book) => {
const matches = (book.title + book.author)
.toLowerCase()
.includes(query.toLowerCase());
return matches && (!hideRead || !book.read);
});
const unread = books.filter((book) => !book.read).length;
function handleToggleRead(id) {
setBooks(
books.map((book) =>
book.id === id ? { ...book, read: !book.read } : book,
),
);
}
return (
<main>
<h1>Reading list</h1>
<Filters
query={query}
onQueryChange={setQuery}
hideRead={hideRead}
onHideReadChange={setHideRead}
/>
<BookList books={visible} onToggleRead={handleToggleRead} />
<Summary unread={unread} total={books.length} />
</main>
);
}
export default App;
That is a working application, and every idea in this course is in it.
Read App once more, because it is the shape of nearly every React component you will
write from here. Three pieces of state at the top, two derived values calculated during
render, and one function that changes state by building a new array rather than modifying
the old one. Then markup that hands each child exactly what it needs and nothing else.
Notice what is not there. There is no code that runs when something changes: no effect
keeping unread in step with books, because unread is recalculated every render and
cannot drift, and no stored copy of the filtered list. Leaving that machinery out is
deliberate.
Things worth trying
- Add a “clear filters” button. Notice you do not have to tell anything to update.
- Add a book with a form, using lesson 8. Where does the new
id come from?
- Sort the list alphabetically. Remember
sort mutates - use toSorted() or copy first.
- Change
key={book.id} to key={index} - you will need (book, index) in BookList’s
map - then filter while a checkbox is focused. Lesson 5, live.
- Try moving
query into Filters and watch the filtering break. That is why it lives
in App.
Recap
The procedure is: draw the boxes, build it static with props only, list every fact and
cross out the ones you can calculate, put each remaining fact in the nearest component that
contains everyone who needs it, then add handlers that replace state rather than modify it.
The order matters more than it looks - most React apps that become hard to work with went
wrong at step 3, by storing something they could have derived.
Next, and last: what this course left out, and what is worth learning after it.