Zustand is the most popular lightweight state management library for React — 47K+ GitHub stars, 4KB bundle, and zero boilerplate. It's the anti-Redux.
Why Zustand Over Redux?
- 4KB vs Redux Toolkit's 35KB+
- No Provider wrapper — just import and use
- No boilerplate — no action types, no reducers, no dispatch
- Works outside React — use in vanilla JS, Node.js, anywhere
- Middleware — persist, devtools, immer, all built in
- TypeScript native — full inference, no extra types
Quick Start
npm install zustand
import { create } from "zustand";
// Create a store in 5 lines
const useCounterStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
// Use in any component — no Provider needed!
function Counter() {
const { count, increment, decrement, reset } = useCounterStore();
return (
<div>
<h1>{count}</h1>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
<button onClick={reset}>Reset</button>
</div>
);
}
Real-World: Todo App
interface Todo {
id: string;
text: string;
completed: boolean;
}
interface TodoStore {
todos: Todo[];
filter: "all" | "active" | "completed";
addTodo: (text: string) => void;
toggleTodo: (id: string) => void;
deleteTodo: (id: string) => void;
setFilter: (filter: TodoStore["filter"]) => void;
filteredTodos: () => Todo[];
}
const useTodoStore = create<TodoStore>((set, get) => ({
todos: [],
filter: "all",
addTodo: (text) =>
set((state) => ({
todos: [...state.todos, { id: crypto.randomUUID(), text, completed: false }],
})),
toggleTodo: (id) =>
set((state) => ({
todos: state.todos.map((t) =>
t.id === id ? { ...t, completed: !t.completed } : t
),
})),
deleteTodo: (id) =>
set((state) => ({
todos: state.todos.filter((t) => t.id !== id),
})),
setFilter: (filter) => set({ filter }),
filteredTodos: () => {
const { todos, filter } = get();
switch (filter) {
case "active": return todos.filter((t) => !t.completed);
case "completed": return todos.filter((t) => t.completed);
default: return todos;
}
},
}));
Async Actions
interface UserStore {
users: User[];
loading: boolean;
error: string | null;
fetchUsers: () => Promise<void>;
createUser: (data: CreateUserInput) => Promise<User>;
}
const useUserStore = create<UserStore>((set) => ({
users: [],
loading: false,
error: null,
fetchUsers: async () => {
set({ loading: true, error: null });
try {
const res = await fetch("/api/users");
const users = await res.json();
set({ users, loading: false });
} catch (error) {
set({ error: (error as Error).message, loading: false });
}
},
createUser: async (data) => {
const res = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
const user = await res.json();
set((state) => ({ users: [...state.users, user] }));
return user;
},
}));
Middleware: Persist
import { create } from "zustand";
import { persist } from "zustand/middleware";
const useSettingsStore = create(
persist(
(set) => ({
theme: "light" as "light" | "dark",
language: "en",
notifications: true,
setTheme: (theme: "light" | "dark") => set({ theme }),
setLanguage: (language: string) => set({ language }),
toggleNotifications: () =>
set((state) => ({ notifications: !state.notifications })),
}),
{
name: "settings-storage", // localStorage key
partialize: (state) => ({
theme: state.theme,
language: state.language,
}), // Only persist these fields
}
)
);
Middleware: Immer (Mutable Syntax)
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
const useStore = create(
immer((set) => ({
users: [] as User[],
addUser: (user: User) =>
set((state) => {
state.users.push(user); // Mutate directly!
}),
updateUser: (id: string, updates: Partial<User>) =>
set((state) => {
const user = state.users.find((u) => u.id === id);
if (user) Object.assign(user, updates);
}),
}))
);
Slices Pattern (Large Stores)
const createUserSlice = (set) => ({
users: [],
fetchUsers: async () => { /* ... */ },
});
const createCartSlice = (set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
total: () => 0,
});
const useStore = create((...a) => ({
...createUserSlice(...a),
...createCartSlice(...a),
}));
Selectors (Prevent Unnecessary Rerenders)
// BAD — rerenders on ANY store change
const { count, users, theme } = useStore();
// GOOD — only rerenders when count changes
const count = useStore((state) => state.count);
// GOOD — shallow compare for objects
import { shallow } from "zustand/shallow";
const { name, email } = useStore(
(state) => ({ name: state.name, email: state.email }),
shallow
);
Zustand vs Redux vs Jotai vs Valtio
| Feature | Zustand | Redux Toolkit | Jotai | Valtio |
|---|---|---|---|---|
| Size | 4KB | 35KB+ | 8KB | 6KB |
| Boilerplate | Minimal | Medium | Minimal | Minimal |
| Provider | No | Yes | Yes | No |
| DevTools | Plugin | Built-in | Plugin | Plugin |
| Persist | Middleware | Manual | Plugin | Plugin |
| Learning curve | Low | Medium | Low | Low |
| Best for | Most apps | Large teams | Atomic state | Proxy-based |
Need to scrape data from any website and get it in structured JSON? Check out my web scraping tools on Apify — no coding required, results in minutes.
Have a custom data extraction project? Email me at spinov001@gmail.com — I build tailored scraping solutions for businesses.
Top comments (0)