Zustand
Simple and fast state management for React
🐻 What is Zustand?
Zustand is a small, fast state management solution for React. It uses hooks and requires minimal boilerplate. Unlike Redux, Zustand is simpler to set up and doesn't need providers or complex configuration.
import create from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }))
}));
Result:
A simple store with state and actions in just a few lines!
Zustand Key Features
Simple API
Minimal boilerplate code
const useStore = create((set) => ({
count: 0
}));
Hook-Based
Use stores with React hooks
const count = useStore(
(state) => state.count
);
No Providers
No context providers needed
// Just use the hook anywhere
const data = useStore();
Tiny Size
Only ~1KB gzipped
npm install zustand
🔹 Creating Your First Store
Create a Zustand store with the create function. Define your state and actions in a single object for simple, organized state management.
# Install Zustand
npm install zustand
// store.js
import create from 'zustand';
const useStore = create((set) => ({
// State
count: 0,
user: null,
// Actions
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
setUser: (user) => set({ user })
}));
export default useStore;
// Counter.jsx
import React from 'react';
import useStore from './store';
function Counter() {
// Select specific state
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
const decrement = useStore((state) => state.decrement);
const reset = useStore((state) => state.reset);
return (
<div>
<h2>Count: {count}</h2>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
);
}
Output:
Counter works without any providers or complex setup!
🔹 Selecting State Efficiently
Select only the state you need to prevent unnecessary re-renders. Zustand automatically optimizes updates when you select specific state slices.
import React from 'react';
import useStore from './store';
function UserProfile() {
// ✅ Good: Select only what you need
const user = useStore((state) => state.user);
// ❌ Bad: Selects entire store (re-renders on any change)
// const { user } = useStore();
return (
<div>
{user ? (
<p>Welcome, {user.name}!</p>
) : (
<p>Please log in</p>
)}
</div>
);
}
function CountDisplay() {
// Only re-renders when count changes
const count = useStore((state) => state.count);
console.log('CountDisplay rendered');
return <div>Count: {count}</div>;
}
function UserDisplay() {
// Only re-renders when user changes
const user = useStore((state) => state.user);
console.log('UserDisplay rendered');
return <div>User: {user?.name || 'None'}</div>;
}
Output:
Components only re-render when their selected state changes.
🔹 Async Actions
Handle async operations like API calls directly in your store actions. Zustand makes async state management straightforward without extra middleware.
// store.js
import create from 'zustand';
const useStore = create((set) => ({
users: [],
loading: false,
error: null,
// Async action to fetch users
fetchUsers: async () => {
set({ loading: true, error: null });
try {
const response = await fetch('/api/users');
const data = await response.json();
set({ users: data, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
// Async action to add user
addUser: async (userData) => {
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
const newUser = await response.json();
set((state) => ({
users: [...state.users, newUser]
}));
} catch (error) {
set({ error: error.message });
}
}
}));
export default useStore;
// UserList.jsx
import React, { useEffect } from 'react';
import useStore from './store';
function UserList() {
const users = useStore((state) => state.users);
const loading = useStore((state) => state.loading);
const error = useStore((state) => state.error);
const fetchUsers = useStore((state) => state.fetchUsers);
useEffect(() => {
fetchUsers();
}, [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>
);
}
Output:
Shows loading state, then displays users or error message.
🔹 Multiple Stores
Create separate stores for different features to keep your code organized. Each store is independent and can be used anywhere in your app.
// authStore.js
import create from 'zustand';
export const useAuthStore = create((set) => ({
user: null,
isAuthenticated: false,
login: (user) => set({ user, isAuthenticated: true }),
logout: () => set({ user: null, isAuthenticated: false })
}));
// todoStore.js
import create from 'zustand';
export const useTodoStore = create((set) => ({
todos: [],
addTodo: (text) => set((state) => ({
todos: [...state.todos, { id: Date.now(), text, done: false }]
})),
toggleTodo: (id) => set((state) => ({
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
})),
deleteTodo: (id) => set((state) => ({
todos: state.todos.filter(todo => todo.id !== id)
}))
}));
// App.jsx
import React from 'react';
import { useAuthStore } from './authStore';
import { useTodoStore } from './todoStore';
function App() {
const user = useAuthStore((state) => state.user);
const todos = useTodoStore((state) => state.todos);
return (
<div>
<h1>Welcome {user?.name || 'Guest'}</h1>
<p>You have {todos.length} todos</p>
</div>
);
}
Output:
Multiple independent stores work together seamlessly.
🔹 Persisting State
Persist store data to localStorage automatically with Zustand's persist middleware. State survives page refreshes without extra code.
// store.js
import create from 'zustand';
import { persist } from 'zustand/middleware';
const useStore = create(
persist(
(set) => ({
count: 0,
user: null,
increment: () => set((state) => ({ count: state.count + 1 })),
setUser: (user) => set({ user })
}),
{
name: 'my-app-storage', // localStorage key
// Optional: customize what to persist
partialize: (state) => ({
count: state.count,
user: state.user
})
}
)
);
export default useStore;
// Counter.jsx
import React from 'react';
import useStore from './store';
function Counter() {
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
return (
<div>
<h2>Count: {count}</h2>
<button onClick={increment}>Increment</button>
<p>Refresh the page - count persists!</p>
</div>
);
}
Output:
Count value persists across page refreshes automatically!
🔹 Zustand vs Redux
Zustand and Redux both manage state, but Zustand is simpler and requires less code. Choose based on your project needs.
Zustand Advantages:
- ✅ Much less boilerplate code
- ✅ No providers needed
- ✅ Smaller bundle size (~1KB)
- ✅ Simpler learning curve
- ✅ Built-in async support
Redux Advantages:
- ✅ Larger ecosystem and community
- ✅ More middleware options
- ✅ Better DevTools integration
- ✅ Established patterns for large apps
// Zustand - Simple and concise
const useStore = create((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 }))
}));
// Redux Toolkit - More structure
const counterSlice = createSlice({
name: 'counter',
initialState: { count: 0 },
reducers: {
increment: (state) => { state.count += 1; }
}
});