Thinking in React
Split UI into a tree. Identify minimal state. Lift it up.
React doesn't tell you howto break a UI into pieces. It just gives you the pieces. Most beginners stare at a design and freeze. The React team published a five-step process years ago that still works perfectly. Memorize it once and you'll never freeze again.
The starting point: a mock
Imagine a designer hands you this: a product list with a search box and an "only in stock" checkbox. The current code is one giantApp.js with everything hardcoded.
// The mess we're starting with
export default function App() {
return (
<div>
<input placeholder="Search..." />
<label>
<input type="checkbox" /> Only show products in stock
</label>
<table>
<thead><tr><th>Name</th><th>Price</th></tr></thead>
<tbody>
<tr><td>Apple</td><td>$1</td></tr>
<tr><td>Dragonfruit</td><td>$1</td></tr>
<tr style={{ color: "red" }}><td>Passionfruit</td><td>$2</td></tr>
</tbody>
</table>
</div>
);
}We want this to be a real component tree backed by data and state. Here are the five steps.
Step 1: split the UI into a component tree
Draw boxes around every logical region of the mock. Use the single responsibility principle. If a region does more than one thing, split it. For our example:
FilterableProductTable(the whole thing)SearchBar(the search input + checkbox)ProductTable(the table)ProductCategoryRow(a category heading row)ProductRow(a single product row)
Step 2: build a static version with no state
First, just make it render with props. No useState, no events, nothing interactive. You pass data in at the top and let it flow down. This proves your component split is good.
function ProductRow({ product }) {
const name = product.stocked
? product.name
: <span style={{ color: "red" }}>{product.name}</span>;
return <tr><td>{name}</td><td>{product.price}</td></tr>;
}
function ProductTable({ products }) {
return (
<table>
<thead><tr><th>Name</th><th>Price</th></tr></thead>
<tbody>
{products.map((p) => <ProductRow key={p.name} product={p} />)}
</tbody>
</table>
);
}Step 3: find the minimal state
Look at every piece of data on the screen and ask three questions:
- Does it change over time? If no, it's not state, it's a prop or a constant.
- Is it passed in from a parent? If yes, it's a prop, not state.
- Can you derive it from other state or props? If yes, derive it. Don't store it.
For our example:
- The list of products: passed in, not state.
- The search text: changes over time, can't be derived. State.
- The "in stock only" flag: same answer. State.
- The filtered products you actually display: derivable from products + filters. Not state. Compute it during render.
Step 4: decide where state lives
Find the closest common ancestor of every component that needs to read or write each piece of state. That's where the state goes.
SearchBarwrites the search text.ProductTablereads it.- Closest common ancestor:
FilterableProductTable. That's whereuseStategoes.
This is called lifting state up. The state lives in the parent. The children get it as props.
Step 5: wire events back up
Data flows down, events flow up. The parent owns the state, so the parent owns the setter. Pass that setter (or a wrapper) down as a prop. Children call it.
function SearchBar({ text, onTextChange }) {
return (
<input
value={text}
onChange={(e) => onTextChange(e.target.value)}
placeholder="Search..."
/>
);
}
function FilterableProductTable({ products }) {
const [text, setText] = useState("");
const [inStockOnly, setInStockOnly] = useState(false);
return (
<>
<SearchBar text={text} onTextChange={setText} />
<ProductTable products={products.filter(/* ... */)} />
</>
);
}That's the whole flow. Down, up, done.
Try it: the full transformation
Below is the finished version of the example. Notice that FilterableProductTable is the only stateful component. Everything else is a pure renderer of props.
Quiz
You have a list of products and the user can filter them. Where does the filtered list belong?
Recap
- Step 1: break the mock into a component tree by responsibility.
- Step 2: build it static. Props only, no state, no handlers.
- Step 3: identify minimal state. If it's derivable, don't store it.
- Step 4: put each state at the nearest common ancestor of its readers.
- Step 5: pass setters down as props so children can request changes.