React State Colocation Patterns: Where State Should Actually Live
A practical guide to deciding where state belongs in a React app, with patterns for lifting, colocating, and splitting state for performance and clarity.
What you'll learn
- ✓Why state location affects re-renders
- ✓How to lift state only as far as needed
- ✓When to split state into smaller components
- ✓How global stores fit into the picture
- ✓Patterns for derived state
Prerequisites
- •Comfortable with JS and HTML
- •Basic React component knowledge
What and Why
React makes it tempting to hoist all state to the top of the tree, “just in case” something else needs it. The result is a giant component that re-renders on every keystroke and is a nightmare to read. State colocation is the discipline of pushing state down to the lowest component that actually needs it.
The payoff is large. Less code in shared parents, fewer re-renders, simpler reasoning, and clearer ownership. The cost is small: occasionally you have to lift a piece of state when a sibling needs it.
Mental Model
Picture your component tree as a corporate org chart. State is data, and every component above it has to handle it whether they want to or not. Putting payroll data at the CEO level means everyone reads emails about salaries. Push it down to HR and only HR knows.
The rule of thumb: find every component that reads or writes a piece of state. Their closest common ancestor is where that state should live. Not higher. Not the root.
App
+-- Header (needs: theme)
+-- Sidebar (needs: theme)
+-- Main
+-- Article
+-- Comments (needs: open?)
+-- Form (needs: draft)
theme -> lives in App (Header + Sidebar share it)
open -> lives in Article (only Comments cares)
draft -> lives in Form (nobody else cares) Hands-on Example
A classic anti-pattern is keeping form state in a parent:
function Page() {
const [name, setName] = useState('');
return (
<>
<Header />
<ExpensiveChart />
<NameForm name={name} setName={setName} />
</>
);
}
Every keystroke re-renders Header and ExpensiveChart, even though they do not care about the name. Colocate:
function NameForm() {
const [name, setName] = useState('');
return <input value={name} onChange={(e) => setName(e.target.value)} />;
}
function Page() {
return (
<>
<Header />
<ExpensiveChart />
<NameForm />
</>
);
}
Now only NameForm re-renders on each keystroke. The chart stays calm.
When you do need to share, lift just enough. If two sibling forms need to know about a “dirty” flag, lift it to their parent, not to the app root. If the whole app needs a current user, that is the right time to use a context or a store like Zustand.
Derived state should usually be calculated, not stored. If you can compute it from existing state on render, do that. Storing fullName = firstName + lastName invites them to fall out of sync.
const fullName = `${firstName} ${lastName}`;
If the calculation is expensive, wrap it with useMemo. Do not invent extra state for performance you do not need.
Common Pitfalls
The first pitfall is premature lifting. Developers see state and think, “what if something else needs it later?” That something else rarely arrives, and the cost of lifting is real. Lift when needed, not in advance.
The second is duplicate sources of truth. Keeping the same value in two places, like both Redux and component state, leads to sync bugs. Pick one home for each piece of state.
The third is over-using context. Context re-renders every consumer when the value changes. A giant context with everything in it becomes a global re-render trigger. Split contexts by concern, or reach for a store with selector-based subscriptions.
Finally, beware of storing things that are not really state, like refs to DOM nodes or the current scroll position. If reading and writing should not trigger a render, use useRef instead.
Best Practices
- Start with state colocated. Lift only when a sibling proves it needs the value.
- Compute derived values; do not store them unless the cost is high.
- Split large components when their state stops feeling related.
- Use context for truly app-wide concerns: theme, auth, feature flags.
- For complex shared state, prefer a store with selectors over giant contexts.
Wrap-up
Where state lives shapes how an app feels: how fast it renders, how easy it is to refactor, how surprising the bugs are. Colocation is the discipline of putting each piece of state at its rightful home. The rule is simple: lowest common ancestor, no higher. Master this and your components shrink, your re-renders drop, and your codebase becomes much easier to navigate.
Related articles
- React React Forms: Controlled vs Uncontrolled Inputs Explained
Understand the difference between controlled and uncontrolled inputs in React, when to use each, and how to combine them with refs and form libraries.
- React The React key Prop: A Deep Dive Into Identity and Reconciliation
Understand what the React key prop actually does, how it drives reconciliation, and why a bad key choice causes mysterious state and animation bugs.
- React React.memo, useMemo, and useCallback Demystified
When memoization actually helps in React: the difference between React.memo, useMemo, and useCallback, and how to avoid memoizing for no reason.
- React React Render Props vs Hooks
Compare render props and hooks as two ways to share reusable logic in React, with examples, mental models, and guidance on which pattern fits modern codebases.