React State Management

Architect scalable frontend applications by selecting between local state, server caches, and global client stores.

TL;DR

  1. Colocate component state strictly where it is rendered.
  2. Delegate remote data fetching to dedicated server cache libraries.
  3. Manage shared global client state with lightweight Zustand stores.

State Architecture Tiers

    Local Component State

    Store UI toggles and form inputs locally.

    const [isOpen, setIsOpen] = useState(false);
    const toggle = () => setIsOpen((o) => !o);
    Lifted Sibling State

    Lift shared state up to closest common parent.

    export function SearchShell() {
      const [query, setQuery] = useState('');
      return (
        <CardView>
          <InputBase value={query} onChange={setQuery} />
          <ResultsList query={query} />
        </CardView>
      );
    }
    Server State Cache

    Query and cache remote endpoint data cleanly.

    const { data, isPending } = useQuery({
      queryKey: ['user', id],
      queryFn: () => fetchUser(id),
    });

Global Stores with Zustand

    Define Zustand Store

    Create lightweight store outside React hierarchy.

    import { create } from 'zustand';
    
    interface CartStore {
      items: string[];
      add: (item: string) => void;
    }
    
    export const useCartStore =
      create<CartStore>((set) => ({
        items: [],
        add: (it) =>
          set((s) => ({ items: [...s.items, it] })),
      }));
    Atomic Selector

    Subscribe only to specific slice to prevent renders.

    export function CartBadge() {
      const count = useCartStore((s) => s.items.length);
      return <Badge text={String(count)} />;
    }
    Action Dispatch Without State

    Extract action functions without subscribing to state.

    export function AddButton({ id }: { id: string }) {
      const add = useCartStore((s) => s.add);
      return <Button onClick={() => add(id)}>Add</Button>;
    }

External Store Subscriptions

    useSyncExternalStore Hook

    Subscribe safely to external store with React 18+.

    const state = useSyncExternalStore(
      store.subscribe,
      store.getSnapshot
    );
    Server Snapshot Hydration

    Provide server snapshot fallback for SSR hydration.

    const state = useSyncExternalStore(
      store.subscribe,
      store.getSnapshot,
      store.getServerSnapshot
    );
    URL Query Parameter State

    Synchronize view filters with browser query parameters.

    const [searchParams, setSearchParams] =
      useSearchParams();

State Management Pitfalls

    Premature Globalization

    Putting local modal flags into global stores.

    // BAD: Global state for single modal
    useGlobalStore((s) => s.loginModalOpen);
    Duplicate Server Caches

    Copying fetch responses into local useState buffers.

    // BAD: Sync nightmare
    const [items, setItems] = useState(data);
    Direct State Mutation

    Mutating arrays directly rather than returning copy.

    // BAD: direct push
    state.items.push(newItem);

Tips

  1. Separate server state from client state by using TanStack Query or SWR useQuery for remote API endpoints and queries.
  2. Use atomic state selectors when consuming global zustand stores to ensure components only re-render when their slice changes.

Warnings

  1. Do not copy asynchronous server cache responses into local useState variables where synchronization bugs will quickly emerge.
  2. Avoid reaching for heavy global state stores when simple parent-child component composition solves prop forwarding naturally.

In Practice

FAQ