State management debates have consumed more engineering hours than the state they manage. The truth that settles most arguments: almost all application state is either server state that lives on the backend and is mirrored in the client, or client state that exists only in the user's session. Each kind wants a different tool, and mixing them is where single-page applications quietly fall apart.

Separate Server State from Client State

Server state — the list of orders, the user profile, unread counts — is asynchronous, owned elsewhere, and eventually stale. It belongs in a server-state library like TanStack Query or SWR, which handles caching, refetching, retries, and mutation invalidation for you. Client state — the open drawer, the form draft, the selected filter — is synchronous and local, and belongs in the simplest tool that works. The moment you put server data into a global client store and write your own cache invalidation, you have reimplemented a solved problem, with bugs.

Prefer Local State, Then Colocate

The best state is state you do not manage globally. A component's internal state, a parent's single useState, a URL parameter for a filter — each removes a source of global complexity. Before introducing a store, ask whether the state is actually shared by multiple branches of the component tree. If it is not, keep it local. Lift state only when real sharing demands it, and prefer colocating state with the component that renders it.

Normalised Stores for Complex Client State

When client state genuinely needs global reach — a complex form wizard, a collaborative editor, multi-step checkout — a normalised store helps. Keep entities in a map keyed by id, reference them by id from other structures, and derive displayed views with selectors. Denormalised copies of the same fact in five places guarantee five versions of the truth the moment anything mutates. Write the mutation once; read the derived view everywhere.

Derive What You Can Compute

Filtered lists, totals, counts — anything computable from existing state should be derived, not duplicated. Derived state removes an entire class of synchronisation bugs: there is no way for the derived value to diverge from its source if it is recomputed on every render. Memoise expensive derivations, but never store their output as truth. A stale total is worse than a slow one.

History, Undo, and State Machines

For flows with strict sequences — onboarding, wizards, payments — model the flow explicitly with a state machine. XState makes states and transitions visible, which turns impossible-to-reproduce bugs into documented edges. For undoable interfaces, keep an immutable history stack: past states are pushed, current state is derived, and every action is a pure transformation. The user-visible outcome of these two choices is a product that never lands in a state nobody designed.

A Decision Table for Your Codebase

  • Local, ephemeral UI state? Component state or context.
  • Server data with loading and refetch needs? A server-state library.
  • Shared, synchronous client state? A small normalised store.
  • Values you can compute on the fly? Derived selectors, never stored.
  • Strict sequential flows? A state machine, not ad-hoc booleans.

The pattern is not about which library you choose; it is about giving each kind of state the tool its lifecycle deserves. Smart Logic builds React and Vue single-page applications on Laravel backends where this separation is engineered from the first commit. If your SPA is turning into a tangle of props, stores, and stale caches, our team can map your current state model and refactor it into one that scales with your product.