Lesson 18 / 25

Async Actions and the Slices Pattern

Fetch in actions; split big stores.

Actions can be async

Zustand has no special async API: an action is a function, so it can be async, call set before and after await, and read fresh state with get(). Track loading and error fields next to the data. For large stores, the slices pattern splits state into creator functions typed with StateCreator<Combined, [], [], Slice> and spreads them into one create call, so each feature owns its part while actions can still reach the whole store. For server data, consider pairing Zustand with TanStack Query rather than hand-rolling caching.

An async slice combined with another

Slices with StateCreator.

import { create, type StateCreator } from 'zustand';

type UserSlice = {
  user: User | null;
  status: 'idle' | 'loading' | 'error';
  loadUser: (id: string) => Promise<void>;
};
type UiSlice = { sidebarOpen: boolean; toggleSidebar: () => void };
type Store = UserSlice & UiSlice;

const createUserSlice: StateCreator<Store, [], [], UserSlice> = (set) => ({
  user: null,
  status: 'idle',
  loadUser: async (id) => {
    set({ status: 'loading' });
    try {
      const res = await fetch(`/api/users/${id}`);
      if (!res.ok) throw new Error(String(res.status));
      set({ user: await res.json(), status: 'idle' });
    } catch {
      set({ status: 'error' });
    }
  },
});

const createUiSlice: StateCreator<Store, [], [], UiSlice> = (set) => ({
  sidebarOpen: false,
  toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
});

export const useStore = create<Store>()((...a) => ({
  ...createUserSlice(...a),
  ...createUiSlice(...a),
}));

Guard against stale responses

If the same action can run twice (fast typing, quick navigation), ignore late responses, for example by checking that the requested id still matches before calling set, or by using an AbortController.

Quick check: How do you write an async action in Zustand?

  • As an async function in the store that calls set before and after await
  • With a required createAsyncThunk import
  • Only through middleware
  • By returning a Promise from the selector
Answer

As an async function in the store that calls set before and after await — Actions are plain functions.