Back to React
React
medium
mid

What is state colocation in React and why does it matter?

Put state as close as possible to where it's used. The wrong default — 'lift everything to App' — re-renders the whole tree for any change and obscures ownership. Colocation: input state in the input, modal-open state in the modal trigger, filter state in the filter panel. Lift only when multiple siblings need it; even then, lift to the lowest common ancestor.

4 min read·~10 min to think through

State colocation is the practice of keeping state as close as possible to where it's actually read and written. It's a quiet but high-leverage habit — it makes apps faster, easier to refactor, and easier to reason about.

The anti-pattern

jsx
function App() {
  // ALL state hoisted here
  const [searchQuery, setSearchQuery] = useState("");
  const [modalOpen, setModalOpen] = useState(false);
  const [theme, setTheme] = useState("light");
  const [cart, setCart] = useState([]);
  // ...50 more
  return <Layout {...allTheProps} />;
}

Every change re-renders the entire app. State ownership is opaque — a contributor reading App.jsx has to guess who uses what. The "lift first, optimize later" default leads here.

The colocated alternative

jsx
function App() {
  // Only truly global state
  return (
    <ThemeProvider>
      <CartProvider>
        <Layout />
      </CartProvider>
    </ThemeProvider>
  );
}

function SearchBox() {
  const [query, setQuery] = useState("");   // local to where it's used
  // ...
}

function ProfileMenu() {
  const [open, setOpen] = useState(false);  // local to this menu
  // ...
}

Why it matters

Performance

A state change re-renders the component holding the state and its children. Local state → narrow re-render. Hoisted state → wide re-render.

Colocation is the cheapest performance optimization — no memoization needed.

Ownership clarity

When state lives next to the component that uses it, you don't have to trace where it comes from. A contributor reads one file.

Refactor safety

Removing a feature means removing one file. With hoisted state, you've got dangling props and unused state across the tree.

Testability

Components with local state can be tested without setting up a global store.

When to lift

Only when multiple siblings need to share state. Even then:

  • Lift only to the lowest common ancestor.
  • Pass via props if the tree is shallow.
  • Reach for context only when prop-drilling crosses many layers or many consumers exist.
  • Reach for a store (Zustand, Redux) only when context's "every consumer re-renders" model hurts.

The "right" mental hierarchy

  1. Local useState — default.
  2. Lift to nearest common parent — when 2-3 siblings need it.
  3. Context — for theme, auth, locale — things many components read, rarely changes.
  4. External store — for hot, frequently-changing app state with many subscribers.

Don't skip steps. Most apps need much less of (3) and (4) than they think.

Server state is different

For server state (data fetched from APIs), use React Query / SWR / TanStack Query — they're caches keyed by query, not "state colocation" in the local sense. Don't try to colocate server data; it's shared by nature.

Concrete examples

Form input

jsx
function NameField() {
  const [name, setName] = useState("");
  // ...
}

Don't lift to a Form component just because "maybe the form needs it." Lift when submit actually needs to assemble.

jsx
function DeleteButton() {
  const [confirmOpen, setConfirmOpen] = useState(false);
  return (<><button onClick={() => setConfirmOpen(true)}>Delete</button>
           {confirmOpen && <ConfirmModal ... />}</>);
}

The trigger owns the modal. Don't hoist to App.

Filter panel state

If only the filter panel and the list need filters, lift to the page component (their common parent). Don't go higher.

Anti-patterns

  • "State should live in Redux because it's app state." No — most state is local.
  • "useState here, then I'll move it later." Move on the way down (colocate) more often than up.
  • "Context for everything." Every Provider value change re-renders every consumer.

Interview framing

"Colocation means putting state next to where it's used. The default of 'hoist state to App' looks like good architecture but it slows the app down (wide re-renders), obscures ownership (opaque dependencies), and complicates refactors (state outlives the features). I default to local useState, lift only when multiple siblings need the value, and even then to the lowest common ancestor. Context for things many components read but that change rarely (theme, auth). External stores only for hot, frequently-changing state with many subscribers. Server state is separate — that lives in a query cache, not in local state. The principle: state should be as local as it can be."

Follow-up questions

  • When should you lift state vs colocate?
  • Why is hoisting everything an anti-pattern?
  • When does context become the right answer?
  • How does this affect performance?

Common mistakes

  • Lifting state to App by default.
  • Context with frequently-changing values.
  • Redux for purely local state.
  • Refactoring local state into 'global' just to share with a sibling — lift, don't globalize.

Performance considerations

  • Colocation IS a perf optimization — narrow re-renders without memoization. Often the simplest fix to 'why is my app slow'.

Edge cases

  • Form state shared between input, validation, and submit — lift to the form.
  • Wizard state that survives step transitions — lift above the steps.
  • Cross-app concerns like cart, auth — global is correct.

Real-world examples

  • Dan Abramov's 'Before You memo()' post on colocation.
  • Form libraries (React Hook Form) that intentionally colocate field state.

Senior engineer discussion

Seniors default to local state, lift deliberately, distinguish UI state from server state, and treat context/Redux as targeted tools — not the default container.

Related questions