Zustand has quickly become one of the most loved state-management libraries for React.
It’s tiny, fast, scalable, and framework-agnostic — and with the new v4+ and v5 releases, it’s more powerful than ever.
In this post, we’ll go from basics to advanced — covering setup, selectors, async actions, middlewares, and best practices for production apps.
🪄 What is Zustand?
“A small, fast, and scalable bear-bones state-management solution.”
It gives you global state with a minimal API — using React hooks, no providers, and no boilerplate.
import { create } from 'zustand';
const useBearStore = create(set => ({
bears: 0,
increase: () => set(state => ({ bears: state.bears + 1 })),
}));
Simple, right? Let’s see how to use it efficiently.
⚙️ Installation & Setup
npm install zustand
Import:
import { create } from 'zustand';
✅ v4+ Change:
Default exports are gone — always use { create }.
Middlewares (optional):
npm install zustand/middleware
🧩 Creating Your First Store
const useBearStore = create(set => ({
bears: 0,
increase: () => set(state => ({ bears: state.bears + 1 })),
removeAll: () => set({ bears: 0 }),
}));
Then in React:
const bears = useBearStore(state => state.bears);
✅ Zustand subscribes only to what you select — super efficient.
🎯 Selectors and Optimization
Bad ❌:
const { bears } = useBearStore(); // subscribes to full store
Good ✅:
const bears = useBearStore(s => s.bears);
Multiple keys? Use shallow:
import { shallow } from 'zustand/shallow';
const { bears, increase } = useBearStore(
s => ({ bears: s.bears, increase: s.increase }),
shallow
);
✅ Only re-renders when bears or increase change.
⚡ Async Actions
Just use async/await inside your store!
const useBearStore = create(set => ({
bears: 0,
loading: false,
fetchBears: async () => {
set({ loading: true });
const data = await fetch('/api/bears').then(r => r.json());
set({ bears: data.length, loading: false });
},
}));
No thunks, no sagas — plain JS.
🧠 Middlewares
Zustand supports composable enhancers like Redux — but with much less boilerplate.
🔹 Devtools — Time Travel & Debugging
Adds Redux DevTools integration so you can inspect, time-travel, and debug store updates visually.
import { devtools } from 'zustand/middleware';
const useStore = create(
devtools(set => ({
bears: 0,
increase: () => set(s => ({ bears: s.bears + 1 })),
}))
);
🔹 Persist — Save & Rehydrate State
Automatically syncs store data to
localStorage,sessionStorage, or custom async storage to survive page reloads.
import { persist } from 'zustand/middleware';
const usePersistedStore = create(
persist(
set => ({
bears: 0,
increase: () => set(s => ({ bears: s.bears + 1 })),
}),
{ name: 'bear-storage' }
)
);
🔹 Immer — Simplify Immutable Updates
Lets you write mutable logic (
state.count++) safely, while Immer handles immutability under the hood.
import { immer } from 'zustand/middleware/immer';
const useStore = create(
immer(set => ({
bears: 0,
increase: () =>
set(state => {
state.bears += 1;
}),
}))
);
✅ You can also compose multiple middlewares:
create(devtools(persist(immer(fn), { name: 'store' })));
📘 Recommended order: immer → persist → devtools
💾 Persistent State — Real-World Example
Here’s a production-ready setup with multiple options:
import { create } from "zustand";
import { devtools, persist, createJSONStorage } from "zustand/middleware";
interface AppState {
user: { id: number; name: string } | null;
theme: "light" | "dark";
notifications: number;
setUser: (user: AppState["user"]) => void;
toggleTheme: () => void;
addNotification: () => void;
clearNotifications: () => void;
}
export const useAppStore = create<AppState>()(
devtools(
persist(
(set) => ({
user: null,
theme: "light",
notifications: 0,
setUser: (user) => set({ user }),
toggleTheme: () => set((s) => ({ theme: s.theme === "light" ? "dark" : "light" })),
addNotification: () => set((s) => ({ notifications: s.notifications + 1 })),
clearNotifications: () => set({ notifications: 0 }),
}),
{
name: "app-storage",
version: 2,
partialize: (s) => ({ theme: s.theme, notifications: s.notifications }),
migrate: (state, version) => (version === 1 ? { ...state, notifications: 0 } : state),
storage: createJSONStorage(() => localStorage),
}
),
{ name: "AppStore" }
)
);
✅ Features:
- 🧭 DevTools ready – Inspect & time-travel easily
- 💾 Versioned migrations – Handle schema changes gracefully
- 🧱 Partial persistence – Save only necessary slices
- ⚙️ Custom storage – Use localStorage, IndexedDB, or async storages
🧱 Vanilla Stores (Non-React)
Zustand works outside React too — perfect for Node or testing.
import { createStore } from "zustand/vanilla";
const bearStore = createStore((set) => ({
bears: 0,
increase: () => set((s) => ({ bears: s.bears + 1 })),
}));
You can use it in React via:
import { useStore } from "zustand";
const useBearStore = (selector) => useStore(bearStore, selector);
✅ Clean separation between UI and logic.
🧩 Best Practices
| ✅ Do | ❌ Don’t |
|---|---|
| Use selectors for each field | Use useStore() without selector |
| Split stores by domain | Put everything in one giant store |
| Persist only what’s needed | Persist tokens or sensitive data |
Use shallow for multi-keys |
Return new objects each render |
| Compose middlewares properly | Mix order randomly |
Write tests via .getState()
|
Mock React unnecessarily |
🧠 Advanced Patterns
Cross-Store Sync
useUserStore.subscribe((s) => {
if (!s.user) useThemeStore.setState({ theme: "light" });
});
Derived State
const useCartStore = create((set, get) => ({
items: [],
get total() {
return get().items.reduce((sum, i) => sum + i.price, 0);
},
}));
Subscriptions
useCartStore.subscribe(
(s) => s.items,
(items) => console.log("Cart changed", items)
);
⚙️ Migrating from Older Versions
| Change | Old | New |
|---|---|---|
| Imports | import create from 'zustand' |
import { create } from 'zustand' |
| TypeScript stores | create<State>(fn) |
create<State>()(fn) |
| Persist API | Basic localStorage only | Supports custom storage, versioning |
| Vanilla API | Unstable | Official createStore()
|
| Devtools | Single param | Supports name, async-safe |
🧭 When to Use Zustand
✅ Ideal for:
- Apps needing shared state without Redux overhead
- React Native or hybrid frameworks
- SSR & non-React logic (via vanilla stores)
- Projects that value simplicity + performance
✨ Key Takeaways
- Zustand is minimal but powerful.
- Uses selectors for performance.
- Supports async, middlewares, persistence, and vanilla mode.
- Fully typed for TypeScript.
- No Providers. No boilerplate. Just React + hooks.
🧡 Final Thought
Zustand hits the perfect sweet spot —
“as simple as
useState, as capable as Redux.”
It’s not just for small apps — it’s the React state library you’ll actually enjoy maintaining.
💬 What’s your favorite Zustand trick or middleware combo?
Drop it in the comments — let’s trade some bear stories 🐻
Top comments (0)