grahamcrackers/skills

zustand

Zustand state management patterns for React including store design, selectors, slices, middleware (immer, persist, devtools), and async actions.

First seen Feb 25, 2026

Installation

$ npx skills add grahamcrackers/skills --skill zustand

Summary

  • 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.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from grahamcrackers/skills · top by installs.

npx skills add grahamcrackers/skills

Browse all from grahamcrackers/skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

License LICENSE
Default branch main
Open issues 0
Status Active

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 6,776 B
  • docs SUMMARY.md 326 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 9 installs

SKILL.md

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 Case Solution
Client UI state (theme, sidebar, modals) Zustand
Server data (API responses, caching) TanStack Query
Form state React Hook Form
URL state (filters, pagination) URL search params
Component-local state useState / useReducer
Global shared state (auth, preferences) Zustand