Need the #1 custom application developer in Brisbane?Click here →

State Management Explained

10 min read

State is any data your interface needs to track. Is this modal open? What has the user typed in the search box? How many items are in the cart? Is the user logged in? As applications grow, managing state becomes complex—state scattered across components is hard to reason about, easy to corrupt, difficult to debug.

State management is the practice of organizing, storing, and updating state in a predictable way. Good state management makes applications reliable, testable, and maintainable. Poor state management causes bugs, race conditions, and data inconsistency.

Local State: Starting Simple

Local state lives in a single component (React's useState hook, Vue's ref or reactive). This is perfect for component-level concerns: Is this accordion open? What does the user see in this dropdown? Local state requires no special tooling.

React developers often jump to a state management library too early. But local state takes you surprisingly far. If a component's state only affects that component and its children, local state is the right tool. It's simple, performant, and easy to reason about.

Tip
Start with local state. Only graduate to a state management library when you have a concrete problem that local state doesn't solve.

Shared State Problem

The problem emerges when multiple unrelated components need access to the same state. The user is logged in—displayed in a header, in a sidebar, in multiple modals. You could lift state up to a common ancestor, but if the ancestor is the root component, every component re-renders when any state changes. That's slow and wastes renders.

Or you pass props down through every intermediate component (prop drilling), making code verbose and fragile—if you rename a prop, you must update every component in the chain.

State management libraries solve this problem by providing a global store that any component can access directly.

React Context API: Built-In Solution

React's Context API lets you share state without prop drilling. You create a context, provide it at the root, and consume it in any descendant component. No library required.

The downside: Context is designed for low-frequency updates (theme changing, user object). If you have frequently-changing data (form state, list filters), Context re-renders all consumers when any value changes, which can be slow. Context doesn't provide developer tools, persistence, or sophisticated update mechanisms.

For many applications, Context is sufficient. Pair it with useCallback and useMemo to prevent unnecessary re-renders, and you can build applications of significant complexity without a state management library.

Zustand: Lightweight and Practical

Zustand is a minimal state management library that's become popular because it's simple, unopinionated, and effective. You define a store, create hooks to access it, and use those hooks in your components. That's it.

Zustand stores are smaller than Redux stores, require less boilerplate, and are easier to understand. It supports middleware for logging, persistence, and dev tools. For most projects, Zustand is the right level of abstraction—enough structure to prevent chaos, but not so much that it feels heavy.

Zustand is excellent for managing application state (user, theme, global preferences) and moderately complex shared state. It's the state management library I'd reach for first in most projects.

Redux: For Large, Complex Applications

Redux is a comprehensive state management library with a specific philosophy: centralized store, pure reducers, immutable updates, explicit actions. This structure prevents entire classes of bugs and makes time-travel debugging possible.

Redux is powerful but requires discipline. You must follow its conventions. Updates are verbose—changing one field in state requires dispatching an action, writing a reducer, etc. This verbosity ensures all state changes are explicit and traceable, which is valuable for large applications with many developers.

Redux is most justified when you have many developers working on the same codebase and complex state logic that would benefit from being explicit and traceable. For smaller teams or simpler applications, Redux is overkill. The convention and discipline that prevents bugs becomes overhead.

Jotai and Recoil: Atomic State

Jotai and Recoil take a different approach: atomic state. Instead of a single global store, you have many small atoms (pieces of state) that can be composed together. This enables fine-grained reactivity—only components that depend on changed atoms re-render.

Atomic state is elegant and performant, but the mental model is different from traditional state management. It requires thinking in terms of atoms and selectors. For most projects, the benefit doesn't justify the learning curve.

Client State vs Server State

An important distinction: client state (UI state, theme, form drafts) is different from server state (user data, database queries, API responses). Confusing them is a source of bugs.

For server state, use React Query (TanStack Query) or SWR instead of a general state management library. React Query handles API calls, caching, background refetching, and keeping data in sync with the server. It's brilliant at this job.

React Query + Zustand (or Context for simple cases) gives you an excellent combination: React Query for server state, Zustand for client state. They have different concerns and require different approaches.

Warning
Server state is hard. When you manage API responses manually with useState or Zustand, you lose caching, refetching, and invalidation logic. React Query gives you these features for free. Use it for any data fetched from an API.

State Management Decision Framework

ScenarioSolutionWhy
Single component stateLocal useStateSimple, no boilerplate, performant
Multiple components, low-frequency updatesReact ContextBuilt-in, no library needed
Multiple components, frequent updatesZustand or JotaiBetter performance than Context, simple API
Large team, complex state logicReduxExplicit, traceable, excellent for coordination
API dataReact Query or SWRHandles caching, refetching, background updates

Common State Management Mistakes

  • Storing derived state: If you can calculate a value from other state, don't store it separately. Derived state causes bugs when the sources change but derived state doesn't.
  • Mixed client and server state: Putting API responses in Redux alongside client state makes cache invalidation complex. Separate them.
  • Using a library before you need it: If local state works, use local state. Adding Redux to a simple application doesn't prevent bugs—it adds complexity.
  • Global state for everything: Not everything should be global. Button open/closed state should be local. User ID should be global.
  • Ignoring the data flow: Where does state come from? Where does it go? If you can't explain this clearly, your state architecture needs work.

The Honest Truth About State Management

Many developers reach for sophisticated state management when local state + lifting state up + React Context would suffice. The extra complexity introduces new bugs and makes the codebase harder to understand.

The pattern: start with local state, add Context when multiple unrelated components need the same state, add a library like Zustand if Context performance becomes a problem. Most applications never need Redux.

Pair state management with React Query for server state, and you have everything you need for applications of reasonable complexity.