Turning a design or feature request into a well-structured React app is a design skill, not just a syntax one. This lesson walks through the five-step process: breaking UI into a hierarchy, building a static version, finding minimal state, placing state correctly, and wiring inverse data flow.
1Design, Not Just Syntax
Knowing hooks and JSX syntax is not the same skill as designing a component tree from a mockup or requirement. Thinking in React is a repeatable five-step process for that design work: breaking the UI into components, building it statically, finding minimal state, deciding where state should live, and adding inverse data flow.
2Step 1: Break the UI into a Component Hierarchy
Looking at a design, draw a box around every distinct piece of UI, then a box around groups of related boxes ā each box becomes a candidate component. A useful heuristic is that if a piece of UI represents one part of your data model, it usually deserves to be its own component.
3Step 2: Build a Static Version First
The entire UI should first be built using only props flowing down the tree, with zero state and zero interactivity. This isolates structural correctness from the added complexity of state management, making bugs in either layer easier to identify separately.
4Step 3: Find the Minimal Set of State
For every piece of data in the app, ask whether it stays constant over time and whether it can be computed from other existing props or state ā if either is true, it isn't state. What remains, data that genuinely changes and can't be derived, forms the minimal set of true state the app needs.
5Step 4: Decide Where State Should Live
For each piece of state, identify every component that renders based on it, then find their closest common ancestor in the tree ā that's where the state should be declared. State should sit as low in the tree as possible while still being accessible to every component that depends on it.
6Step 5: Add Inverse Data Flow
Because state flows down through props but user interactions happen in components lower in the tree, the component that owns the state must pass a setter function down as a prop. Children call that setter to trigger updates to state living above them, completing the loop: data flows down, events flow up.
7Step-by-Step Breakdown
Design, Not Just Syntax. You know the hooks. You know JSX. The harder skill is turning a design mockup or a feature request into a well-structured component tree before you write a single line of code. 'Thinking in React' is a five-step process for doing exactly that, and it's one of the most useful mental models a React developer can have.
Step 1: Break the UI into a Component Hierarchy. Look at your design and draw boxes around every piece of UI, then boxes around groups of boxes. Each box is a candidate component. A good rule of thumb: if a piece of UI represents one part of your data model, it's probably its own component. Name each box, and you've sketched your component tree before writing any code.
Step 2: Build a Static Version First. Build the entire UI using props only, passing data down through the tree, with zero interactivity and zero state. This forces you to focus purely on structure and rendering correctness before dealing with the added complexity of state changes. Resist the urge to add useState at this stage, even if it's tempting.
Why does 'Thinking in React' recommend building a fully static version before adding any state?
- āIt separates structural correctness from the added complexity of state
- āIt's simply faster to type than useState calls
Step 3: Find the Minimal Set of State. Now identify what actually needs to be state. For each piece of data, ask: does it stay constant over time? If so, it's not state. Can it be computed from other props or state? If so, it's not state either ā it's derived. What remains, the data that truly changes and can't be derived, is your minimal state.
Step 4: Decide Where State Should Live. For each piece of state, find every component that renders based on it, then find their closest common parent component. Put the state there. If it's unclear where the common parent should live, a temporary intermediate component is fine ā but always place state as low in the tree as possible while still being shared by everything that needs it.
Two sibling components, SearchBar and ProductTable, both need the current search text. Where should that state live?
- āIn their closest common parent component
- āDuplicated separately inside each sibling
Step 5: Add Inverse Data Flow. State flows down through props, but user input happens in child components lower in the tree. The parent that owns the state must pass a setter function down as a prop, so the child can call it to update the state that lives above it. This closes the loop ā data flows down, and events flow up.
Mastery Achieved. You now have a repeatable process for turning any design or requirement into a well-structured React app: break the UI into a hierarchy, build it static, find the minimal state, place that state at the right level, and wire up inverse data flow. Next, you'll go deeper into component composition ā the techniques for keeping that hierarchy flexible as it grows.
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
This is a design methodology, not a browser API.
Fully applicable.
Fully applicable.
Fully applicable.
Accessibility (A11y)
1Design the Component Hierarchy Around Semantic Structure Too
When breaking a design into components, consider not just visual grouping but the underlying semantic HTML structure (landmarks, headings, lists) so the resulting component tree naturally produces accessible markup.
SEO Implications
- 1
A Clean Component Hierarchy Maps Naturally to Server/Client Boundaries
A well-designed, single-responsibility component tree makes it far easier to decide which pieces should be Server Components versus Client Components, directly affecting how much content ships as indexable server-rendered HTML.
Best Practices
Resist Adding State During the Static Build Step
Even when it's tempting to reach for useState while building the static version, hold off ā finishing the static structure first makes it much easier to identify exactly which values later need to become real state.
Re-derive Rather Than Duplicate
If a value can be computed from state or props you already have, compute it inline during render instead of storing a duplicate copy in its own state ā duplicated state is a common source of out-of-sync UI bugs.
Frequent Bugs
Two sibling components show search results that fall out of sync with each other.
The search state was likely duplicated in each sibling instead of being lifted to their closest common parent and passed down as props ā shared data should have exactly one source of truth.
A component can't update a value that visually lives in a component above it in the tree.
Inverse data flow is missing ā the parent needs to pass a setter function down as a prop so the child can call it, rather than trying to modify the parent's state directly.
Real-World Examples
Designing a Filterable Product Table
Given a mockup of a search bar above a product table, the design breaks into FilterableProductTable (root), SearchBar, and ProductTable with ProductRow children. The search text is the only true state ā it changes and can't be derived ā and it lives in FilterableProductTable since both SearchBar and ProductTable depend on it.
function FilterableProductTable({ products }) {
const [searchText, setSearchText] = useState('');
const filtered = products.filter(p => p.name.includes(searchText));
return (
<>
<SearchBar searchText={searchText} onSearchTextChange={setSearchText} />
<ProductTable products={filtered} />
</>
);
}