5 min read
0%

State Colocation Strategy

Back to Blog
State Colocation Strategy

State Colocation Strategy

State colocation means putting state as close to where it’s used as possible. It’s the counterpart to lifting state — you lift when necessary, but colocate by default.

Why It Matters

State high in the tree causes more components to re-render when it changes. State at the leaf level means only that leaf re-renders.

// Bad: form state in the page — re-renders the whole page on every keystroke
function SettingsPage() {
  const [email, setEmail] = useState('');
  const [username, setUsername] = useState('');
  return (
    <div>
      <HeavyChart /> {/* re-renders on every keystroke */}
      <EmailInput email={email} onChange={setEmail} />
      <UsernameInput username={username} onChange={setUsername} />
    </div>
  );
}

// Good: form state colocated in the form component
function SettingsPage() {
  return (
    <div>
      <HeavyChart /> {/* unaffected by form state */}
      <SettingsForm />
    </div>
  );
}

function SettingsForm() {
  const [email, setEmail] = useState('');
  const [username, setUsername] = useState('');
  return (...);
}

The Colocation Decision Tree

  1. Used only in one component? → put it there
  2. Used in a few adjacent siblings? → lift to their common parent
  3. Used across many unrelated components? → context or an external store

UI State vs Server State

UI state (modals, selected tabs, hover) — colocate at the component or feature level.

Server state (fetched data, cache) — libraries like React Query or SWR handle sharing and caching automatically. Don’t duplicate it in component state.

Avoid Derived State

// Bad: totalPrice must be kept in sync with items manually
const [items, setItems] = useState([]);
const [totalPrice, setTotalPrice] = useState(0);

// Good: derive from the source of truth
const [items, setItems] = useState([]);
const totalPrice = items.reduce((sum, item) => sum + item.price, 0);

Derived state creates synchronization problems. Compute inline or with useMemo.

Colocation with Custom Hooks

Group related state into a custom hook at the right level:

function useCartState() {
  const [items, setItems] = useState([]);
  const addItem = useCallback(item => setItems(s => [...s, item]), []);
  const removeItem = useCallback(id => setItems(s => s.filter(i => i.id !== id)), []);
  const total = useMemo(() => items.reduce((s, i) => s + i.price, 0), [items]);
  return { items, addItem, removeItem, total };
}

Keep the hook at the level where the cart lives — not at the app root unless the cart is needed everywhere.


Canvas is not supported in your browser