A Detailed Introduction to Zustand
Zustand is a state-management library for React, created by Poimandres. "Zustand" means "state" in German. Its philosophy is to offer a small, fast, unopinionated solution without much boilerplate.
On this page
A Detailed Introduction to Zustand
Zustand is a state-management library for React, created by Poimandres. The name "Zustand" means "state" in German. Its philosophy is to provide a small, fast, unopinionated solution, solving state-management problems without a lot of boilerplate code.
Why Choose Zustand?
- Extremely simple: no
Providerneeded to wrap the whole application. Just create a store and use it like a hook. - Little boilerplate: compared to Redux, you don't need to separately define
actions,reducers, anddispatchers. - Render optimization: a component only re-renders when the "slice" of state it subscribes to changes, which helps performance.
- Supports async and middleware: handling asynchronous tasks (like API calls) is very intuitive. Built-in middleware exists for
persist(saving state) anddevtools(Redux DevTools integration). - Usable outside React: you can access and update state from ordinary JavaScript modules.
Usage Guide
1. Installation
npm install zustand
2. Creating a Store
A "store" in Zustand is essentially a hook, created via the create function.
// src/stores/useCounterStore.js
import { create } from 'zustand';
const useCounterStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
export default useCounterStore;
3. Using the Store in a Component
Import the store and call it like a normal hook.
// src/components/Counter.jsx
import useCounterStore from '../stores/useCounterStore';
function Counter() {
const { count, increment, decrement, reset } = useCounterStore();
return (
<div>
<h1>Count: {count}</h1>
...
</div>
);
}
4. Render Optimization (Selectors)
To avoid unnecessary re-renders, you can pass a "selector" to grab only the piece of state you need.
import useCounterStore from '../stores/useCounterStore';
function DisplayCount() {
const count = useCounterStore((state) => state.count);
}
function Controls() {
const increment = useCounterStore((state) => state.increment);
}
Best Practices
To use Zustand effectively, follow these principles:
1. Split Stores
Don't create one giant single store. Instead, split your state into smaller stores organized by "domain" or "feature."
- Example:
useUserStore,useCartStore,useSettingsStore. - Benefit: easier to manage, easier to debug, more reusable, and fewer conflicts when working in a team.
2. Use Selectors Intelligently
Always "select" only the smallest slice of state your component needs. This is key to performance optimization.
// Good 👍: only re-renders when `user.name` changes
const userName = useUserStore(state => state.user.name);
// Bad 👎: re-renders when any property of `user` changes
const { user } = useUserStore();
A note on selecting multiple values
A selector function like state => ({ ... }) ALWAYS creates a NEW object on every run => Zustand's reference comparison thinks it's a new object and re-renders.
You can solve this with shallow to avoid unnecessary re-renders when the underlying values haven't actually changed.
// Bad 👎: re-renders every time the selector function runs, state => ({ username: state.username, email: state.email })
const { username, email } = useUserStore(
(state) => ({ username: state.username, email: state.email })
);
// Good 👍
const username = useUserStore(state => state.user.username);
const email = useUserStore(state => state.user.email);
// Or use shallow
import { shallow } from 'zustand/shallow';
// Only re-renders when `username` or `email` changes
const { username, email } = useUserStore(
(state) => ({ username: state.username, email: state.email }),
shallow
);
3. Updating Complex State with Immer
When you need to update nested objects, using the spread syntax (...) can get complicated. The immer middleware lets you write code as if you were mutating state directly, while still preserving immutability.
npm install immer
// Bad 👎
updateName: (newNames) => set(state => (
{
...state, // 1. Copy the entire original state
user: {
...state.user, // 2. Copy the `user` object
profile: {
...state.user.profile, // 3. Copy the `profile` object
name: 'Jane Doe' // 4. Finally, update the `name` value
}
}
}
));
// Good 👍
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
const useUserStore = create(
// Wrap the store creator with the `immer` middleware
immer((set) => ({
user: {
profile: {
name: 'John Doe',
email: 'john@example.com'
},
settings: {
theme: 'dark'
}
},
posts: [],
// Action to update the user's name
updateName: (newName) => {
// Inside the set function, `state` is now an Immer `draft`
set((state) => {
// Direct-mutation syntax, very clean!
state.user.profile.name = newName;
});
},
);
export default useUserStore;
4. Separate Actions from State
To keep components "dumber" and focused only on rendering, you can separate actions out from the state definition. This also lets components that only use actions avoid re-rendering when state changes.
// src/stores/useCartStore.js
const initialState = { items: [], total: 0 };
export const useCartStore = create((set) => ({
...initialState,
// Actions defined separately
addItem: (item) => set(state => ({ items: [...state.items, item] })),
clearCart: () => set({ items: [], total: 0 }),
}));
// Grab actions to use elsewhere without subscribing to state
export const { addItem, clearCart } = useCartStore.getState();
5. Name Stores by Hook Convention
Always start your store names with use (e.g., useUserStore). This follows the Rules of Hooks and lets ESLint catch mistakes if you use it incorrectly.
Advanced Features
1. Handling Asynchronous Tasks
Write an async function and call set once you have a result.
const useUserStore = create((set) => ({
user: null,
loading: false,
fetchUser: async (userId) => {
set({ loading: true });
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
const user = await response.json();
set({ user, loading: false });
} catch (error) {
set({ loading: false });
}
},
}));
2. Middleware (devtools and persist)
Zustand supports middleware to extend its functionality.
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
const useSettingsStore = create(
devtools(
persist(
(set) => ({
theme: 'light',
toggleTheme: () => set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' })),
}),
{
name: 'settings-storage', // The key name in localStorage
}
)
)
);
A Quick Comparison with Redux
| Feature | Zustand | Redux (with Redux Toolkit) |
|---|---|---|
| Boilerplate | Very little | Quite a lot |
| Provider | Not needed | Required |
| Difficulty | Easy | Harder |
| Render optimization | Automatic via selectors | Requires useSelector |
| Async | Natural (async/await) | Requires createAsyncThunk |
| Size | ~1 KB | ~8 KB+ |
