Skip to content
React rc state-management 5 min read

Local vs Global State

Choosing where state lives is one of the highest-leverage decisions in a React app. Put it too low and you end up duplicating and synchronizing values; reach for a global store too early and you trade a small inconvenience for app-wide coupling, harder testing, and needless re-renders. The guiding principle is colocation: keep each piece of state as close as possible to where it is used, and only widen its scope when a concrete need forces you to.

What “local” and “global” actually mean

Local state is owned by a single component (or a small subtree) via useState or useReducer. It is born when the component mounts and dies when it unmounts. Global state is shared across distant parts of the tree — typically through Context, or a library such as Redux Toolkit, Zustand, or Jotai — and outlives any single component.

AspectLocal stateGlobal state
OwnerOne component / subtreeThe whole app (or a feature)
LifetimeTied to the componentTied to the app session
AccessDirect props/hooksStore, Context, or selector
Best forUI toggles, form fields, hover/focusAuth, theme, cart, cross-route data
Cost when overusedProp drillingCoupling, re-renders, harder tests

Start local: colocation

Most state is genuinely local. A modal’s open flag, an input’s value, or whether a row is expanded only concerns the component rendering it. Keep it there.

import { useState } from "react";

function SearchBox({ onSearch }) {
  const [query, setQuery] = useState("");

  function handleSubmit(e) {
    e.preventDefault();
    onSearch(query.trim());
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <button type="submit">Go</button>
    </form>
  );
}

The query value never leaves this component, so promoting it anywhere else would only add noise. Colocation keeps related logic together, makes the component easy to reason about, and lets it be deleted or moved without ripple effects.

Lift when siblings must agree

When two or more components need to read or write the same value, move that state up to their nearest common parent — the classic “lifting state up” pattern. The parent owns the state and passes it down with a setter.

import { useState } from "react";

function TemperatureConverter() {
  const [celsius, setCelsius] = useState(20);

  return (
    <div>
      <CelsiusInput value={celsius} onChange={setCelsius} />
      <FahrenheitDisplay celsius={celsius} />
    </div>
  );
}

function CelsiusInput({ value, onChange }) {
  return (
    <input
      type="number"
      value={value}
      onChange={(e) => onChange(Number(e.target.value))}
    />
  );
}

function FahrenheitDisplay({ celsius }) {
  return <p>{(celsius * 9) / 5 + 32}°F</p>;
}

Lifting is the right answer surprisingly often. Before reaching for a global store, ask whether a shared parent already exists — it usually does.

When to go global

Lifting breaks down when the common ancestor is far away and the value has to be threaded through many intermediate components that do not care about it (prop drilling), or when state is truly app-wide. Reach for global state when:

  • The data is needed by many, distant components (current user, theme, locale, feature flags).
  • It must survive navigation between routes (a shopping cart, a draft document).
  • Multiple unrelated features read and mutate the same source of truth.
  • You need it accessible outside the render tree (e.g., in event handlers, interceptors, or background tasks).

For low-frequency, read-mostly values, Context is enough. For frequently updated or large state, a dedicated store with selectors avoids re-rendering every consumer.

import { create } from "zustand";

const useCartStore = create((set) => ({
  items: [],
  add: (item) =>
    set((state) => ({ items: [...state.items, item] })),
  clear: () => set({ items: [] }),
}));

function CartBadge() {
  // Subscribes only to length — other state changes won't re-render this.
  const count = useCartStore((s) => s.items.length);
  return <span className="badge">{count}</span>;
}

The cost of premature global state

Going global “just in case” is a common, expensive mistake.

Gotcha: Putting form input or a hover flag into a global store means every keystroke can re-render unrelated consumers, and your store fills up with transient UI noise that obscures the data that actually matters.

Premature globalization also couples components to a store’s shape, making them harder to reuse and unit-test in isolation, and it turns “where did this value change?” into an app-wide investigation. Local state, by contrast, has an obvious owner and an obvious blast radius.

Tip: Server data (fetched from an API) usually does not belong in your global UI store at all. Tools like RTK Query or React Query own caching, deduping, and refetching far better than hand-rolled global state.

A decision checklist

Walk down this list and stop at the first match:

  1. Is the value used by only one component? → Local state (useState/useReducer).
  2. Is it shared by a few nearby components? → Lift it to the common parent.
  3. Is it server data (lists, entities, queries)? → A data-fetching cache (RTK Query, React Query).
  4. Is it needed by many distant components and read-mostly? → Context.
  5. Is it app-wide, frequently updated, or accessed outside render? → A global store (Redux Toolkit, Zustand, Jotai).

Best Practices

  • Default to local state; treat every promotion to global as a decision that needs justification.
  • Lift state only as high as the closest common ancestor — no higher.
  • Keep transient UI state (open/closed, hover, focus, input drafts) out of global stores.
  • Separate server cache from client UI state; let a query library own remote data.
  • Use selectors so consumers subscribe to the narrowest slice they need.
  • When prop drilling becomes painful through 3+ layers, prefer Context before a full store, and a store only when Context re-renders or ergonomics bite.
  • Co-locate related state, derived values, and the logic that updates them in the same place.
Last updated June 14, 2026
Was this helpful?