React design decision flowchart
This is the mental map I run through. Three flowcharts because they’re orthogonal questions: structure, state, and performance. You ask them in order — get structure right first, then state, then optimize.
1. Structure: where does this thing live?
flowchart TD
A[New piece of UI or behavior] --> B{Does it persist<br/>across navigation?}
B -->|Yes| C[Root layout client component]
B -->|No| D{Is it shared across<br/>multiple pages?}
D -->|Yes| E[Shared component in /components,<br/>imported where needed]
D -->|No, page-specific| F{Is it server-renderable?}
F -->|Yes — pure data + markup| G[Server component<br/>in page.tsx or layout.tsx]
F -->|No — interactive,<br/>uses state/effects| H["use client" leaf component<br/>imported by server parent]
C --> I{Does it need DOM access<br/>or browser-only APIs?}
I -->|Yes| J[Component owns a ref;<br/>store/context exposes ref-bound API]
I -->|No| K[Pure React state]
H --> L{Single instance only?<br/>e.g. modal, player, dialog}
L -->|Yes| M[Mount once in root layout,<br/>control via global store]
L -->|No| N[Mount inline where used]Key principle: the React tree position determines lifecycle. Where a component sits decides when it mounts and unmounts. That’s a more important decision than what it looks like.
2. State: where does this data live?
flowchart TD
A[I need to track some state] --> B{Is it derivable from<br/>props, URL, or other state?}
B -->|Yes| C[Derive in render —<br/>NOT state. No setState.]
B -->|No| D{Who needs to read it?}
D -->|One component| E[useState local]
D -->|Component + immediate children| F[useState + prop passing]
D -->|Many components in a subtree| G{Does it change often<br/>4+Hz like timeupdate?}
D -->|Many components across<br/>the app, including root| G
G -->|No — changes on user action| H[Context provider]
G -->|Yes — high frequency| I[External store +<br/>useSyncExternalStore +<br/>selector hook]
E --> J{Does it need to survive<br/>navigation or refresh?}
F --> J
H --> J
I --> J
J -->|Survives navigation,<br/>not refresh| K[Lift to a higher level OR<br/>use external store]
J -->|Survives refresh| L{Sensitive?}
L -->|No| M[localStorage]
L -->|Yes — user data| N[Server, fetched via<br/>server component or API]
J -->|Per-page only| O[Stay local; don't lift]Two traps interns fall into:
- Storing derivable data in state. If you have
firstNameandlastNamein state, you don’t also storefullName. Compute it. State that mirrors other state is a bug factory. - Putting everything in Context. Context re-renders ALL consumers on ANY change. For high-frequency state (audio timeupdate, mouse position, scroll), you need an external store with selectors so only the components that read a specific slice re-render.
3. Performance: should I care yet?
flowchart TD
A[Is the UI feeling slow?] --> B{Have I measured?}
B -->|No| C[Don't optimize —<br/>use React DevTools profiler first]
B -->|Yes| D{What's slow?}
D -->|Re-render storm| E[Find the highest re-rendering<br/>component in the profiler]
E --> F{Is it re-rendering<br/>because parent re-renders<br/>but its props haven't changed?}
F -->|Yes| G[memo, useMemo for object props,<br/>useCallback for function props<br/>OR React Compiler]
F -->|No — own state changed| H[Split state — does the whole<br/>component need it, or one leaf?]
D -->|Animation jank| I{Animating layout<br/>properties? width/height/top/left}
I -->|Yes| J[Move to transform/opacity —<br/>GPU-accelerated, skip layout]
I -->|No| K[will-change: transform,<br/>or move animation to CSS]
D -->|Slow initial render| L{Is server component possible?}
L -->|Yes| M[Convert to server component,<br/>fetch on server]
L -->|No| N[Lazy load with dynamic import,<br/>or move below the fold]
D -->|Many list items| O[Virtualize with react-virtuoso<br/>or tanstack-virtual<br/>— only if 100+ items]The meta-flowchart: what question am I even answering?
When stuck, ask in this order:
flowchart LR
A[Stuck on a React decision] --> B{Is it about<br/>where the code lives?}
B -->|Yes| C[Structure question →<br/>flowchart #1]
B -->|No| D{Is it about<br/>what data lives where?}
D -->|Yes| E[State question →<br/>flowchart #2]
D -->|No| F{Is it about speed?}
F -->|Yes| G[Performance question →<br/>flowchart #3]
F -->|No| H{Is it about<br/>how it looks or feels?}
H -->|Yes| I[Design question —<br/>NOT a React question]
H -->|No| J[You're probably solving<br/>the wrong problem]Most “React feels hard” moments are actually #4 — design — masquerading as a code question. The audioclip player was a great example: “how do I structure the play bar component?” is the wrong question. The right one is “where does the audio element live?” That’s structure (#1), and once you answer it the rest falls out.
The two heuristics that override the flowcharts
- Co-location until it hurts. Put state, types, helpers as close to where they’re used as possible. Lift them only when a second consumer appears. Don’t pre-extract for hypothetical reuse.
- The boundary is the seam. Server → client component boundaries, controlled → uncontrolled component boundaries, sync → async boundaries — these are where bugs concentrate. When designing, draw the boundary explicitly and make sure both sides know who owns what.