home / skills / prowler-cloud / prowler / zustand-5

zustand-5 skill

/skills/zustand-5

This skill applies Zustand 5 patterns to organize client-side state with stores, selectors, slices, and middleware for scalable React apps.

npx playbooks add skill prowler-cloud/prowler --skill zustand-5

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

Files (1)
SKILL.md
4.7 KB
---
name: zustand-5
description: >
  Zustand 5 state management patterns.
  Trigger: When implementing client-side state with Zustand (stores, selectors, persist middleware, slices).
license: Apache-2.0
metadata:
  author: prowler-cloud
  version: "1.0"
  scope: [root, ui]
  auto_invoke: "Using Zustand stores"
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task
---

## 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)
);
```

Overview

This skill explains practical patterns for client-side state management with Zustand 5. It covers store basics, selectors, persist and immer middleware, slices, async actions, DevTools, and accessing the store outside React. The content focuses on reducing re-renders, organizing stores, and persisting state safely.

How this skill works

It shows how to define typed stores using create, apply middleware (persist, immer, devtools), and compose slices into a single store. It demonstrates selector usage and useShallow to limit re-renders, async actions for fetching, and using getState/subscribe outside React. Examples include persisting settings, immutable updates with Immer, and splitting concerns via slice factories.

When to use it

  • Building small to medium client-side state without Redux or heavy boilerplate
  • Persisting user settings across sessions (theme, locale)
  • Composing independent features with slice pattern (user, cart, todos)
  • Performing async data loading and keeping loading/error flags in store
  • Debugging state with Redux DevTools or subscribing outside React

Best practices

  • Select only necessary fields in components to avoid unnecessary re-renders (useShallow for multiple fields)
  • Use persist middleware for safe storage; choose clear keys and consider migrations/expiration
  • Use slices to keep store modules focused and testable; compose into a single store
  • Use immer middleware for concise immutable updates when manipulating nested state
  • Keep async actions idempotent and manage loading/error state to show UI feedback

Example use cases

  • Simple counter or UI toggles using a minimal store and direct actions
  • User settings persisted to localStorage (theme, language) with persist middleware
  • Fetching user profile with fetchUser action that sets loading and error flags
  • Shopping cart implemented as a slice with addItem/removeItem and composed into app store
  • Todo list managed with immer for easy in-place mutations and unique IDs

FAQ

How do I prevent components from re-rendering on unrelated state changes?

Select only the fields you need (useUserStore(s => s.name)). For multiple fields, use useShallow so components re-render only when selected values change.

When should I use immer middleware?

Use immer when you need readable, in-place mutation syntax for complex nested updates. It keeps code concise while preserving immutability under the hood.