Skip to content
C Codeloom
React

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.

·4 min read · By Codeloom
Intermediate 9 min read

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)
Colocate state at the lowest common ancestor

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.