zustand-patterns

Zustand State Management Patterns

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-patterns" with this command: npx skills add masanao-ohba/claude-manifests/masanao-ohba-claude-manifests-zustand-patterns

Zustand State Management Patterns

Basic Store

Simple Store

// stores/counter.ts import { create } from 'zustand';

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

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

Usage

'use client';

import { useCounterStore } from '@/stores/counter';

export function Counter() { const count = useCounterStore((state) => state.count); const increment = useCounterStore((state) => state.increment);

return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> </div> ); }

Store Patterns

Slice Pattern

Split large stores into focused slices:

// stores/app-store.ts import { create } from 'zustand';

// Auth slice interface AuthSlice { user: User | null; isAuthenticated: boolean; login: (user: User) => void; logout: () => void; }

const createAuthSlice = (set): AuthSlice => ({ user: null, isAuthenticated: false, login: (user) => set({ user, isAuthenticated: true }), logout: () => set({ user: null, isAuthenticated: false }), });

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

const createUISlice = (set): UISlice => ({ sidebarOpen: true, toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })), });

// Combined store type AppState = AuthSlice & UISlice;

export const useAppStore = create<AppState>()((...a) => ({ ...createAuthSlice(...a), ...createUISlice(...a), }));

Computed Values

Derive values from state:

interface CartState { items: CartItem[]; addItem: (item: CartItem) => void; removeItem: (id: string) => void; // Computed value as function total: () => number; itemCount: () => number; }

export const useCartStore = create<CartState>((set, get) => ({ items: [], addItem: (item) => set((state) => ({ items: [...state.items, item] })), removeItem: (id) => set((state) => ({ items: state.items.filter((item) => item.id !== id), })), total: () => { return get().items.reduce((sum, item) => sum + item.price, 0); }, itemCount: () => get().items.length, }));

// Usage function CartSummary() { const total = useCartStore((state) => state.total()); const itemCount = useCartStore((state) => state.itemCount());

return <div>Total: ${total} ({itemCount} items)</div>; }

Selectors

Optimized Selectors

Prevent unnecessary re-renders:

import { useCartStore } from '@/stores/cart'; import { shallow } from 'zustand/shallow';

// Bad: Entire store subscribed function BadComponent() { const store = useCartStore(); return <div>{store.items.length}</div>; }

// Good: Only subscribe to needed value function GoodComponent() { const itemCount = useCartStore((state) => state.items.length); return <div>{itemCount}</div>; }

// Better: Multiple values with shallow comparison function BetterComponent() { const { items, addItem } = useCartStore( (state) => ({ items: state.items, addItem: state.addItem }), shallow );

return <div>{items.length}</div>; }

Custom Selectors

// stores/selectors.ts import { useUserStore } from './user';

export const useIsAdmin = () => useUserStore((state) => state.user?.role === 'admin');

export const useUserName = () => useUserStore((state) => state.user?.name ?? 'Guest');

export const useHasPermission = (permission: string) => useUserStore((state) => state.user?.permissions.includes(permission) );

// Usage function AdminPanel() { const isAdmin = useIsAdmin(); if (!isAdmin) return null; return <div>Admin Panel</div>; }

Async Actions

Fetch Data

interface PostsState { posts: Post[]; isLoading: boolean; error: string | null; fetchPosts: () => Promise<void>; }

export const usePostsStore = create<PostsState>((set) => ({ posts: [], isLoading: false, error: null, fetchPosts: async () => { set({ isLoading: true, error: null }); try { const response = await fetch('/api/posts'); const posts = await response.json(); set({ posts, isLoading: false }); } catch (error) { set({ error: error.message, isLoading: false }); } }, }));

// Usage function PostList() { const { posts, isLoading, error, fetchPosts } = usePostsStore();

useEffect(() => { fetchPosts(); }, [fetchPosts]);

if (isLoading) return <Loading />; if (error) return <Error message={error} />; return <div>{posts.map((post) => <PostCard post={post} />)}</div>; }

Middleware

Persist

Persist state to localStorage:

import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware';

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

export const usePreferencesStore = create<UserPreferences>()( persist( (set) => ({ theme: 'light', language: 'en', setTheme: (theme) => set({ theme }), setLanguage: (language) => set({ language }), }), { name: 'user-preferences', // localStorage key storage: createJSONStorage(() => localStorage), partialize: (state) => ({ theme: state.theme, language: state.language, }), // Only persist these fields } ) );

Devtools

Redux DevTools integration:

import { create } from 'zustand'; import { devtools } from 'zustand/middleware';

export const useAppStore = create<AppState>()( devtools( (set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 }), false, 'increment'), decrement: () => set((state) => ({ count: state.count - 1 }), false, 'decrement'), }), { name: 'AppStore' } ) );

Immer

Use Immer for immutable updates:

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

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

export const useTodoStore = create<TodoState>()( immer((set) => ({ todos: [], addTodo: (text) => set((state) => { state.todos.push({ id: Date.now().toString(), text, done: false }); }), toggleTodo: (id) => set((state) => { const todo = state.todos.find((t) => t.id === id); if (todo) todo.done = !todo.done; }), })) );

Combined Middleware

import { create } from 'zustand'; import { devtools, persist } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer';

export const useStore = create<State>()( devtools( persist( immer((set) => ({ // store implementation })), { name: 'app-storage' } ), { name: 'AppStore' } ) );

Store Organization

Structure

stores/ ├── index.ts # Export all stores ├── auth-store.ts # Authentication state ├── cart-store.ts # Shopping cart ├── ui-store.ts # UI state (modals, sidebar, etc.) ├── preferences-store.ts # User preferences └── selectors/ ├── auth-selectors.ts └── cart-selectors.ts

Index File

// stores/index.ts export { useAuthStore } from './auth-store'; export { useCartStore } from './cart-store'; export { useUIStore } from './ui-store'; export { usePreferencesStore } from './preferences-store';

TypeScript Patterns

Typed Actions

interface UserState { user: User | null; status: 'idle' | 'loading' | 'success' | 'error'; error: string | null; setUser: (user: User) => void; clearUser: () => void; fetchUser: (id: string) => Promise<void>; }

export const useUserStore = create<UserState>((set) => ({ user: null, status: 'idle', error: null, setUser: (user) => set({ user, status: 'success', error: null }), clearUser: () => set({ user: null, status: 'idle', error: null }), fetchUser: async (id) => { set({ status: 'loading' }); try { const user = await api.fetchUser(id); set({ user, status: 'success', error: null }); } catch (error) { set({ status: 'error', error: error.message }); } }, }));

Testing

Test Setup

// tests/stores/counter.test.ts import { renderHook, act } from '@testing-library/react'; import { useCounterStore } from '@/stores/counter';

describe('useCounterStore', () => { beforeEach(() => { // Reset store before each test useCounterStore.setState({ count: 0 }); });

it('increments count', () => { const { result } = renderHook(() => useCounterStore());

act(() => {
  result.current.increment();
});

expect(result.current.count).toBe(1);

});

it('decrements count', () => { const { result } = renderHook(() => useCounterStore());

act(() => {
  result.current.decrement();
});

expect(result.current.count).toBe(-1);

}); });

Best Practices

Do

  • Keep stores focused on specific domains

  • Use selectors to prevent unnecessary re-renders

  • Use middleware for cross-cutting concerns

  • Type all stores with TypeScript

  • Extract complex logic to separate functions

  • Use shallow comparison for object selections

  • Persist only necessary state

Don't

  • Don't put all state in one store

  • Don't select entire store when only part is needed

  • Don't mutate state directly (use set or Immer)

  • Don't use Zustand for server state (use React Query)

  • Don't persist sensitive data

  • Don't forget to reset state when needed

When to Use

Use Zustand

  • Client-side UI state (modals, sidebar, theme)

  • Form state (multi-step forms)

  • Global app state (user preferences, settings)

  • Temporary state shared across components

Use React Query

  • Server state (API data)

  • Caching and revalidation

  • Background updates

  • Optimistic updates with server sync

Use React State

  • Local component state

  • Form inputs

  • Toggle states

  • State not shared with other components

Performance

Optimization

  • Use selective subscriptions (don't select entire store)

  • Use shallow comparison for object selections

  • Batch updates when possible

  • Split large stores into smaller, focused stores

  • Use computed values (functions) instead of derived state

Monitoring

  • Use Redux DevTools middleware in development

  • Monitor render counts in React DevTools

  • Profile component re-renders

  • Check localStorage size if using persist

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.

General

requirement-analyzer

No summary provided by upstream source.

Repository SourceNeeds Review
General

test-case-designer

No summary provided by upstream source.

Repository SourceNeeds Review
General

test-validator

No summary provided by upstream source.

Repository SourceNeeds Review
General

functional-designer

No summary provided by upstream source.

Repository SourceNeeds Review