Skip to content
fearchitect
State & Data

Client State Management

Decide where UI state lives; pick the right tool for the scope.

By Abas TurabliReviewed

Summary

Client state is data that exists only in the browser: UI toggles, form drafts, selection, transient layout. The decision tree is local → lifted → global. useState and useReducer cover most cases; context gates renders to a subtree; external stores (Zustand, Redux Toolkit, Jotai) step in when state fan-out or update frequency outgrows React's own diffing.

Jump to the interview angle

Client state is anything the browser owns and the server does not need to persist. It sits on a spectrum by scope: local (one component, useState/useReducer), lifted (siblings share it via a common ancestor), and global/cross-cutting (auth, theme, cart — lives in a store or context). The key rule: server-fetched data is not client state. Data from an API belongs in a cache layer (React Query, SWR, RTK Query), not in useState — putting it there duplicates the truth and creates sync bugs.

Choosing a client-state tool

ToolBest fitRe-render modelBundle cost
useState / useReducerLocal or lifted stateOwning component onlyZero — built-in
Context + useContextRarely-changing subtree valueEvery consumer on any reference changeZero — built-in
Zustand (create)App-wide UI state, moderate complexitySelector-scoped — only chosen slice~3 kB gzipped
Redux Toolkit (createSlice)Large team, auditable action logSelector-scoped via useSelector~16 kB gzipped
Jotai (atom)Scattered feature-local atomsPer-atom — unused atoms tree-shaken~3 kB gzipped

Scope decision rules

  • Start local: useState owns one value; useReducer owns complex shape or multi-step transitions.
  • Lift before you globalise: two sibling components sharing state go to their nearest common ancestor.
  • Context works for low-frequency shared values (theme, locale); split contexts by update frequency to limit re-renders.
  • A Zustand selector (useStore(s => s.count)) re-renders only when count changes — use it over bare useStore().
  • Jotai atoms are co-located with features and tree-shaken automatically; prefer it when state is scattered across many features.

Zustand store with typed selectors

A minimal cart store using Zustand's create factory. The selector on the last line means the component re-renders only when totalItems changes — unrelated store mutations are skipped.

Zustand cart storets
import { create } from "zustand";

interface CartState {
  items: { id: string; qty: number }[];
  addItem: (id: string) => void;
  totalItems: () => number;
}

export const useCartStore = create<CartState>()((set, get) => ({
  items: [],
  addItem: (id) =>
    set((s) => {
      const existing = s.items.find((i) => i.id === id);
      if (existing) {
        return {
          items: s.items.map((i) =>
            i.id === id ? { ...i, qty: i.qty + 1 } : i,
          ),
        };
      }
      return { items: [...s.items, { id, qty: 1 }] };
    }),
  totalItems: () => get().items.reduce((sum, i) => sum + i.qty, 0),
}));

// In a component — re-renders only when the total quantity changes
const count = useCartStore((s) => s.totalItems());

The selector s => s.totalItems() means this component skips re-renders when unrelated store slices change. No provider needed.

Don't put server data in a client store

Server-fetched data needs background refetch, deduplication, and cache invalidation — none of which useState or a Zustand store provides. Storing API responses in client state creates silent stale-data bugs. Use React Query, SWR, or RTK Query instead. The same rule applies to any value whose source of truth lives on the server.

Interview angle

Interviewers test whether you default to global state or think in scope. Walk through local → lifted → context → store, explain why server state doesn't belong in a client store, and name the re-render problem with context. If asked to compare tools, lead with the selector story.

Soundbite: "Start local, lift when siblings need it, add a store only when context's re-render cost becomes the bottleneck."

Key terms

useState
React hook for a single local state value; re-renders the owning component on change.
useReducer
React hook managing state via a pure `(state, action) => state` function.
Context
React mechanism to share a value to a subtree without prop-drilling; re-renders all consumers on change.
Zustand `create`
Factory that returns a typed store hook; selectors limit which components re-render.
Jotai `atom`
Smallest unit of Jotai state; atoms compose via derived atoms and are tree-shakeable.
createSlice
RTK function that generates action creators and a reducer from a single object.

Further reading

Search fearchitect

Jump to a topic, mode, or action.