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
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.
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.