zustand

Zustand state management patterns for React including store design, selectors, slices, middleware (immer, persist, devtools), and async actions. Use when managing client-side state, creating stores, working with Zustand, or when the user asks about global state management, store patterns, or state persistence.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "zustand" with this command: npx skills add grahamcrackers/skills/grahamcrackers-skills-zustand

Zustand Patterns

Basic Store

import { create } from "zustand";

interface CounterStore {
    count: number;
    increment: () => void;
    decrement: () => void;
    reset: () => void;
}

const useCounterStore = create<CounterStore>((set) => ({
    count: 0,
    increment: () => set((state) => ({ count: state.count + 1 })),
    decrement: () => set((state) => ({ count: state.count - 1 })),
    reset: () => set({ count: 0 }),
}));

Selectors

Always select only the state you need — this prevents re-renders when unrelated state changes:

// Select individual values
const count = useCounterStore((state) => state.count);
const increment = useCounterStore((state) => state.increment);

// Select multiple values with useShallow
import { useShallow } from "zustand/shallow";

const { count, increment } = useCounterStore(
    useShallow((state) => ({ count: state.count, increment: state.increment })),
);

Never destructure the entire store without a selector:

// Bad — re-renders on every state change
const { count, increment } = useCounterStore();

// Good — re-renders only when count changes
const count = useCounterStore((state) => state.count);

Async Actions

interface UserStore {
    user: User | null;
    isLoading: boolean;
    error: string | null;
    fetchUser: (id: string) => Promise<void>;
}

const useUserStore = create<UserStore>((set) => ({
    user: null,
    isLoading: false,
    error: null,
    fetchUser: async (id) => {
        set({ isLoading: true, error: null });
        try {
            const user = await api.users.getById(id);
            set({ user, isLoading: false });
        } catch (error) {
            set({ error: "Failed to fetch user", isLoading: false });
        }
    },
}));

For server data, prefer TanStack Query over Zustand — Zustand is for client-only state.

Middleware

Immer

Write mutable-looking updates safely:

import { create } from "zustand";
import { immer } from "zustand/middleware/immer";

const useTodoStore = create<TodoStore>()(
    immer((set) => ({
        todos: [],
        addTodo: (text) =>
            set((state) => {
                state.todos.push({ id: crypto.randomUUID(), text, completed: false });
            }),
        toggleTodo: (id) =>
            set((state) => {
                const todo = state.todos.find((t) => t.id === id);
                if (todo) todo.completed = !todo.completed;
            }),
    })),
);

Persist

Sync state to storage:

import { persist } from "zustand/middleware";

const useSettingsStore = create<SettingsStore>()(
    persist(
        (set) => ({
            theme: "light",
            language: "en",
            setTheme: (theme) => set({ theme }),
            setLanguage: (language) => set({ language }),
        }),
        {
            name: "settings",
            partialize: (state) => ({
                theme: state.theme,
                language: state.language,
            }),
        },
    ),
);
  • name is the storage key.
  • partialize controls which state is persisted — exclude functions and transient state.
  • Default storage is localStorage. Use storage: createJSONStorage(() => sessionStorage) for session storage.

Devtools

import { devtools } from "zustand/middleware";

const useStore = create<Store>()(
    devtools(
        (set) => ({
            // ...
        }),
        { name: "MyStore" },
    ),
);

Combining Middleware

Stack middleware from inside out — immer → persist → devtools:

const useStore = create<Store>()(
    devtools(
        persist(
            immer((set) => ({
                // store definition
            })),
            { name: "store-key" },
        ),
        { name: "StoreName" },
    ),
);

Slice Pattern

Split large stores into logical slices:

interface AuthSlice {
    user: User | null;
    login: (credentials: Credentials) => Promise<void>;
    logout: () => void;
}

interface UISlice {
    sidebarOpen: boolean;
    toggleSidebar: () => void;
}

const createAuthSlice: StateCreator<AuthSlice & UISlice, [], [], AuthSlice> = (set) => ({
    user: null,
    login: async (credentials) => {
        const user = await api.auth.login(credentials);
        set({ user });
    },
    logout: () => set({ user: null }),
});

const createUISlice: StateCreator<AuthSlice & UISlice, [], [], UISlice> = (set) => ({
    sidebarOpen: true,
    toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
});

const useAppStore = create<AuthSlice & UISlice>()((...args) => ({
    ...createAuthSlice(...args),
    ...createUISlice(...args),
}));

Computed / Derived State

Derive values in selectors, not in the store:

// In the component or a custom hook
const completedCount = useTodoStore((state) => state.todos.filter((t) => t.completed).length);

// For expensive computations, memoize
const stats = useTodoStore(
    useShallow((state) => ({
        total: state.todos.length,
        completed: state.todos.filter((t) => t.completed).length,
    })),
);

Accessing State Outside React

// Get current state
const count = useCounterStore.getState().count;

// Subscribe to changes
const unsubscribe = useCounterStore.subscribe((state) => console.log("Count:", state.count));

// Set state from outside React
useCounterStore.getState().increment();

Store Organization

src/
├── stores/
│   ├── auth-store.ts
│   ├── settings-store.ts
│   └── ui-store.ts
  • One store per domain concern.
  • Keep stores small and focused — don't create a single global "app store".
  • Name stores with the use*Store convention.

When to Use Zustand vs. Alternatives

Use CaseSolution
Client UI state (theme, sidebar, modals)Zustand
Server data (API responses, caching)TanStack Query
Form stateReact Hook Form
URL state (filters, pagination)URL search params
Component-local stateuseState / useReducer
Global shared state (auth, preferences)Zustand

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

typescript-best-practices

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

clean-code-principles

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

typescript-advanced-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

bulletproof-react-patterns

No summary provided by upstream source.

Repository SourceNeeds Review