Back to all notes

Colocating State: Keep State Close to Where It's Used

Reduce complexity by moving state into the component that actually needs it.

2 min read

Colocating state means keeping state as close as possible to the component that uses it. It’s the fix for a habit I see constantly: state parked high in the tree and passed down everywhere, while only one component actually reads it.

When state lives higher than necessary, the parent does extra work, props trickle down levels that don’t want them, and it stops being obvious who owns what. Colocation puts state next to the UI that depends on it.

The Parent That Shouldn’t Know

Here state is lifted unnecessarily high. App holds isVisible, threads it into ToggleSection, and gains two props it has no interest in:

import { useState } from "react";

function ToggleSection({ isVisible, setIsVisible }) {
  return (
    <div>
      <button onClick={() => setIsVisible(!isVisible)}>
        {isVisible ? "Hide" : "Show"} Content
      </button>
      {isVisible && (
        <div>
          <p>This content can be toggled!</p>
        </div>
      )}
    </div>
  );
}

function App() {
  const [isVisible, setIsVisible] = useState(false);
  return (
    <div>
      <h1>Colocating State Example</h1>
      <ToggleSection isVisible={isVisible} setIsVisible={setIsVisible} />
    </div>
  );
}

export default App;

Move It Down

ToggleSection is the only component that cares about visibility, so the state belongs inside it. Remove isVisible from the parent, add const [isVisible, setIsVisible] = useState(false) inside ToggleSection, wire the button to the local state, and render conditionally from it.

import { useState } from "react";

function ToggleSection() {
  const [isVisible, setIsVisible] = useState(false);

  return (
    <div>
      <button onClick={() => setIsVisible(!isVisible)}>
        {isVisible ? "Hide" : "Show"} Content
      </button>
      {isVisible && (
        <div>
          <p>This content can be toggled!</p>
        </div>
      )}
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Colocating State Example</h1>
      <ToggleSection />
    </div>
  );
}

The component becomes self-contained and reusable, and the parent gets simpler by default. No props for visibility control at all.

When Not to Colocate

Two situations push the other way. If multiple siblings need to read or update the same state, lift it to the nearest common parent. If many deep descendants need the value, reach for Context instead of drilling.

Start by colocating state in the smallest component that needs it, and lift only when duplication actually appears. Controlled components like inputs should keep their transient UI state local unless a parent truly coordinates it. And if a child must expose some control upward, a callback prop like onToggle beats pushing all the state up.