Frontend State Management: Local, Global and Server State

Frontend state management explained: local vs global vs server state, stores, data-fetching caches, normalisation, optimistic updates and when to use each.

Intermediate 3 min read · Updated

Frontend state management is how an application stores and updates the data its UI renders. The most useful distinction is between local UI state (is this dropdown open?), global client state (the current theme, the signed-in user, a multi-step form's progress) and server state (posts, orders, profiles that live on the server and are cached on the client). Each has different problems, and most bugs come from treating server state like local state.

Three kinds of state

KindExamplesOwnerMain problemsTypical tools
Local UI stateInput value, hover, open modalOne componentKeeping it close to where it's usedComponent state
Global client stateTheme, auth session, feature flags, wizard progressThe appAvoiding unnecessary re-renders, prop drillingContext, a small store
Server stateFeed posts, user profiles, cart, search resultsThe serverCaching, staleness, refetching, deduplication, pagination, optimistic updates, invalidationData-fetching cache libraries (query or GraphQL clients)
URL stateCurrent page, filters, search query, selected tabThe URLShareable, bookmarkable, restored on back/forwardThe router

Put state in the URL whenever a user would expect a link to reproduce it, such as search filters and the selected item. It's the most underused state store.

Server state is a cache

Data fetched from an API is a copy of the server's truth that can go stale. Treat it like any cache: it needs keys, freshness rules, invalidation and deduplication. Data-fetching libraries provide:

  • Request deduplication: ten components asking for the same user trigger one request.
  • Stale-while-revalidate: show cached data immediately, refetch in the background, and update when fresh data arrives.
  • Invalidation: after a mutation, mark related queries stale so they refetch (adding a comment invalidates that post's comment list).
  • Pagination and infinite queries: cache pages by cursor. See pagination.
  • Cancellation: abort in-flight requests that are no longer needed, such as superseded search queries.

Normalisation

If the same post appears in the feed, a search result and a detail page, storing three copies means a like updates one and the others show stale counts. A normalised cache stores each entity once by ID, and lists store IDs:

entities: {
  posts: { "p1": { id: "p1", text: "...", likes: 12, author: "u7" } },
  users: { "u7": { id: "u7", name: "Asha" } },
}
feeds: { home: { ids: ["p1", "p4", "p9"], nextCursor: "c_123" } }

Updating posts.p1.likes updates every view that renders post p1. GraphQL clients normalise automatically by type and ID; REST-based caches often keep per-query results and rely on invalidation instead, which is simpler but can show briefly inconsistent data.

Optimistic updates

For actions that usually succeed (likes, sending a message, reordering), update the UI immediately, send the request, and roll back if it fails, with a visible error. Combine with an idempotency key so a retry after a timeout doesn't double-apply, and with a pending state for actions that are slower or riskier, such as payments.

UserUI cacheAPItap likelikes 12 → 13 immediatelyPOST /like (idempotency key)200 OK (or error → rollback to 12)
An optimistic like: the UI updates instantly, the request runs in the background, and a failure rolls the change back.

Global stores: when and how

  • Reach for a global store only for state that's genuinely app-wide and client-owned; most "global state" in older apps was server data that a fetching cache handles better.
  • Subscribe components to the specific slices they need, so unrelated updates don't re-render the whole tree, which hurts INP.
  • Keep state transitions explicit (actions or events), which makes bugs reproducible and enables undo and time-travel debugging.
  • Persist only what should survive reloads, such as drafts or preferences, and version it so old saved shapes don't break new code.

Where it’s used

Social feeds

A normalised entity cache keeps like counts consistent across the feed, profile and detail views.

E-commerce

Cart state is server-owned with optimistic updates; filters live in the URL for shareable searches.

Dashboards

Stale-while-revalidate queries keep charts responsive while background refetches keep them fresh.

In an interview

  • Separate local, global client, server and URL state.
  • Treat server state as a cache with keys, staleness and invalidation.
  • Normalise entities by ID to keep views consistent.
  • Use optimistic updates with rollback and idempotency keys.

Further reading