Thinking in React
Every chapter so far has taught one piece: components, props, state, events, lists. This chapter is where they come together into a method. Given a design, how do you decide what the components are, which values are state, and where that state should live? There's a repeatable way to work through it, and once you've done it a few times you'll reach for the same five steps on almost every screen you build.
We'll walk one small UI end to end: a searchable product list. It shows a set of products, a search box to filter them by name, and a checkbox to hide anything out of stock. Here's the data it renders:
const PRODUCTS = [
{ id: 1, name: 'Apple', price: '$1', stocked: true },
{ id: 2, name: 'Dragonfruit', price: '$1', stocked: false },
{ id: 3, name: 'Passionfruit', price: '$2', stocked: true },
]Step 1: Break the design into a component tree
Look at the design and draw boxes around the pieces. A good box is one thing that does one job, the same instinct you use when deciding what a function should do. Our little screen splits cleanly into a few:
FilterableProductListwraps the whole thing.SearchBarholds the text input and the in-stock checkbox.ProductTableshows the list of products.ProductRowis a single row in that table.
Those nest inside each other, so they form a component tree:
FilterableProductList
├── SearchBar
└── ProductTable
└── ProductRow (one per product)There's no single correct split, and the components chapter's rule of thumb applies here: keep each one small and give it a clear job. A row knows how to draw one product; the table knows how to lay out rows; the search bar knows about the controls. If a piece starts doing two unrelated things, that's usually a sign to split it.
Step 2: Build a static version with props only
Now build a version that renders the data but does nothing yet. No clicking, no filtering, no typing that changes anything. Data flows one way, down the tree, through props. No useState appears anywhere in this step.
function FilterableProductList({ products }) {
return (
<div>
<SearchBar />
<ProductTable products={products} />
</div>
)
}
function ProductTable({ products }) {
return (
<table>
<tbody>
{products.map(product => (
<ProductRow key={product.id} product={product} />
))}
</tbody>
</table>
)
}
function ProductRow({ product }) {
return (
<tr>
<td>{product.name}</td>
<td>{product.price}</td>
</tr>
)
}
function SearchBar() {
return (
<form>
<input type="text" placeholder="Search..." />
<label>
<input type="checkbox" /> Only show products in stock
</label>
</form>
)
}This renders the full list every time. The search box and checkbox are on screen, but they don't do anything yet. That's the point of the step: you get the whole layout working from props alone, and you confirm the tree from step 1 actually holds together, before any of the moving parts are involved. The products array comes in as a prop at the top and gets passed down; nothing here remembers or changes anything.
Step 3: Find the minimal state
Now make it interactive, and the first question is which values need to be state at all. State is expensive to keep correct, so you want as little of it as possible. The rest you'll calculate.
The test for each value is short. Ask two things:
- Does it change over time?
- Is it something you can't calculate from another value you already have?
If both are yes, it's state. If either is no, it isn't. Walk the values in our UI through that test:
- The list of products. It's passed in and doesn't change while the user pokes at the page, so it's not state. It's a prop.
- The search text the user typed. It changes over time, and there's nothing to calculate it from. It's state.
- Whether the in-stock checkbox is on. Same story: it changes, and nothing derives it. It's state.
- The filtered list actually shown on screen. It changes, but you can compute it from the products, the search text, and the checkbox. So it is not state.
That last one is the rule worth burning in: derive, do not duplicate. The visible list is the products passed through the current filters, so you calculate it during render instead of storing a second copy in state:
const visible = products.filter(product => {
const matchesText = product.name
.toLowerCase()
.includes(filterText.toLowerCase())
const matchesStock = !inStockOnly || product.stocked
return matchesText && matchesStock
})If you had stored visible in its own useState, you'd have to remember to update it every single time the products, the text, or the checkbox changed. Miss one and the screen shows a stale list. Deriving it means there's only one source of truth, and it can never fall out of sync, because it's recomputed fresh on every render. So the minimal state here is exactly two values: filterText and inStockOnly.
Step 4: Decide where each piece of state should live
You have two pieces of state. Now figure out which component owns each one. The rule is to put state in the closest common parent of every component that needs it, which is the same idea the lifting state up chapter goes into in depth.
Work out who needs each value:
SearchBarneeds both values, because it renders the input and the checkbox that show them.ProductTableneeds both values too, because it filters the rows using them.
SearchBar and ProductTable are siblings. A value can only flow down through props, so neither sibling can hand state to the other. The closest component that sits above both of them is FilterableProductList. That's where the state goes:
import { useState } from 'react'
function FilterableProductList({ products }) {
const [filterText, setFilterText] = useState('')
const [inStockOnly, setInStockOnly] = useState(false)
return (
<div>
<SearchBar
filterText={filterText}
inStockOnly={inStockOnly}
onFilterTextChange={setFilterText}
onInStockOnlyChange={setInStockOnly}
/>
<ProductTable
products={products}
filterText={filterText}
inStockOnly={inStockOnly}
/>
</div>
)
}The state lives in the parent, and the values flow down to both children as props. ProductTable reads them to build its visible list; SearchBar reads them to fill in the input and the checkbox.
Step 5: Add the interaction that updates state
The values reach the children, but the user still can't change them. Data only flows down, so to send a change back up you pass the setters down as props and let the child call them. FilterableProductList already passes onFilterTextChange and onInStockOnlyChange above. SearchBar wires them to the inputs:
function SearchBar({
filterText,
inStockOnly,
onFilterTextChange,
onInStockOnlyChange,
}) {
return (
<form>
<input
type="text"
placeholder="Search..."
value={filterText}
onChange={e => onFilterTextChange(e.target.value)}
/>
<label>
<input
type="checkbox"
checked={inStockOnly}
onChange={e => onInStockOnlyChange(e.target.checked)}
/>{' '}
Only show products in stock
</label>
</form>
)
}Now the loop is closed. Typing in the box calls onFilterTextChange, which is setFilterText up in the parent. That updates the state, the parent re-renders, the new filterText flows back down to both children, ProductTable recomputes its visible list, and the screen matches what the user typed. The checkbox works the same way. State goes down as a value, changes come back up as a function call, and the derived list follows along on its own.
That's the whole method: draw the component tree, build it static with props, find the minimal state, lift it to the right owner, then wire the interaction. Almost any screen you meet yields to those five steps.
Next up: Accessible React, where the interface you just designed becomes usable by everyone.

