Billy.Hire me
All posts
12 Aug 2026ReactState ManagementEngineering

Zustand as the Main Store — State Management Without the Ceremony

Why Zustand keeps winning the state-management debate, how to structure a store for real apps, and the pitfalls that show up after the honeymoon.

The pitch, in one paragraph

Zustand is a tiny React store with a deceptive amount of power. No context providers, no boilerplate, no ceremony — you create a store with a plain function, read state with a hook, and mutate it with actions that live right next to the state they change. It stays out of your way until you need it, and then it scales surprisingly far.

import { create } from "zustand";

interface Store {
  count: number;
  inc: () => void;
  dec: () => void;
}

export const useCounter = create<Store>((set) => ({
  count: 0,
  inc: () => set((s) => ({ count: s.count + 1 })),
  dec: () => set((s) => ({ count: s.count - 1 })),
}));

That is the entire API surface most apps ever need.

Why it wins the debate

I have built with Redux, Context, Recoil, Jotai, and MobX across production apps. Zustand is not always the "best" at any single thing — it is the best at not taxing you while you build.

  • No providers. A store is a module, not a tree. You can use it anywhere, including outside React, which makes it trivial for utilities and workers.
  • Selectors prevent re-renders by default. You read a slice, you re-render on that slice:
const count = useCounter((s) => s.count);

Sloppy selector usage is the one real footgun — more on that below.

  • Actions co-located with state. No action types, no reducers, no action creators, no selectors folder. The store is one cohesive unit you can read top to bottom.
  • No framework lock-in. The store is framework-agnostic; the React binding is an adapter. Migrate an app to another renderer without rewriting state.

Structuring a store for a real application

The common failure is one giant global store. Zustand gives you no structure — which is a feature until it becomes a tax. The pattern that holds up:

  • One store per domain. Auth, cart, notifications, settings. Not one store for everything, not one store per component.
  • Actions, not setters. Expose submitOrder() and setQuantity(id, n), never a raw set. It keeps the store honest and the component dumb.
  • Keep derived data out of the store. Compute it in selectors or via useShallow, so changing one slice does not invalidate unrelated state.
import { useShallow } from "zustand/react/shallow";

const { items, total } = useCart(
  useShallow((s) => ({
    items: s.items,
    total: s.items.reduce((sum, i) => sum + i.price * i.qty, 0),
  })),
);

useShallow is the detail most tutorials skip and most production bugs hide behind. Without it, a selector returning a fresh object re-renders on every store change.

The footguns

Three things will bite you, in this order:

  1. Unstable selector returns. Returning { ... } or a new array from a selector triggers infinite or constant re-renders. Use useShallow, or select primitives.
  2. Slices that cross-import. When splitting a store with the slices pattern, two slices importing each other is a circular-dependency trap. Keep one slice as the owner of shared state, or use actions that live above the slices.
  3. Server state pretending to be client state. Zustand is for client state. The moment you hand it fetched data without sync logic, you get staleness and cache bugs for free. Pair it with a server cache, or hydrate and invalidate explicitly.

Where it sits in an architecture

The way I reach for it in practice:

  • Client-only state — UI, forms, cart, feature flags → Zustand.
  • Server state — fetched and cached data → a server cache layer, kept out of Zustand.
  • Complex derived state — cross-domain computations → selectors on top of Zustand, never duplicated across components.

Zustand solves the hard 80% with almost no API. The remaining 20% is the discipline you bring — and that is true of every state library.

The verdict

Zustand as the main store is a defensible default. It is small enough to replace, fast enough to keep, and explicit enough to reason about. In a codebase where every other choice — fetch strategy, routing, styling — is already opinionated, a store that demands nothing from you is the one piece of state management you will stop thinking about entirely.

And that is exactly the point.