Zustand: Lightweight React State Management Made Simple
Master Zustand for React state management. Learn store creation, async actions, middleware, persistence, and TypeScript integration patterns.
Moshiour Rahman
Advertisement
What is Zustand?
Zustand is a small, fast, and scalable state management library for React. It offers a simpler API than Redux while providing powerful features like middleware and devtools support.
Why Zustand?
| Feature | Zustand | Redux | Context |
|---|---|---|---|
| Bundle Size | ~1KB | ~10KB | Built-in |
| Boilerplate | Minimal | High | Low |
| Learning Curve | Easy | Steep | Easy |
| Performance | Excellent | Good | Can be slow |
| DevTools | Yes | Yes | Limited |
Getting Started
Installation
npm install zustand
Basic Store
import { create } from 'zustand';
// Define store
interface CounterStore {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
}
const useCounterStore = create<CounterStore>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 })
}));
// Use in component
function Counter() {
const { count, increment, decrement, reset } = useCounterStore();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
);
}
Selecting State
// Select single value (component only re-renders when count changes)
function CountDisplay() {
const count = useCounterStore((state) => state.count);
return <p>Count: {count}</p>;
}
// Select multiple values
function Controls() {
const { increment, decrement } = useCounterStore((state) => ({
increment: state.increment,
decrement: state.decrement
}));
return (
<>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</>
);
}
// Shallow comparison for objects
import { shallow } from 'zustand/shallow';
function UserInfo() {
const { name, email } = useUserStore(
(state) => ({ name: state.name, email: state.email }),
shallow
);
return <p>{name} - {email}</p>;
}
Real-World Store Patterns
Todo Store
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: 'all' | 'active' | 'completed') => void;
clearCompleted: () => void;
}
const useTodoStore = create<TodoStore>((set) => ({
todos: [],
filter: 'all',
addTodo: (text) => set((state) => ({
todos: [
...state.todos,
{ id: crypto.randomUUID(), 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 }),
clearCompleted: () => set((state) => ({
todos: state.todos.filter((todo) => !todo.completed)
}))
}));
// Derived state with selector
const useFilteredTodos = () => useTodoStore((state) => {
switch (state.filter) {
case 'active':
return state.todos.filter((t) => !t.completed);
case 'completed':
return state.todos.filter((t) => t.completed);
default:
return state.todos;
}
});
Auth Store
interface User {
id: string;
name: string;
email: string;
}
interface AuthStore {
user: User | null;
token: string | null;
isLoading: boolean;
error: string | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
checkAuth: () => Promise<void>;
}
const useAuthStore = create<AuthStore>((set, get) => ({
user: null,
token: null,
isLoading: false,
error: null,
login: async (email, password) => {
set({ isLoading: true, error: null });
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (!response.ok) {
throw new Error('Invalid credentials');
}
const { user, token } = await response.json();
set({ user, token, isLoading: false });
localStorage.setItem('token', token);
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Login failed',
isLoading: false
});
}
},
logout: () => {
localStorage.removeItem('token');
set({ user: null, token: null });
},
checkAuth: async () => {
const token = localStorage.getItem('token');
if (!token) return;
set({ isLoading: true });
try {
const response = await fetch('/api/me', {
headers: { Authorization: `Bearer ${token}` }
});
if (response.ok) {
const user = await response.json();
set({ user, token, isLoading: false });
} else {
localStorage.removeItem('token');
set({ isLoading: false });
}
} catch {
set({ isLoading: false });
}
}
}));
Shopping Cart Store
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface CartStore {
items: CartItem[];
addItem: (product: Omit<CartItem, 'quantity'>) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
clearCart: () => void;
totalItems: () => number;
totalPrice: () => number;
}
const useCartStore = create<CartStore>((set, get) => ({
items: [],
addItem: (product) => set((state) => {
const existing = state.items.find((item) => item.id === product.id);
if (existing) {
return {
items: state.items.map((item) =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
)
};
}
return { items: [...state.items, { ...product, quantity: 1 }] };
}),
removeItem: (id) => set((state) => ({
items: state.items.filter((item) => item.id !== id)
})),
updateQuantity: (id, quantity) => set((state) => ({
items: quantity <= 0
? state.items.filter((item) => item.id !== id)
: state.items.map((item) =>
item.id === id ? { ...item, quantity } : item
)
})),
clearCart: () => set({ items: [] }),
// Computed values using get()
totalItems: () => get().items.reduce((sum, item) => sum + item.quantity, 0),
totalPrice: () => get().items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
)
}));
Async Actions
Fetching Data
interface Post {
id: number;
title: string;
body: string;
}
interface PostStore {
posts: Post[];
isLoading: boolean;
error: string | null;
fetchPosts: () => Promise<void>;
createPost: (post: Omit<Post, 'id'>) => Promise<void>;
}
const usePostStore = create<PostStore>((set, get) => ({
posts: [],
isLoading: false,
error: null,
fetchPosts: async () => {
set({ isLoading: true, error: null });
try {
const response = await fetch('/api/posts');
const posts = await response.json();
set({ posts, isLoading: false });
} catch (error) {
set({
error: 'Failed to fetch posts',
isLoading: false
});
}
},
createPost: async (post) => {
set({ isLoading: true });
try {
const response = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(post)
});
const newPost = await response.json();
set((state) => ({
posts: [...state.posts, newPost],
isLoading: false
}));
} catch (error) {
set({ error: 'Failed to create post', isLoading: false });
}
}
}));
Middleware
Persist Middleware
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
interface SettingsStore {
theme: 'light' | 'dark';
language: string;
notifications: boolean;
setTheme: (theme: 'light' | 'dark') => void;
setLanguage: (language: string) => void;
toggleNotifications: () => void;
}
const useSettingsStore = create<SettingsStore>()(
persist(
(set) => ({
theme: 'light',
language: 'en',
notifications: true,
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language }),
toggleNotifications: () => set((state) => ({
notifications: !state.notifications
}))
}),
{
name: 'settings-storage',
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
theme: state.theme,
language: state.language
}) // Only persist these fields
}
)
);
DevTools Middleware
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
const useStore = create<Store>()(
devtools(
(set) => ({
count: 0,
increment: () => set(
(state) => ({ count: state.count + 1 }),
false,
'increment' // Action name for DevTools
)
}),
{ name: 'MyStore' }
)
);
Immer Middleware
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
interface Store {
users: { id: string; name: string; settings: { theme: string } }[];
updateUserTheme: (userId: string, theme: string) => void;
}
const useStore = create<Store>()(
immer((set) => ({
users: [],
updateUserTheme: (userId, theme) => set((state) => {
const user = state.users.find((u) => u.id === userId);
if (user) {
user.settings.theme = theme; // Direct mutation with Immer
}
})
}))
);
Combining Middleware
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
const useStore = create<Store>()(
devtools(
persist(
immer((set) => ({
// Store definition
})),
{ name: 'my-storage' }
),
{ name: 'MyStore' }
)
);
Slices Pattern
Splitting Large Stores
// userSlice.ts
export interface UserSlice {
user: User | null;
setUser: (user: User) => void;
clearUser: () => void;
}
export const createUserSlice = (set: any): UserSlice => ({
user: null,
setUser: (user) => set({ user }),
clearUser: () => set({ user: null })
});
// cartSlice.ts
export interface CartSlice {
items: CartItem[];
addItem: (item: CartItem) => void;
clearCart: () => void;
}
export const createCartSlice = (set: any): CartSlice => ({
items: [],
addItem: (item) => set((state: any) => ({
items: [...state.items, item]
})),
clearCart: () => set({ items: [] })
});
// store.ts
import { create } from 'zustand';
import { createUserSlice, UserSlice } from './userSlice';
import { createCartSlice, CartSlice } from './cartSlice';
type Store = UserSlice & CartSlice;
const useStore = create<Store>()((...args) => ({
...createUserSlice(...args),
...createCartSlice(...args)
}));
export default useStore;
Testing
Testing Stores
import { act, renderHook } from '@testing-library/react';
import { useCounterStore } from './counterStore';
describe('Counter Store', () => {
beforeEach(() => {
// Reset store before each test
useCounterStore.setState({ count: 0 });
});
it('should increment count', () => {
const { result } = renderHook(() => useCounterStore());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('should decrement count', () => {
useCounterStore.setState({ count: 5 });
const { result } = renderHook(() => useCounterStore());
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(4);
});
});
Mocking Stores
// __mocks__/store.ts
import { create } from 'zustand';
const mockStore = create(() => ({
user: { id: '1', name: 'Test User' },
isLoading: false,
login: jest.fn(),
logout: jest.fn()
}));
export default mockStore;
// In test file
jest.mock('./store');
Best Practices
1. Keep Stores Focused
// Good: Separate concerns
const useAuthStore = create<AuthStore>(...);
const useCartStore = create<CartStore>(...);
const useUIStore = create<UIStore>(...);
// Avoid: One giant store
const useStore = create<EverythingStore>(...);
2. Use Selectors
// Good: Only re-renders when count changes
const count = useStore((state) => state.count);
// Avoid: Re-renders on any state change
const state = useStore();
3. Memoize Selectors for Objects
import { shallow } from 'zustand/shallow';
// Good: Shallow comparison prevents unnecessary re-renders
const { name, email } = useStore(
(state) => ({ name: state.name, email: state.email }),
shallow
);
Summary
| Feature | Usage |
|---|---|
create | Create store |
set | Update state |
get | Access state in actions |
persist | Save to storage |
devtools | Redux DevTools |
immer | Immutable updates |
shallow | Prevent re-renders |
Zustand provides a simple yet powerful solution for React state management with minimal boilerplate.
Advertisement
Moshiour Rahman
Software Architect & AI Engineer
Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.
Related Articles
React Hooks Complete Guide: useState to Custom Hooks
Master all React hooks from basics to advanced. Learn useState, useEffect, useContext, useReducer, useMemo, useCallback, and create custom hooks.
JavaScriptVite + React: Modern Frontend Development Setup
Master Vite for React development. Learn project setup, configuration, plugins, environment variables, and production optimization.
JavaScriptNext.js 14 Tutorial: Complete Guide with App Router
Master Next.js 14 with App Router. Learn server components, data fetching, routing, server actions, and build full-stack React applications.
Comments
Comments are powered by GitHub Discussions.
Configure Giscus at giscus.app to enable comments.