Command Palette

Search for a command to run...

Hectal
PHASE 2Beginner ~26 min· topic 7 of 7

Topic 2.7

Thinking in React

In one line

Thinking in React is the five-step method for turning a design into an app: break the mockup into a component tree, build a static version with props only, find the minimal state, decide which component owns each piece of state, and finally pass callbacks down so children can change it (inverse data flow).

Think of it like this

Building a LEGO set from the picture on the box. You don't start by pouring all the bricks out and sticking them together randomly. You look at the picture and find the big parts (the house, the garden, the car), build each part without moving pieces first, then add the bits that move (the door that opens), and finally connect them. Thinking in React is that plan for a screen. The React team calls it the way to "think in React", and it works for every app, from a search box to a whole dashboard.

Words you'll meet

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

Mockup
A picture or design of the screen before it's built.
Component hierarchy
The tree of components you get by drawing nested boxes on the mockup.
Single responsibility
The idea that each component should do one job. If it does several, split it.
Static version
The app rendered from data with props only: it looks right but nothing reacts yet.
Minimal state
The smallest set of changing values from which everything else on screen can be computed.
DRY
"Don't repeat yourself": store a fact once and compute everything else from it.
Closest common parent
The lowest component in the tree that is above all the components needing a piece of state.
Inverse data flow
Children changing the parent's state by calling callback props the parent passed down.

Step by step

01Start from the mockup and the data

The design: a search box, an "Only show products in stock" checkbox, and a table of products grouped by category, with out-of-stock names in red. The API returns a flat list.

products.jsonwhole filejson
[
  { "category": "Fruits", "price": "$1", "stocked": true, "name": "Apple" },
  { "category": "Fruits", "price": "$1", "stocked": true, "name": "Dragonfruit" },
  { "category": "Fruits", "price": "$2", "stocked": false, "name": "Passionfruit" },
  { "category": "Vegetables", "price": "$2", "stocked": true, "name": "Spinach" },
  { "category": "Vegetables", "price": "$4", "stocked": false, "name": "Pumpkin" },
  { "category": "Vegetables", "price": "$1", "stocked": true, "name": "Peas" }
]

02Step 1: draw boxes and name them

Five components, each with one job: FilterableProductTable holds the whole app, SearchBar takes user input, ProductTable shows and filters the list, ProductCategoryRow shows a category heading, and ProductRow shows one product.

Step 1: draw boxes and name themdiagram
Rendering diagram…

03Step 2: build it static, with props only

Each component gets its data through props and returns JSX. No useState yet. The first live example below is exactly this version: it looks finished but nothing reacts.

04Step 3: find the minimal state

Go through every piece of data. The product list is passed in and doesn't change: not state. The search text changes and can't be computed: state. The checkbox value changes and can't be computed: state. The filtered list can be computed from the products, the search text and the checkbox: not state.

State planwhole filetext
Data                       Changes?  From props?  Computable?  State?
-------------------------  --------  -----------  -----------  ------
product list               no        yes          -            no
search text                yes       no           no           YES
checkbox (in stock only)   yes       no           no           YES
filtered list of products  yes       -            yes          no

05Step 4: decide who owns the state

ProductTable filters by both values; SearchBar displays both values. Their closest common parent is FilterableProductTable, so both pieces of state live there and go down as props.

FilterableProductTable.jsxwhole filejsx
function FilterableProductTable({ products }) {
  const [filterText, setFilterText] = useState("");
  const [inStockOnly, setInStockOnly] = useState(false);
  return (
    <div>
      <SearchBar filterText={filterText} inStockOnly={inStockOnly} />
      <ProductTable products={products} filterText={filterText} inStockOnly={inStockOnly} />
    </div>
  );
}

06Step 5: send changes back up

Pass the setters to SearchBar as onFilterTextChange and onInStockOnlyChange. The inputs call them on every change, the parent's state updates, and the new values flow down to both children. The second live example is the finished app.

SearchBar.jsxwhole filejsx
<input
  type="text"
  value={filterText}
  placeholder="Search..."
  onChange={(e) => onFilterTextChange(e.target.value)}
/>
Step 5: send changes back updiagram
Rendering diagram…

Try it yourself

  1. 1

    Add a product

    In both live versions, add { category: "Fruits", price: "$3", stocked: true, name: "Mango" } after Passionfruit. Only the data changes; every component already handles it.

  2. 2

    Add a third filter

    In the finished app, add a "Max price" number input. Run the three questions of step 3: is it state? (Yes: it changes and can't be computed.) Where does it live? (The same parent.) Then add it, pass it down to both children, and filter with Number(product.price.slice(1)) <= maxPrice.

  3. 3

    Predict the logs

    In the last example, change the scripted text from "p" to "pu". Predict the last line before running. (Results shows: Pumpkin.)

Code & diagrams

Step 2: the static version (props only) New tab

Everything comes from `PRODUCTS` through props, and the components are pure. Typing in the box or ticking the checkbox changes nothing in the table yet: there is no state. (`key` on each row lets React tell rows apart, Topic 3.3.)

Preview

Press Run to see the page.

Console

Nothing logged yet.
Steps 3 to 5: state in the right place, callbacks up New tab

Type `p` or tick the checkbox. Two pieces of state live in `FilterableProductTable`, the closest common parent of `SearchBar` and `ProductTable`. `SearchBar` changes them only through the callbacks it received. Inputs whose value comes from state are called controlled inputs (Topic 5.1).

Preview

Press Run to see the page.

Console

Nothing logged yet.
Watching inverse data flow happen New tab

First render: both children show the empty search. Then a `p` is typed: the child calls the callback (up), the parent's state changes, and both children render again with the new value (down).

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

SearchBox shows: ""
Results shows: Apple, Peas, Pumpkin
App: state changes to "p"
SearchBox shows: "p"
Results shows: Apple, Peas, Pumpkin

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

Keep the state in the wrong component

In the finished app, move filterText state into SearchBar (with its own useState) and stop passing it from the parent. ProductTable gets filterText="".

terminal
$ Preview
── what you'll see ──
The search box accepts typing, but the table never filters.

Break #2

Make the input controlled but forget the callback

Remove onFilterTextChange={setFilterText} from <SearchBar ... />.

terminal
$ Console (on the first keystroke)
── what you'll see ──
Uncaught TypeError: onFilterTextChange is not a function

Myth vs fact

Myth

Start by writing state and effects, then add the markup.

Fact

Build the static version first. It's mostly typing and gives you a correct component tree before you add the hard part, which is state.

Myth

Store the filtered list in state so it's ready to show.

Fact

Anything you can compute from existing props and state isn't state. Storing it creates two sources of truth that drift apart (Topic 6.2).

Myth

Children should update their parent's data directly.

Fact

Children call callback props; the parent changes its own state. That's inverse data flow, and it keeps every change traceable.

Pro corner

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

  • ▸

    Step 3 is the step seniors get right and juniors skip. Redundant state (a stored filteredProducts, a fullName next to firstName and lastName) is the root of most "the screen shows stale data" bugs. Topic 6.1 lists the rules for structuring state.

  • ▸

    Step 4's "closest common parent" is a starting point, not a law. Some state belongs in the URL (filters that should survive a reload or be shareable, Topic 13.3), some on the server (Phase 14), some in context when many distant components need it (Phase 9). Ask where it lives before you ask how to share it.

  • ▸

    Filtering during render is fine for hundreds of rows. For thousands, measure first, then consider useMemo (Topic 16.4) or useDeferredValue so typing stays responsive (Topic 16.7).

  • ▸

    Inverse data flow is explicit by design. Frameworks with two-way binding save typing, but in React you can always find who changed a value by following the callback. That traceability is a big part of why React apps stay debuggable as they grow.

Remember this

  1. 1

    Step 1: Break the UI into a component hierarchy. Draw boxes around every part of the mockup and name them. Use the single responsibility principle: a component should ideally do one thing; if it grows to do several, split it. Your data often helps: if the JSON has categories of products, the UI probably has a row per category and a row per product. Nest the boxes to get a hierarchy (a tree) with one top component.

  2. 2

    Step 2: Build a static version. Write components that render the mockup from the data using props only: no state, no interactivity. A static version is lots of typing and little thinking; interactivity is little typing and lots of thinking, so keep them apart. Build top-down (start at the top component) for small apps, or bottom-up (start with the smallest pieces) for big ones. At the end you have reusable components and data flowing one way, from the top component down through props.

  3. 3

    Step 3: Find the minimal but complete representation of UI state. State is the data that changes over time and that the app must remember (Phase 4). Keep it minimal: store only what you can't compute (the principle is called DRY, "don't repeat yourself"). For each piece of data ask three questions. Does it stay the same over time? Then it isn't state. Is it passed in from a parent through props? Then it isn't state. Can you compute it from existing state or props? Then it definitely isn't state. Whatever is left is state.

  4. 4

    Step 4: Identify where your state should live. React data flows down, so state must live in a component that is above every component that uses it. For each piece of state: find every component that renders something based on it, find their closest common parent, and put the state there (or higher if that makes more sense). This is called lifting state up (Topic 6.3). If no existing component fits, create one just to hold the state.

  5. 5

    Step 5: Add inverse data flow. Now the UI shows state but can't change it: typing in the search box does nothing, because the box's value comes from the parent's state. Children can't change a parent's state directly. So the parent passes its setter functions (or handlers that call them) down as props, such as onFilterTextChange, and the child calls them when the user types. Data flows down as props; changes flow up through callbacks. That upward path is inverse data flow. It's a bit more typing than two-way binding, but you can always see exactly how data changes.

  6. 6

    The method scales. On a team, step 1 is often done on the design file itself, step 2 produces a component library you can test in isolation, steps 3 and 4 become a written state plan, and step 5 is where the app comes alive. You'll use the same five steps for the capstone in Phase 27 and in machine-coding interviews (Topic 26.5).

Explain it without notes

01

List the five steps of Thinking in React.

02

How do you decide whether a piece of data should be state?

03

How do you decide which component should own a piece of state?

04

What is inverse data flow, and why does React use it instead of two-way binding?

Practice

01

Apply steps 1 and 3 to a to-do app with: a text input and Add button, a list of to-dos with checkboxes, and a footer saying 2 items left. List the components and the minimal state.

02

In the finished product table, the header should show Showing 4 of 6 products. Is the count state? Where would you compute it?

Trade-offs

  • ↔

    Lifting state to a common parent keeps one source of truth but makes the parent re-render its whole subtree on every change and can force props through middle components. When that hurts, composition (Topic 2.5), context (Phase 9) or a store (Phase 10) help.

  • ↔

    Building a static version first costs time up front, and on tiny widgets you may merge steps. On anything bigger, skipping it usually means rewriting components once the state plan becomes clear.

Done when you can

  • I can break a mockup into a component tree with one job per component.

  • I can build a static version with props only.

  • I can find the minimal state with the three questions.

  • I can place state in the closest common parent.

  • I can add inverse data flow with callback props.