Hermes Wiki
Developer/FrontendWebMobile/StateManagement/Fundamentals/state-management-local-global-server-state

State Management: Local, Global, and Server State

Concept

"State management" reads like a single problem with a single library answer, but it's actually several distinct categories of state that behave differently and want different tools — treating them as one problem is where most state-management pain comes from. The categories that matter in practice:

  • Local UI state — a toggle, an input value before submit, whether a dropdown is open. Lives and dies with the component; nothing outside needs to see it.
  • Global client state — state genuinely shared across distant, unrelated parts of the component tree: theme, a cart drawer's open/closed state, feature-flag overrides set client-side.
  • Server state — data that actually lives on a backend, can change independently of anything the current user does, and was fetched asynchronously: a user profile, an order history, a product list. This is fundamentally different from client state because it can go stale the moment it's fetched, needs retry/dedupe/cache-invalidation logic, and multiple components may want the same data without re-fetching it separately.
  • URL state — anything that should be shareable or bookmarkable (a search filter, a pagination page, a selected tab) belongs in the URL, not in memory.
  • Form state — input values, validation errors, and submission status while a form is being filled out; typically short-lived and localized to the form itself.

The category-first framing matters because each one has a purpose-built answer: useState/useReducer for local UI state, a lightweight global store (Zustand, Jotai) for genuinely cross-cutting client state, a server-state library (TanStack Query, SWR) for anything fetched from an API, useSearchParams for URL state, and a form library (React Hook Form, paired with a schema validator) for form state. Reaching for one tool to cover all five categories is exactly the pattern that produces bloated global stores and stale-cache bugs.

Tradeoffs

Tool Strengths Weaknesses Good fit for
useState/useReducer (local) Simplest, colocated with the component, zero dependencies Doesn't share across distant components; lifting it up too far reintroduces prop drilling Ephemeral UI state — toggles, unsaved input, open/closed flags
Context API Built into React, no extra dependency Re-renders every consumer on any value change; not built for high-frequency updates Small, infrequently-changing shared state — theme, locale, auth-user object
Zustand / Jotai (global store) Selective subscriptions (a component only re-renders for the slice it reads), small bundle versus Redux + React-Redux Still hand-managed state — another mental model and dependency to reason about Cross-cutting client state genuinely needed app-wide, not data that came from a server
TanStack Query / SWR (server state) Built-in caching, request dedupe, retries, background refetch, stale-time control Overkill for purely local or derived state; introduces its own concepts (query keys, invalidation) that have to be learned Any data fetched from a backend API

The practical pattern most teams converge on: start local by default, promote to a global client-state store only when a genuine cross-tree sharing need appears, and never let server data enter local useState or a global store as its long-term home — a server-state library should own the cache for anything that came from fetch.

When to use / when not to

  • Default to useState for anything a single component (or its direct children via props) can own; only lift state up or move it to a store when a real sharing need appears, not preemptively.
  • Route all data fetched from a backend through a server-state library — never manage it with useState + useEffect, even for "just one simple GET request." The complexity that library hides (race conditions on unmount, deduping identical in-flight requests, cache invalidation) reappears by hand the moment a second component needs the same data.
  • Reach for a global client-state store only for state that's genuinely used across unrelated, non-nested parts of the tree — most state (a commonly cited rule of thumb puts it around 80%) should stay local.
  • Put anything shareable or bookmarkable (filters, search queries, selected tabs, pagination) in the URL, not in component state — otherwise a shared link or a page refresh silently loses it.
  • Don't duplicate server data into a global store "for convenience" — it now has two sources of truth that can drift, and the store's copy won't get the server-state library's automatic revalidation.

Common pitfall

Fetching data with useEffect and storing the response in local useState — this quietly rebuilds a small, incomplete state machine for every single API call: loading/error/success flags managed by hand, no protection against a component unmounting mid-fetch, no deduplication when two components independently request the same data, and no automatic refetch when the underlying data goes stale. None of this is wrong in isolation, but it's exactly what a server-state library already solves, tested and for free — reimplementing it ad hoc per component is where subtle race-condition bugs (setting state on an unmounted component, a slower earlier request overwriting a faster later one) tend to live.

Engineering Lens

The transferable skill here isn't "know which library to pick" — it's correctly categorizing a given piece of state (local, global-client, server, URL, form) before reaching for any tool at all, since the categorization determines the answer almost automatically. This is the same discipline that shows up in rendering-strategy decisions (Fundamentals/rendering-strategies-ssr-csr-ssg-isr in this same topic folder): asking "what kind of thing is this, actually?" before optimizing tends to dissolve problems that look hard when framed as "which library should I use."

Sources

Hermes Wiki