· By John Kavanagh

Choosing Where State Lives in React: Local State, Context, URL, Cache, or Server

Abstract image used to represent Choosing State in React: Local, Context, URL, Server
Image by Krakograff Textures.

Many React state problems are really ownership problems.

The bug is not that there is state in the application. The bug is that the state is living in the wrong place, so the code spends its life synchronising values that should never have been split apart in the first place.

That is why state decisions matter so much. Once the ownership is right, the code often gets simpler, the data flow becomes easier to explain, and a surprising number of "complex state management" issues disappear before we reach for another library.


Not All State Deserves the Same Home

In day‑to‑day React work, state usually falls into a few different buckets:

  • local UI state
  • shared client state
  • URL state
  • cached server state
  • server‑owned request or page state

The mistake is assuming one tool should own all of them.

React gives us local state and context. The browser gives us the URL. Data libraries give us caches. Frameworks and server layers give us server‑owned rendering state. The job is to choose the smallest honest owner for the information we are dealing with.


Local State is the Default for a Reason

If one component owns the behaviour and no other part of the application needs to coordinate with it, local state is usually right.

Examples:

  • whether a menu is open
  • which row is hovered
  • the text in a controlled draft field before save
  • whether a panel is expanded

A controlled input can keep its draft value in component state. An uncontrolled input keeps its current value in the DOM instead. Both can be useful; the distinction is who holds the value. Local component state stays easy to follow when one component owns it, and becomes more awkward when several parts of the application need to coordinate updates.


Context Helps When Many Nearby Consumers Need the Same Client State

Context is useful when a group of components needs access to the same state and prop drilling has become noisy.

Good context candidates include:

  • theme
  • authenticated user session summary
  • feature flags already resolved elsewhere
  • wizard step state for one feature area

Poor context candidates include everything the team was too tired to place properly.

Context passes a value from a provider to its consumers; it does not store state by itself. The provider might pass state held in a component, or a value supplied by another source. If that value changes frequently, every consumer reading the context can need to render again. Keep the provided value focused on what those components actually share.


The URL Should Own State That Needs to Survive Navigation

If the user expects refresh, share, or back‑button behaviour to preserve the state, the URL is usually the right owner.

That often includes:

  • search queries
  • selected filters
  • sorting
  • pagination
  • tab selection when it matters to navigation

This is a strong example because teams often keep those values in component state first and only later try to mirror them into the URL. That creates two sources of truth for one concept.

A better pattern is to let the URL own the navigational state and derive the UI from it.

If the state should be shareable, it probably deserves a URL representation.


Server Data is Not the Same Thing as Client State

This one causes a lot of confusion.

A list of articles fetched from an API is not "just React state" in the same sense as isMenuOpen. It is server‑owned data. The client may cache it, render it, filter it, or invalidate it, but the underlying authority sits elsewhere.

That difference matters because the questions are different:

  • how fresh does this data need to be
  • who can invalidate it
  • does stale data matter
  • can several components consume the same cached result

Once we name it as server data rather than general state, the right tool often becomes clearer. A request cache, framework loader, or server component boundary is usually a better home than a hand‑rolled context full of fetch status and duplicated entities.


A Practical Example Makes the Split Easier to See

Suppose we are building an article listing page.

The page has:

  • a search box
  • category filters
  • pagination
  • a results list from the server
  • a "filters open" toggle on mobile

Those values do not all belong in the same place.

One reasonable split would be:

  • isFiltersOpen: local component state
  • search, category, and page: URL state
  • fetched results: server‑owned data cached by the framework or data layer
  • selected row hover or transient highlight: local state

If we instead store everything in a giant client context, we end up re‑implementing navigation semantics and server data ownership by hand.


Beware Mirrored State

Mirrored state is where the pain usually begins.

That looks like:

  • keeping the current page in local state and the URL
  • storing fetched data in context and in a query cache
  • copying server props into local state "just in case"

Mirroring feels safe because there is always another copy available. In practice it creates drift.

If two places claim to own the same value, we now need sync logic. Sync logic means more effects, more edge cases, and more uncertainty about which value is actually authoritative after a navigation, refresh, or async update.

The stronger pattern is to pick one owner and derive the rest.


Ask a Few Blunt Questions

When deciding where state should live, I usually ask:

  1. Who actually owns this information?
  2. Who needs to read it?
  3. Should it survive refresh, share, or back‑button navigation?
  4. Is it authoritative server data or local interaction state?
  5. Would two copies of this value create drift?

Those questions are often enough to kill the wrong design early.


State Libraries Do Not Remove This Decision

Even if a project uses Zustand, Redux, Jotai, or another state library, the ownership question still exists.

The library may change the mechanics of how state is stored and subscribed to, but it does not change whether the value should really be local, URL‑based, cached server data, or request‑owned server state.

That is why global state tools sometimes disappoint teams. They can make distribution easier, but they cannot rescue a bad ownership decision.


The Simplest Honest Owner Usually Wins

This is the rule I trust most:

  • if it is local interaction state, keep it local
  • if it is shared nearby client state, consider context
  • if it should survive navigation, let the URL own it
  • if it comes from the server, treat it as server data rather than ordinary client state

That does not solve every edge case automatically, but it solves many of the structural mistakes that make React applications feel more complicated than they needed to be.


Useful References

Most state problems get cheaper once ownership becomes explicit. The application stops trying to synchronise every value everywhere, and the flow starts reflecting what the data actually is.

That is usually the real improvement. Better state management often begins with less state management theatre and clearer answers about where a value truly belongs.


Need a senior engineer involved?

I can work directly in the codebase, review the architecture, or support the team through delivery when the work needs more than extra hands.