
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:
useAuthStoreuseCartStoreuseUiStoreuseFiltersStore
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