Redux needs actions, reducers, selectors, middleware, and 200 lines of boilerplate for a counter. Zustand needs 10 lines. Same power, zero ceremony.
What Zustand Gives You for Free
- Minimal API — create store in 5 lines, use it anywhere
- No boilerplate — no actions, reducers, dispatchers, or context providers
- No Provider wrapper — just import and use
- TypeScript-first — fully typed without extra config
- Middleware — persist, devtools, immer, subscriptions
- 1KB gzipped — vs Redux Toolkit's 30KB+
Quick Start
npm install zustand
Your First Store (10 Lines)
import { create } from 'zustand';
interface CounterStore {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
}
const useCounter = create<CounterStore>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
Using It (No Provider Needed)
// ANY component, ANY file — just import and use
function Counter() {
const count = useCounter((state) => state.count);
const increment = useCounter((state) => state.increment);
return (
<button onClick={increment}>
Count: {count}
</button>
);
}
// No <Provider> wrapper in App.tsx!
// No connect() or useSelector/useDispatch
Real-World Example: Auth Store
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface AuthStore {
user: User | null;
token: string | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
isAuthenticated: () => boolean;
}
const useAuth = create<AuthStore>()(
persist(
(set, get) => ({
user: null,
token: null,
login: async (email, password) => {
const { user, token } = await api.login(email, password);
set({ user, token });
},
logout: () => set({ user: null, token: null }),
isAuthenticated: () => get().token !== null,
}),
{ name: 'auth-storage' } // Persists to localStorage
)
);
Async Actions (No Thunks Needed)
const useTodos = create<TodoStore>((set, get) => ({
todos: [],
loading: false,
fetchTodos: async () => {
set({ loading: true });
const todos = await api.getTodos();
set({ todos, loading: false });
},
addTodo: async (title: string) => {
const todo = await api.createTodo({ title });
set((state) => ({ todos: [...state.todos, todo] }));
},
toggleTodo: (id: string) => {
set((state) => ({
todos: state.todos.map(t =>
t.id === id ? { ...t, done: !t.done } : t
)
}));
},
}));
No createAsyncThunk. No middleware. Just async/await.
Middleware Stack
import { create } from 'zustand';
import { devtools, persist, immer } from 'zustand/middleware';
const useStore = create<Store>()(
devtools( // Redux DevTools support
persist( // localStorage persistence
immer( // Immer for mutable updates
(set) => ({
items: [],
addItem: (item) => set((state) => {
state.items.push(item); // Mutable! (Immer handles it)
}),
})
),
{ name: 'my-store' }
)
)
);
Zustand vs Redux vs Jotai vs Recoil
| Feature | Zustand | Redux Toolkit | Jotai | Recoil |
|---|---|---|---|---|
| Bundle size | 1KB | 30KB | 3KB | 20KB |
| Boilerplate | Minimal | Medium | Minimal | Medium |
| Provider | Not needed | Required | Required | Required |
| Async | Built-in | createAsyncThunk | Built-in | Selectors |
| DevTools | Plugin | Built-in | Plugin | Built-in |
| Learning curve | 5 min | 1 hour | 15 min | 30 min |
| TypeScript | Excellent | Good | Excellent | Good |
The Verdict
Zustand is state management for people who hate state management boilerplate. 1KB, no providers, no reducers, just functions that update state. If Redux feels like too much ceremony, Zustand is the answer.
Need help building production web scrapers or data pipelines? I build custom solutions. Reach out: spinov001@gmail.com
Check out my awesome-web-scraping collection — 400+ tools for extracting web data.
Top comments (0)