React 70 ⚛️ Zustand Store Setup and Actions
Zustand is a small state management library for React. It does not have a provider, an action type, or a reducer. The store is a hook that returns the state and the actions, and the components call the hook and read what they need. The state lives outside the React tree, and the components subscribe to the slices they use. The result is a global state with almost no boilerplate, and the selection model that re-renders the component only when the selected slice changes.
Key point: The Zustand store is a function that returns the state. The create function takes the initializer, and the initializer returns the state and the actions. The useStore hook is the store, and the component calls it with the selector. The re-render happens when the selected value changes by the Object.is.
Why Zustand exists
The boilerplate problem. The Redux requires the actions, the reducers, and the store setup. The Zustand requires the one create call, and the actions live inside the store.
The provider problem. The Redux requires the Provider at the root. The Zustand does not; the store is the module-level hook, and any component imports it.
The re-render problem. The Redux requires the useSelector with the careful selector. The Zustand also requires the selector, and the same narrow-selector rule applies.
The async problem. The Zustand supports the async actions inside the store. The action calls the set after the await, and the store updates.
The TypeScript problem. The Zustand is the TypeScript-friendly. The create<State>()(...) pattern infers the types, and the actions and the state are typed.
The composition problem. The Zustand supports the slices pattern. The store is the composition of the smaller stores, and the slice pattern is the convention.
The middleware problem. The Zustand has the persist, the devtools, and the immer middleware. The persist saves the store to the localStorage, and the devtools connects to the Redux DevTools.
a. The store creation
The create function takes the initializer and returns the hook. The initializer receives the set and the get, and returns the state and the actions.
The basic store
// store/todosStore.js
import { create } from 'zustand';
export const useTodosStore = create((set) => ({
items: [],
filter: 'all',
addTodo: (text) =>
set((state) => ({
items: [...state.items, { id: Date.now(), text, completed: false }],
})),
toggleTodo: (id) =>
set((state) => ({
items: state.items.map((t) =>
t.id === id ? { ...t, completed: !t.completed } : t
),
})),
removeTodo: (id) =>
set((state) => ({
items: state.items.filter((t) => t.id !== id),
})),
setFilter: (filter) => set({ filter }),
}));
The set updates the state, and the get reads the current state. The addTodo is the action, and it lives inside the store.
The TypeScript store
// store/todosStore.ts
import { create } from 'zustand';
interface Todo {
id: string;
text: string;
completed: boolean;
}
interface TodosState {
items: Todo[];
filter: 'all' | 'active' | 'completed';
addTodo: (text: string) => void;
toggleTodo: (id: string) => void;
removeTodo: (id: string) => void;
setFilter: (filter: TodosState['filter']) => void;
}
export const useTodosStore = create<TodosState>((set) => ({
items: [],
filter: 'all',
addTodo: (text) =>
set((state) => ({
items: [...state.items, { id: crypto.randomUUID(), text, completed: false }],
})),
// ...
}));
The create<TodosState>()(...) pattern types the store. The set and the get are inferred.
The set and the get
| Function | Purpose |
|---|---|
set(partial) | The merge the partial into the state |
set((state) => partial) | The functional update |
set(partial, true) | The replace the state |
get() | The read the current state |
The nested update
updateUser: (id, name) =>
set((state) => ({
users: state.users.map((u) =>
u.id === id ? { ...u, name } : u
),
})),
The Zustand uses the spread for the immutable update. The Immer middleware is the alternative.
The async action
fetchTodos: async () => {
const response = await fetch('/api/todos');
const items = await response.json();
set({ items });
},
The async action awaits the fetch and calls the set. The action is the function inside the store.
The store table
| Element | Purpose |
|---|---|
create | The store creation |
set | The update |
get | The read |
| The actions | The functions |
| The state | The data |
b. The component usage
The component calls the store hook with the selector, and the selector returns the slice.
The basic usage
import { useTodosStore } from '../store/todosStore';
function TodoList() {
const items = useTodosStore((state) => state.items);
const removeTodo = useTodosStore((state) => state.removeTodo);
return (
<ul>
{items.map((todo) => (
<li key={todo.id}>
{todo.text}
<button onClick={() => removeTodo(todo.id)}>Remove</button>
</li>
))}
</ul>
);
}
The two selectors read the items and the removeTodo. The component re-renders when the items change.
The narrow selector
// ❌ The broad selector re-renders on any change.
const state = useTodosStore((state) => state);
// ✅ The narrow selector re-renders only when the items change.
const items = useTodosStore((state) => state.items);
The narrow selector is the rule. The broad selector re-renders on any state change.
The shallow comparison
import { useShallow } from 'zustand/react/shallow';
function TodoSummary() {
const { count, filter } = useTodosStore(
useShallow((state) => ({
count: state.items.length,
filter: state.filter,
}))
);
return <div>{count} todos ({filter})</div>;
}
The useShallow compares the top-level properties. The component re-renders only when one of the selected properties changes.
The single selector versus the multiple
// ❌ The two selectors, the two subscriptions.
const items = useTodosStore((state) => state.items);
const filter = useTodosStore((state) => state.filter);
// ✅ The shallow comparison, the one subscription.
const { items, filter } = useTodosStore(
useShallow((state) => ({ items: state.items, filter: state.filter }))
);
The useShallow reduces the number of the subscriptions. The two separate selectors are also the option.
The actions outside the component
import { useTodosStore } from '../store/todosStore';
// The action is stable, and the component does not re-render.
const addTodo = useTodosStore.getState().addTodo;
addTodo('New todo');
The getState() reads the store outside the React, and the action is the function. The pattern is used in the event handlers and the utility functions.
The component table
| Form | Purpose |
|---|---|
useStore((s) => s.value) | The narrow selector |
useStore(useShallow((s) => ({ ... }))) | The shallow compare |
useStore.getState() | The read outside |
useStore.setState() | The write outside |
useStore.subscribe() | The subscribe |
c. The middleware
The Zustand middleware wraps the store and adds the functionality.
The persist middleware
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export const useTodosStore = create(
persist(
(set) => ({
items: [],
addTodo: (text) =>
set((state) => ({
items: [...state.items, { id: Date.now(), text, completed: false }],
})),
}),
{
name: 'todos-storage',
partialize: (state) => ({ items: state.items }),
}
)
);
The persist middleware saves the store to the localStorage. The name is the key, and the partialize selects the fields.
The devtools middleware
import { devtools } from 'zustand/middleware';
export const useTodosStore = create(
devtools(
(set) => ({
items: [],
addTodo: (text) =>
set(
(state) => ({
items: [...state.items, { id: Date.now(), text, completed: false }],
}),
false,
'todos/addTodo'
),
}),
{ name: 'TodosStore' }
)
);
The devtools middleware connects to the Redux DevTools. The third argument of the set is the action name.
The immer middleware
import { immer } from 'zustand/middleware/immer';
export const useTodosStore = create(
immer((set) => ({
items: [],
addTodo: (text) =>
set((state) => {
state.items.push({ id: Date.now(), text, completed: false });
}),
}))
);
The immer middleware lets the “mutation” code produce the immutable update. The pattern is the same as the Redux Toolkit.
The combining the middleware
export const useTodosStore = create(
devtools(
persist(
immer((set) => ({
items: [],
addTodo: (text) =>
set((state) => {
state.items.push({ id: Date.now(), text, completed: false });
}),
})),
{ name: 'todos-storage' }
),
{ name: 'TodosStore' }
)
);
The three middleware compose. The immer is the innermost, and the devtools is the outermost.
The slices pattern
// store/todosSlice.js
export const createTodosSlice = (set) => ({
items: [],
addTodo: (text) =>
set((state) => ({
items: [...state.items, { id: Date.now(), text, completed: false }],
})),
});
// store/userSlice.js
export const createUserSlice = (set) => ({
user: null,
setUser: (user) => set({ user }),
});
// store/index.js
import { create } from 'zustand';
export const useStore = create((...a) => ({
...createTodosSlice(...a),
...createUserSlice(...a),
}));
The slice pattern composes the store from the smaller pieces.
The middleware table
| Middleware | Purpose |
|---|---|
persist | The localStorage |
devtools | The Redux DevTools |
immer | The mutation code |
subscribeWithSelector | The selector subscribe |
combine | The combine the slices |
Complete Example Session
// ============================================
// PART 1: THE BASIC STORE
// ============================================
import { create } from 'zustand';
export const useTodosStore = create((set) => ({
items: [],
addTodo: (text) =>
set((state) => ({
items: [...state.items, { id: Date.now(), text, completed: false }],
})),
removeTodo: (id) =>
set((state) => ({
items: state.items.filter((t) => t.id !== id),
})),
}));
// ============================================
// PART 2: THE COMPONENT USAGE
// ============================================
import { useTodosStore } from '../store/todosStore';
function TodoList() {
const items = useTodosStore((state) => state.items);
const removeTodo = useTodosStore((state) => state.removeTodo);
return (
<ul>
{items.map((todo) => (
<li key={todo.id}>
{todo.text}
<button onClick={() => removeTodo(todo.id)}>Remove</button>
</li>
))}
</ul>
);
}
// ============================================
// PART 3: THE NARROW SELECTOR
// ============================================
const items = useTodosStore((state) => state.items);
// ============================================
// PART 4: THE SHALLOW COMPARISON
// ============================================
import { useShallow } from 'zustand/react/shallow';
const { items, filter } = useTodosStore(
useShallow((state) => ({ items: state.items, filter: state.filter }))
);
// ============================================
// PART 5: THE ASYNC ACTION
// ============================================
export const useTodosStore = create((set) => ({
items: [],
loading: false,
fetchTodos: async () => {
set({ loading: true });
const response = await fetch('/api/todos');
const items = await response.json();
set({ items, loading: false });
},
}));
// ============================================
// PART 6: THE PERSIST MIDDLEWARE
// ============================================
import { persist } from 'zustand/middleware';
export const useTodosStore = create(
persist(
(set) => ({
items: [],
addTodo: (text) => set((state) => ({ items: [...state.items, { text }] })),
}),
{ name: 'todos-storage' }
)
);
// ============================================
// PART 7: THE DEVTOOLS MIDDLEWARE
// ============================================
import { devtools } from 'zustand/middleware';
export const useTodosStore = create(
devtools(
(set) => ({
items: [],
addTodo: (text) => set((state) => ({ items: [...state.items, { text }] })),
}),
{ name: 'TodosStore' }
)
);
// ============================================
// PART 8: THE IMMER MIDDLEWARE
// ============================================
import { immer } from 'zustand/middleware/immer';
export const useTodosStore = create(
immer((set) => ({
items: [],
addTodo: (text) =>
set((state) => {
state.items.push({ text, completed: false });
}),
}))
);
// ============================================
// PART 9: THE SLICES PATTERN
// ============================================
const createTodosSlice = (set) => ({
items: [],
addTodo: (text) => set((state) => ({ items: [...state.items, { text }] })),
});
const createUserSlice = (set) => ({
user: null,
setUser: (user) => set({ user }),
});
export const useStore = create((...a) => ({
...createTodosSlice(...a),
...createUserSlice(...a),
}));
// ============================================
// PART 10: THE COMPLETE PATTERN
// ============================================
// 1. Use the create for the store.
// 2. The set updates, and the get reads.
// 3. The actions live inside the store.
// 4. The component uses the narrow selector.
// 5. The useShallow for the object selector.
// 6. The getState for the read outside the React.
// 7. The persist for the localStorage.
// 8. The devtools for the DevTools.
// 9. The immer for the mutation code.
// 10. The slices pattern for the composition.
The ten parts showed the basic store, the component usage, the narrow selector, the shallow comparison, the async action, the persist, the devtools, the immer, the slices pattern, and the complete pattern.
Quick Reference
The create
| Element | Purpose |
|---|---|
create(initializer) | The store creation |
set | The update |
get | The read |
set(partial) | The merge |
set(fn) | The functional |
set(partial, true) | The replace |
The Component Usage
| Form | Purpose |
|---|---|
useStore((s) => s.value) | The narrow selector |
useStore(useShallow((s) => ({ ... }))) | The shallow |
useStore.getState() | The read outside |
useStore.setState() | The write outside |
useStore.subscribe() | The subscribe |
The Middleware
| Middleware | Purpose |
|---|---|
persist | The localStorage |
devtools | The DevTools |
immer | The mutation code |
subscribeWithSelector | The selector subscribe |
combine | The combine |
The create Options
| Option | Purpose |
|---|---|
create<T>()(...) | The TypeScript |
create(persist(...)) | The persist |
create(devtools(...)) | The DevTools |
create(immer(...)) | The immer |
The Slice Pattern
| Element | Purpose |
|---|---|
createTodosSlice | The slice |
createUserSlice | The slice |
create((...a) => ({ ... })) | The combine |
Best Practices
✅ Do This:
// Use the create for the store.
export const useStore = create((set) => ({ ... })); // ✅
// Use the narrow selector.
const items = useStore((state) => state.items); // ✅
// Use the useShallow for the object selector.
const { items, filter } = useStore(useShallow((state) => ({ items: state.items, filter: state.filter }))); // ✅
// Use the getState outside the React.
const addTodo = useStore.getState().addTodo; // ✅
// Use the persist for the localStorage.
create(persist((set) => ({ ... }), { name: 'storage' })); // ✅
// Use the devtools for the DevTools.
create(devtools((set) => ({ ... }), { name: 'Store' })); // ✅
// Use the immer for the mutation code.
create(immer((set) => ({ addTodo: (text) => set((s) => { s.items.push({ text }); }) }))); // ✅
❌ Don’t Do This:
// Don't use the broad selector.
const state = useStore((state) => state); // ❌ The re-render on any change
// Don't create the new object without the shallow.
const { items } = useStore((state) => ({ items: state.items })); // ❌ The new reference
// Don't mutate the state directly.
set((state) => { state.items.push(item); return state; }); // ❌ The Immer is needed
// Don't forget the persist name.
persist((set) => ({ ... })); // ❌ The name is required
// Don't use the store in the module scope for the read.
const items = useStore((state) => state.items); // ❌ The hook outside the component
// Don't use the getState in the render.
function Component() {
const items = useStore.getState().items; // ❌ The no subscription
return ...;
}
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The re-render loop | The new object in the selector | Use useShallow |
| The component re-renders too often | The broad selector | Narrow it |
| The state not updated | The mutation without the Immer | Use the spread or the immer |
| The persist not working | No name | Add the name |
| The DevTools not working | No devtools | Add the middleware |
The getState in the render | The no subscription | Use the hook |
| The slices overwrite | The same key | Use the unique keys |
Real-World Examples
1. The Basic Store
export const useStore = create((set) => ({ items: [], addTodo: (text) => set(...) }));
2. The Component
const items = useStore((state) => state.items);
3. The Narrow Selector
const filter = useStore((state) => state.filter);
4. The Shallow
const { items, filter } = useStore(useShallow((s) => ({ items: s.items, filter: s.filter })));
5. The Async
fetchTodos: async () => {
const response = await fetch('/api/todos');
set({ items: await response.json() });
},
6. The Persist
create(persist((set) => ({ ... }), { name: 'storage' }));
7. The DevTools
create(devtools((set) => ({ ... }), { name: 'Store' }));
8. The Immer
create(immer((set) => ({ addTodo: (text) => set((s) => { s.items.push({ text }); }) })));
9. The Slices
create((...a) => ({ ...createTodosSlice(...a), ...createUserSlice(...a) }));
10. The Complete Pattern
export const useStore = create((set) => ({ ... }));
Visual
The Zustand Store
┌────────────────────────────────────────────────────────────┐
│ THE ZUSTAND STORE │
│ │
│ const useStore = create((set, get) => ({ │
│ items: [], ← the state │
│ addTodo: (text) => ← the action │
│ set((state) => ({ │
│ items: [...state.items, { text }], │
│ })), │
│ })); │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ The store is the hook. │ │
│ │ The set updates, and the get reads. │ │
│ │ The actions live inside. │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The Component Subscription
┌────────────────────────────────────────────────────────────┐
│ THE COMPONENT SUBSCRIPTION │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ const items = useStore((state) => state.items); │ │
│ │ │ │
│ │ The component subscribes to the items. │ │
│ │ The re-render happens when the items change │ │
│ │ by the Object.is. │ │
│ │ │ │
│ │ The narrow selector → the less re-render. │ │
│ │ The broad selector → the more re-render. │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The Middleware
┌────────────────────────────────────────────────────────────┐
│ THE MIDDLEWARE │
│ │
│ create( │
│ devtools( ← the outermost │
│ persist( ← the middle │
│ immer( ← the innermost │
│ (set) => ({ │
│ items: [], │
│ addTodo: (text) => │
│ set((state) => { │
│ state.items.push({ text }); │
│ }), │
│ }) │
│ ), │
│ { name: 'storage' } │
│ ), │
│ { name: 'Store' } │
│ ); │
│ │
│ The middleware composes. The innermost runs first. │
└────────────────────────────────────────────────────────────┘
The Slices
┌────────────────────────────────────────────────────────────┐
│ THE SLICES │
│ │
│ const createTodosSlice = (set) => ({ │
│ items: [], │
│ addTodo: (text) => set(...), │
│ }); │
│ │
│ const createUserSlice = (set) => ({ │
│ user: null, │
│ setUser: (user) => set({ user }), │
│ }); │
│ │
│ export const useStore = create((...a) => ({ │
│ ...createTodosSlice(...a), │
│ ...createUserSlice(...a), │
│ })); │
│ │
│ The store composes from the slices. Each slice owns the │
│ part of the state. │
└────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
create | The store creation |
set | The update |
get | The read |
| The selector | The read the slice |
useShallow | The shallow compare |
getState | The read outside |
persist | The localStorage |
devtools | The DevTools |
immer | The mutation code |
| The slices pattern | The composition |
Key takeaways:
- The Zustand store is the hook. The
createfunction takes the initializer, and the initializer returns the state and the actions. The store is the module-level, and any component imports it. - The
setupdates the state, and thegetreads it. Theset(partial)merges the partial, theset(fn)computes from the current, and theset(partial, true)replaces. - The component uses the narrow selector. The
useStore((state) => state.items)subscribes to the items. The broad selector re-renders on any change, and the narrow reduces the re-render. - The
useShallowcompares the top-level properties. The object selector returns the new reference on every call, and theuseShallowcompares the properties. The component re-renders only when one of them changes. - The
getStatereads the store outside the React. TheuseStore.getState().addTodois the function, and the pattern is for the event handlers and the utilities. - The
persistsaves the store to the localStorage. Thenameis the key, and thepartializeselects the fields. The store is the rehydrated on the load. - The
devtoolsconnects to the Redux DevTools. Thenameis the label, and thesetthird argument is the action name. The DevTools show the action log. - The
immerlets the “mutation” code. Thestate.items.push(...)looks like the mutation, and the library produces the immutable update. The pattern is the same as the Redux Toolkit. - The slices pattern composes the store. The
createTodosSliceand thecreateUserSliceare the pieces, and thecreate((...a) => ({ ... }))combines them.
Remember: The Zustand store is the small state management library for React. The create function creates the store, the set and the get manage the state, and the component uses the narrow selector. The middleware adds the persist, the devtools, and the immer. The result is the global state with the minimal boilerplate, and the components that read only what they need.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!