home / skills / gentleman-programming / gentleman-skills / zustand-5

zustand-5 skill

/curated/zustand-5

This skill helps you implement Zustand state patterns including slices, persistence, selectors, async actions, and middleware for React apps.

npx playbooks add skill gentleman-programming/gentleman-skills --skill zustand-5

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
4.6 KB
---
name: zustand-5
description: >
  Zustand 5 state management patterns.
  Trigger: When managing React state with Zustand.
license: Apache-2.0
metadata:
  author: gentleman-programming
  version: "1.0"
---

## Basic Store

```typescript
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 }),
}));

// Usage
function Counter() {
  const { count, increment, decrement } = useCounterStore();
  return (
    <div>
      <span>{count}</span>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
}
```

## Persist Middleware

```typescript
import { create } from "zustand";
import { persist } from "zustand/middleware";

interface SettingsStore {
  theme: "light" | "dark";
  language: string;
  setTheme: (theme: "light" | "dark") => void;
  setLanguage: (language: string) => void;
}

const useSettingsStore = create<SettingsStore>()(
  persist(
    (set) => ({
      theme: "light",
      language: "en",
      setTheme: (theme) => set({ theme }),
      setLanguage: (language) => set({ language }),
    }),
    {
      name: "settings-storage",  // localStorage key
    }
  )
);
```

## Selectors (Zustand 5)

```typescript
// ✅ Select specific fields to prevent unnecessary re-renders
function UserName() {
  const name = useUserStore((state) => state.name);
  return <span>{name}</span>;
}

// ✅ For multiple fields, use useShallow
import { useShallow } from "zustand/react/shallow";

function UserInfo() {
  const { name, email } = useUserStore(
    useShallow((state) => ({ name: state.name, email: state.email }))
  );
  return <div>{name} - {email}</div>;
}

// ❌ AVOID: Selecting entire store (causes re-render on any change)
const store = useUserStore();  // Re-renders on ANY state change
```

## Async Actions

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

const useUserStore = create<UserStore>((set) => ({
  user: null,
  loading: false,
  error: null,

  fetchUser: async (id) => {
    set({ loading: true, error: null });
    try {
      const response = await fetch(`/api/users/${id}`);
      const user = await response.json();
      set({ user, loading: false });
    } catch (error) {
      set({ error: "Failed to fetch user", loading: false });
    }
  },
}));
```

## Slices Pattern

```typescript
// userSlice.ts
interface UserSlice {
  user: User | null;
  setUser: (user: User) => void;
  clearUser: () => void;
}

const createUserSlice = (set): UserSlice => ({
  user: null,
  setUser: (user) => set({ user }),
  clearUser: () => set({ user: null }),
});

// cartSlice.ts
interface CartSlice {
  items: CartItem[];
  addItem: (item: CartItem) => void;
  removeItem: (id: string) => void;
}

const createCartSlice = (set): CartSlice => ({
  items: [],
  addItem: (item) => set((state) => ({ items: [...state.items, item] })),
  removeItem: (id) => set((state) => ({
    items: state.items.filter(i => i.id !== id)
  })),
});

// store.ts
type Store = UserSlice & CartSlice;

const useStore = create<Store>()((...args) => ({
  ...createUserSlice(...args),
  ...createCartSlice(...args),
}));
```

## Immer Middleware

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

interface TodoStore {
  todos: Todo[];
  addTodo: (text: string) => void;
  toggleTodo: (id: string) => void;
}

const useTodoStore = create<TodoStore>()(
  immer((set) => ({
    todos: [],

    addTodo: (text) => set((state) => {
      // Mutate directly with Immer!
      state.todos.push({ id: crypto.randomUUID(), text, done: false });
    }),

    toggleTodo: (id) => set((state) => {
      const todo = state.todos.find(t => t.id === id);
      if (todo) todo.done = !todo.done;
    }),
  }))
);
```

## DevTools

```typescript
import { create } from "zustand";
import { devtools } from "zustand/middleware";

const useStore = create<Store>()(
  devtools(
    (set) => ({
      // store definition
    }),
    { name: "MyStore" }  // Name in Redux DevTools
  )
);
```

## Outside React

```typescript
// Access store outside components
const { count, increment } = useCounterStore.getState();
increment();

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

## Keywords
zustand, state management, react, store, persist, middleware

Overview

This skill documents practical Zustand 5 state management patterns for React apps, covering basic stores, persistence, selectors, async actions, slices, Immer, DevTools, and usage outside React. It focuses on patterns that reduce re-renders, improve testability, and scale state logic across components. Use it as a quick reference to implement common store features and recommended practices.

How this skill works

The skill explains how to create typed stores with create(), compose slices to build larger stores, and apply middlewares like persist, immer, and devtools. It demonstrates selector usage to avoid unnecessary re-renders, async action patterns for data fetching, and how to read/subscribe to state outside React components. Examples show minimal, composable code you can drop into your project.

When to use it

  • Local or global React state that needs to be simple, fast, and predictable
  • When you want persisted settings like theme or language across sessions
  • To split large state into focused slices for maintainability and reuse
  • When you need immutable updates with a simple API using Immer
  • When you need to debug state changes with Redux DevTools
  • Accessing or subscribing to state from non-React code (e.g., utilities)

Best practices

  • Select only the fields a component needs to prevent unnecessary re-renders
  • Use useShallow for multiple fields to avoid deep equality costs
  • Compose store logic with slices to keep code modular and testable
  • Wrap async flows in actions that set loading/error states for UI feedback
  • Persist only necessary keys and avoid storing large transient data
  • Use immer middleware for ergonomic mutations when complex nested updates exist

Example use cases

  • A counter component with increment/decrement/reset backed by a simple store
  • User settings persisted to localStorage: theme and language across reloads
  • User profile fetch with loading and error handling implemented as an async action
  • A shopping cart built from cart and user slices combined into one store
  • Todo list using Immer to mutate nested arrays while keeping immutability
  • Inspecting and subscribing to store changes from a non-React background task

FAQ

How do I avoid re-renders when reading multiple fields?

Select specific fields with a selector or use useShallow when returning multiple properties to prevent extra re-renders.

Should I persist the entire store?

No. Persist only stable, necessary keys like user preferences. Avoid persisting large or sensitive transient data.