← React for Beginners

Thinking in React: build the whole thing

Lesson 11 of 12 · 16 min

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:

A wireframe of the reading list: a heading, a filter box and a hide-read checkbox, three books each with a read checkbox, title and author, and a count of unread books at the bottom. Annotations mark which component owns each region. Reading list filter... hide read The Shipping Forecast Kate Bell Deep Work Cal Newport The Art of Doing Science Richard H. 2 of 3 unread Filters BookRow BookList Summary Draw the boxes first. The hierarchy falls out of the picture.
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.