| |

React 70 ⚛️ Zustand Store Setup and Actions

⏱️ Estimated read: 14 min | |

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

FunctionPurpose
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

ElementPurpose
createThe store creation
setThe update
getThe read
The actionsThe functions
The stateThe 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

FormPurpose
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

MiddlewarePurpose
persistThe localStorage
devtoolsThe Redux DevTools
immerThe mutation code
subscribeWithSelectorThe selector subscribe
combineThe 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

ElementPurpose
create(initializer)The store creation
setThe update
getThe read
set(partial)The merge
set(fn)The functional
set(partial, true)The replace

The Component Usage

FormPurpose
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

MiddlewarePurpose
persistThe localStorage
devtoolsThe DevTools
immerThe mutation code
subscribeWithSelectorThe selector subscribe
combineThe combine

The create Options

OptionPurpose
create<T>()(...)The TypeScript
create(persist(...))The persist
create(devtools(...))The DevTools
create(immer(...))The immer

The Slice Pattern

ElementPurpose
createTodosSliceThe slice
createUserSliceThe 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

PitfallWhy It HappensFix
The re-render loopThe new object in the selectorUse useShallow
The component re-renders too oftenThe broad selectorNarrow it
The state not updatedThe mutation without the ImmerUse the spread or the immer
The persist not workingNo nameAdd the name
The DevTools not workingNo devtoolsAdd the middleware
The getState in the renderThe no subscriptionUse the hook
The slices overwriteThe same keyUse 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

ItemValue
createThe store creation
setThe update
getThe read
The selectorThe read the slice
useShallowThe shallow compare
getStateThe read outside
persistThe localStorage
devtoolsThe DevTools
immerThe mutation code
The slices patternThe composition

Key takeaways:

  • The Zustand store is the hook. The create function takes the initializer, and the initializer returns the state and the actions. The store is the module-level, and any component imports it.
  • The set updates the state, and the get reads it. The set(partial) merges the partial, the set(fn) computes from the current, and the set(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 useShallow compares the top-level properties. The object selector returns the new reference on every call, and the useShallow compares the properties. The component re-renders only when one of them changes.
  • The getState reads the store outside the React. The useStore.getState().addTodo is the function, and the pattern is for the event handlers and the utilities.
  • The persist saves the store to the localStorage. The name is the key, and the partialize selects the fields. The store is the rehydrated on the load.
  • The devtools connects to the Redux DevTools. The name is the label, and the set third argument is the action name. The DevTools show the action log.
  • The immer lets the “mutation” code. The state.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 createTodosSlice and the createUserSlice are the pieces, and the create((...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!