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.
[
{ "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.
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.
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 no05Step 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.
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.
<input
type="text"
value={filterText}
placeholder="Search..."
onChange={(e) => onFilterTextChange(e.target.value)}
/>Try it yourself
- 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
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
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
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
Console
Nothing logged yet.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
Console
Nothing logged yet.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
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, PumpkinBreak 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="".
Break #2
Make the input controlled but forget the callback
Remove onFilterTextChange={setFilterText} from <SearchBar ... />.
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, afullNamenext tofirstNameandlastName) 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) oruseDeferredValueso 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
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
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
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
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
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
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
List the five steps of Thinking in React.
How do you decide whether a piece of data should be state?
How do you decide which component should own a piece of state?
What is inverse data flow, and why does React use it instead of two-way binding?
Practice
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.
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.