Skip to content

2026-05-06

Going deep on Zustand

A practical deep dive into Zustand: when it’s a great fit, how to structure stores, and the patterns I’ve found work well in real projects.

Zustand cover (placeholder)

Why I switched from Redux

Redux is a fantastic tool — but in many product teams, it becomes a lot of ceremony for relatively small problems: action types, reducers, selectors, middleware, and the constant mental load of “where does this live?”.

After shipping a few production apps with Zustand, I’ve found it hits a sweet spot:

  • Minimal API surface area
  • Great ergonomics for React components
  • Enough flexibility to scale to “real” apps
  • Easy to test and refactor

This post is a practical walkthrough of the approach I now reach for most often.

The mental model

Zustand gives you a global store that is:

  • Just state + functions (no reducers required)
  • Subscribed per selector (components re-render only when the selected slice changes)
  • Composable (you can split or combine stores when needed)

If you want one sentence: treat your store like a tiny in-memory service.

Most apps don't need Redux. After three projects with Zustand, I'm convinced it hits the sweet spot of simplicity and power.

A small store, done right

Start with a store that reads like an API — not like a dumping ground.

import { create } from "zustand";

export const useBearStore = create((set, get) => ({
  bears: 0,

  addBear: () => set((s) => ({ bears: s.bears + 1 })),
  removeBear: () => set((s) => ({ bears: Math.max(0, s.bears - 1) })),

  reset: () => set({ bears: 0 }),
  getCount: () => get().bears,
}));

Then in components, subscribe to only what you need:

const bears = useBearStore((s) => s.bears);
const addBear = useBearStore((s) => s.addBear);

My go-to patterns

1) Store per domain, not “one mega store”

When your app grows, it’s tempting to throw everything into one store. I prefer one store per domain:

  • useAuthStore
  • useCartStore
  • useUiStore
  • useFiltersStore

This keeps files small and ownership clear.

2) Keep async logic close to the domain

I usually keep loading / error state and the async action in the same store, so components stay simple:

export const useUserStore = create((set) => ({
  user: null,
  loading: false,
  error: null,

  fetchUser: async () => {
    set({ loading: true, error: null });
    try {
      const res = await fetch("/api/me");
      const user = await res.json();
      set({ user, loading: false });
    } catch (e) {
      set({ error: "Failed to load user", loading: false });
    }
  },
}));

3) Derive data with selectors (not computed fields everywhere)

Derived values should generally live in selectors or helper functions, not duplicated in state.

When I don’t use Zustand

Zustand is not the answer to everything. I’ll reach for something else when:

  • I need server-state caching: React Query / SWR
  • I need URL-synced state for navigation: router/searchParams patterns
  • The app already has a strong established state architecture that would be costly to change

Takeaways

  • Zustand shines for client state: UI, preferences, local workflow state, and “app glue”.
  • Keep stores domain-focused and API-shaped (state + actions).
  • Use selectors so components subscribe to minimal slices.

If you want, next we can:

  • Add syntax highlighting (Shiki / rehype-pretty-code)
  • Add a nicer hero image per post (and OG image generation)
  • Add tags + an RSS feed so new posts are discoverable