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.
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
| Kind | Examples | Owner | Main problems | Typical tools |
|---|---|---|---|---|
| Local UI state | Input value, hover, open modal | One component | Keeping it close to where it's used | Component state |
| Global client state | Theme, auth session, feature flags, wizard progress | The app | Avoiding unnecessary re-renders, prop drilling | Context, a small store |
| Server state | Feed posts, user profiles, cart, search results | The server | Caching, staleness, refetching, deduplication, pagination, optimistic updates, invalidation | Data-fetching cache libraries (query or GraphQL clients) |
| URL state | Current page, filters, search query, selected tab | The URL | Shareable, bookmarkable, restored on back/forward | The 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.
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
A normalised entity cache keeps like counts consistent across the feed, profile and detail views.
Cart state is server-owned with optimistic updates; filters live in the URL for shareable searches.
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
- Normalizing State ShapeRedux documentation
- TanStack Query: Important defaultsTanStack Query documentation