Field note / react

publishedupdated 2026-05-04#react#state-management#context#reducer#architecture

React State Management

How I decide state ownership in modern React: local state, URL state, reducers, context, external stores, and the mistakes that create long-term frontend drag.

TL;DR

  • The hard problem is not “Redux or Zustand or Context?” It is “who owns this state, who mutates it, and what is the source of truth?”
  • I separate server state, URL state, local UI state, workflow state, and cross-tree shared state before I pick any tool.
  • Most bad React state architectures come from duplicating server truth, overusing global state, or putting unrelated update frequencies into the same provider.
  • Good state management feels boring: explicit ownership, predictable transitions, and small re-render blast radius.

Context

Most frontend state debates are really ownership debates in disguise.

On bigger teams, the recurring failures are predictable:

  • remote data copied into client stores for no reason
  • giant context providers that re-render half the app
  • local component state carrying workflow logic that now spans five screens
  • URL-relevant state hidden in memory, which breaks navigation, refresh, and shareability

Before I choose a library, I want to classify the state.

The Approach

1. Split state by responsibility first

This is the model I use:

State typeBest homeExample
Server statefetch/cache layerproducts, profile, inventory
URL staterouter/search paramsfilters, sort, tab, pagination cursor
Local UI statecomponentopen modal, input text, hover
Workflow statereducercheckout step, wizard transitions, bulk edit
Shared client statecontext or storecurrent tenant, feature flags, live collaboration selection

If I get this wrong, the tool choice does not save me.

My first state-management decision is always ownership, not library branding.

2. Start with local state and widen scope only when the pain is real

This progression is still right most of the time:

  1. local component state
  2. lifted state in a common parent
  3. reducer for structured transitions
  4. scoped context for cross-branch access
  5. external store only when the frequency or reach justifies it
type Action =
  | { type: "added"; title: string }
  | { type: "removed"; id: string }

function todosReducer(state: Array<{ id: string; title: string }>, action: Action) {
  switch (action.type) {
    case "added":
      return [...state, { id: crypto.randomUUID(), title: action.title }]
    case "removed":
      return state.filter((todo) => todo.id !== action.id)
    default:
      return state
  }
}

I like reducers when:

  • multiple events can change the same state
  • transitions need to be explicit
  • I want logic that is easy to test outside the UI

3. Do not duplicate server state into client-global state without a reason

This is one of the most common staff-level critiques I make.

If a data-fetching layer already owns:

  • caching
  • revalidation
  • retries
  • stale/fresh semantics

then copying the same data into a second client store often creates:

  • dual source of truth
  • stale reads after mutation
  • more invalidation code
  • harder debugging

I only promote server data into client-global state when the app truly needs a special client-side projection or offline-first workflow that the normal data layer cannot express cleanly.

4. Context is a delivery mechanism, not a free global store

Context is great for low-frequency shared state like:

  • theme
  • auth session summary
  • tenant selection
  • feature flags

It gets noisy when I dump high-churn data into one broad provider.

Bad pattern:

<AppContext.Provider value={{ user, theme, cartCount, filters, setFilters }}>
  {children}
</AppContext.Provider>

Every provider value change creates a new object, and every consumer gets dragged into the blast radius.

Better pattern:

  • split providers by concern
  • memoize provider values when appropriate
  • move high-frequency shared state to a store with selectors if context churn becomes expensive

5. Put URL-worthy state in the URL

If the user expects refresh, back/forward, or sharing to preserve state, I usually put it in route params or search params:

  • search query
  • selected tab
  • filters
  • sort order
  • pagination cursor

That keeps state aligned with navigation and avoids the “why did my page reset when I refreshed?” class of bug.

6. External stores earn their keep when update frequency or reach gets high

I reach for an external store when:

  • state is shared across distant branches
  • update frequency is high
  • I need selectors to prevent broad re-renders
  • workflow coordination crosses many route-local boundaries

Examples:

  • collaborative editing presence state
  • workspace-wide selection
  • advanced dashboard filter orchestration
  • micro-frontend shared client state that should not depend on one subtree staying mounted

The rule is still the same: the store should own the right state, not just more state.

7. Separate workflow state from derived presentation

One of the easiest mistakes is storing too much derived state:

const [items, setItems] = useState<Product[]>([])
const [visibleItems, setVisibleItems] = useState<Product[]>([])

If visibleItems is just items filtered by search and sort, I would rather derive it than synchronize two copies:

const visibleItems = useMemo(() => {
  return items
    .filter((item) => item.name.toLowerCase().includes(search.toLowerCase()))
    .sort(sortProducts)
}, [items, search, sortProducts])

Derived state drift is a silent bug factory.

Trade-offs

  • Local state is easy to reason about, but cross-tree coordination gets messy once many branches need the same workflow.
  • Context reduces prop drilling, but large high-frequency providers become accidental global state with poor render isolation.
  • External stores scale coordination better, but if the ownership model is unclear they only make the confusion more durable.
  • URL state improves shareability and navigation semantics, but it forces more discipline around serialization and route design.

References