Zustand
A small, fast, and scalable state management solution with minimal boilerplate
Zustand - Bear Necessities for State
Zustand (German for "state") is a small, fast, and scalable state management solution. It has a simple API based on hooks, doesn't require providers, and works with React's concurrent mode. Zustand is perfect for those who want the power of Redux without the boilerplate.
Why Zustand?
- Minimal — Tiny bundle size (~1KB)
- No Providers — No wrapper components needed
- Hooks-based — Simple hook API
- TypeScript — First-class TypeScript support
- Flexible — Works outside React too
Installation
npm install zustand
Basic Store
import { create } from 'zustand';
// Create a store with state and actions
const useStore = create((set) => ({
// State
count: 0,
// Actions
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
setCount: (value) => set({ count: value }),
}));
// Use in components - no Provider needed!
function Counter() {
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
const decrement = useStore((state) => state.decrement);
return (
<div>
<span>{count}</span>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
// Or destructure multiple values (causes re-render when any changes)
function CounterAlt() {
const { count, increment, decrement } = useStore();
return (/* same JSX */);
}
Todo App Example
import { create } from 'zustand';
const useTodoStore = create((set, get) => ({
todos: [],
filter: 'all', // 'all' | 'active' | 'completed'
// Actions
addTodo: (text) => set((state) => ({
todos: [...state.todos, {
id: Date.now(),
text,
completed: false,
}],
})),
toggleTodo: (id) => set((state) => ({
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
),
})),
deleteTodo: (id) => set((state) => ({
todos: state.todos.filter(todo => todo.id !== id),
})),
setFilter: (filter) => set({ filter }),
// Computed values using get()
getFilteredTodos: () => {
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;
}
},
clearCompleted: () => set((state) => ({
todos: state.todos.filter(todo => !todo.completed),
})),
}));
// Components
function TodoList() {
const todos = useTodoStore((state) => state.getFilteredTodos());
const toggleTodo = useTodoStore((state) => state.toggleTodo);
const deleteTodo = useTodoStore((state) => state.deleteTodo);
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
);
}
Async Actions
const useUserStore = create((set, get) => ({
users: [],
loading: false,
error: null,
fetchUsers: async () => {
set({ loading: true, error: null });
try {
const response = await fetch('/api/users');
const users = await response.json();
set({ users, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
fetchUserById: async (id) => {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
// Merge with existing users
set((state) => ({
users: state.users.some(u => u.id === id)
? state.users.map(u => u.id === id ? user : u)
: [...state.users, user]
}));
return user;
},
}));
// Usage
function UsersList() {
const { users, loading, error, fetchUsers } = useUserStore();
useEffect(() => {
fetchUsers();
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
Middleware - Persist, DevTools, Immer
import { create } from 'zustand';
import { persist, devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
// Persist to localStorage
const useStore = create(
persist(
(set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
}),
{
name: 'app-settings', // localStorage key
}
)
);
// DevTools integration
const useDebugStore = create(
devtools(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }), false, 'increment'),
}),
{ name: 'MyStore' }
)
);
// Immer for immutable updates
const useTodoStore = create(
immer((set) => ({
todos: [],
addTodo: (text) => set((state) => {
// Can "mutate" with Immer!
state.todos.push({ id: Date.now(), text, completed: false });
}),
toggleTodo: (id) => set((state) => {
const todo = state.todos.find(t => t.id === id);
if (todo) todo.completed = !todo.completed;
}),
}))
);
// Combine multiple middleware
const useAdvancedStore = create(
devtools(
persist(
immer((set) => ({
// ... state and actions
})),
{ name: 'advanced-store' }
)
)
);
TypeScript Support
import { create } from 'zustand';
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface TodoState {
todos: Todo[];
addTodo: (text: string) => void;
toggleTodo: (id: number) => void;
deleteTodo: (id: number) => void;
}
const useTodoStore = create<TodoState>()((set) => ({
todos: [],
addTodo: (text) => set((state) => ({
todos: [...state.todos, { id: Date.now(), text, completed: false }],
})),
toggleTodo: (id) => set((state) => ({
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
),
})),
deleteTodo: (id) => set((state) => ({
todos: state.todos.filter(todo => todo.id !== id),
})),
}));
Zustand vs Redux
| Feature | Zustand | Redux |
|---|---|---|
| Bundle size | ~1KB | ~10KB+ |
| Provider | Not needed | Required |
| Boilerplate | Minimal | More verbose |
| Learning curve | Low | Higher |
| DevTools | Via middleware | Built-in |
💡 Best Practices
- • Use selector functions to prevent unnecessary re-renders
- • Split stores by domain/feature for large apps
- • Use persist middleware for data that should survive refreshes
- • Enable devtools in development for debugging
- • Use immer middleware for complex nested state updates