zustand

📁 dsantiagomj/dsmj-ai-toolkit 📅 12 days ago
4
总安装量
4
周安装量
#47930
全站排名
安装命令
npx skills add https://github.com/dsantiagomj/dsmj-ai-toolkit --skill zustand

Agent 安装分布

gemini-cli 4
github-copilot 4
codex 4
amp 4
kimi-cli 4
opencode 4

Skill 文档

Zustand – Minimalist State Management

Simple, fast, and scalable state management without the boilerplate


When to Use

Use Zustand when you need:

  • Global state shared across multiple components
  • Simple API without boilerplate (no providers, actions, reducers)
  • Performance with fine-grained subscriptions
  • TypeScript support with excellent type inference
  • Middleware for persistence, devtools, or immer
  • Lightweight solution (small bundle size)

Choose alternatives when:

  • Component-local state is sufficient (use useState)
  • You need server state management (use React Query, SWR)
  • Complex state machines required (use XState)
  • Team strongly prefers Redux patterns

Critical Patterns

Pattern 1: Selective Subscriptions for Performance

// ✅ Good: Subscribe to specific state slices
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);

// Component only re-renders when count changes
export function Counter() {
  const count = useStore((state) => state.count);
  const increment = useStore((state) => state.increment);

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

// ✅ Good: Multiple values with shallow comparison
import { shallow } from 'zustand/shallow';

const { user, isLoading } = useUserStore(
  (state) => ({ user: state.user, isLoading: state.isLoading }),
  shallow
);

// ❌ Bad: Subscribing to entire store
const store = useStore(); // Re-renders on ANY state change!

// ❌ Bad: Creating new object without shallow
const { count, total } = useStore((state) => ({
  count: state.count,
  total: state.total,
})); // New object every render, always re-renders

Why: Selective subscriptions prevent unnecessary re-renders; shallow comparison for multiple values avoids performance issues.


Pattern 2: Actions with setState Patterns

// ✅ Good: Update state immutably with function form
interface CounterStore {
  count: number;
  increment: () => void;
  incrementBy: (value: number) => void;
  reset: () => void;
}

export const useCounterStore = create<CounterStore>((set) => ({
  count: 0,
  
  increment: () => set((state) => ({ count: state.count + 1 })),
  
  incrementBy: (value) => set((state) => ({ count: state.count + value })),
  
  reset: () => set({ count: 0 }), // Direct object when not using previous state
}));

// ✅ Good: Complex updates with get
export const useCartStore = create<CartStore>((set, get) => ({
  items: [],
  total: 0,
  
  addItem: (item) => set((state) => {
    const newItems = [...state.items, item];
    const newTotal = newItems.reduce((sum, i) => sum + i.price, 0);
    
    return { items: newItems, total: newTotal };
  }),
  
  removeItem: (id) => set((state) => ({
    items: state.items.filter(item => item.id !== id),
  })),
  
  getItemCount: () => get().items.length, // Access state without subscribing
}));

// ❌ Bad: Mutating state directly
increment: () => {
  const state = get();
  state.count++; // MUTATION!
  set(state);
}

// ❌ Bad: Not using function form when depending on previous state
increment: () => set({ count: get().count + 1 }); // Race condition possible

Why: Immutable updates prevent bugs; function form ensures correct updates with concurrent actions; get() allows reading state in actions.


Pattern 3: Organizing Large Stores with Slices

// ✅ Good: Split large stores into slices
import { StateCreator } from 'zustand';

export interface UserSlice {
  user: User | null;
  setUser: (user: User) => void;
  clearUser: () => void;
}

export const createUserSlice: StateCreator<StoreState, [], [], UserSlice> = (set) => ({
  user: null,
  setUser: (user) => set({ user }),
  clearUser: () => set({ user: null }),
});

// Combine slices
type StoreState = UserSlice & SettingsSlice;
export const useStore = create<StoreState>()((...a) => ({
  ...createUserSlice(...a),
  ...createSettingsSlice(...a),
}));

// ❌ Bad: One massive store object with 50+ fields

Why: Slices improve maintainability and separate concerns. For full slice patterns, see references/patterns.md.


Pattern 4: Async Actions with Proper Loading States

// ✅ Good: Track loading and error states
interface UserStore {
  user: User | null;
  isLoading: boolean;
  error: string | null;
  fetchUser: (id: string) => Promise<void>;
  clearError: () => void;
}

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

  fetchUser: async (id) => {
    set({ isLoading: true, error: null });

    try {
      const response = await fetch(`/api/users/${id}`);
      
      if (!response.ok) {
        throw new Error('Failed to fetch user');
      }
      
      const user = await response.json();
      set({ user, isLoading: false });
    } catch (error) {
      set({
        error: error instanceof Error ? error.message : 'Unknown error',
        isLoading: false,
        user: null,
      });
    }
  },

  clearError: () => set({ error: null }),
}));

// Usage in component
export function UserProfile({ userId }: { userId: string }) {
  const { user, isLoading, error, fetchUser } = useUserStore(
    (state) => ({
      user: state.user,
      isLoading: state.isLoading,
      error: state.error,
      fetchUser: state.fetchUser,
    }),
    shallow
  );

  useEffect(() => {
    fetchUser(userId);
  }, [userId, fetchUser]);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>No user found</div>;

  return <div>{user.name}</div>;
}

// ❌ Bad: No loading or error states
export const useUserStore = create<UserStore>((set) => ({
  user: null,
  
  fetchUser: async (id) => {
    const response = await fetch(`/api/users/${id}`);
    const user = await response.json();
    set({ user }); // No indication of loading or errors
  },
}));

Why: Loading states provide UX feedback; error handling improves reliability; clear error makes debugging easier.


Pattern 5: Middleware Usage

For data persistence, debugging, and state immutability, see Middleware & Advanced.


Anti-Patterns

Anti-Pattern 1: Using Zustand for Server State

// ❌ Problem: Managing server data with Zustand
export const usePostsStore = create<PostsStore>((set) => ({
  posts: [],
  isLoading: false,
  
  fetchPosts: async () => {
    set({ isLoading: true });
    const posts = await fetch('/api/posts').then(r => r.json());
    set({ posts, isLoading: false });
  },
  
  updatePost: async (id, data) => {
    await fetch(`/api/posts/${id}`, { method: 'PUT', body: JSON.stringify(data) });
    // Manual cache invalidation...
  },
}));

Why it’s wrong: Manual cache management; no automatic refetching; cache invalidation is hard; missing features like background refetch, deduplication.

Solution:

// ✅ Use React Query for server state
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

export function usePosts() {
  return useQuery({
    queryKey: ['posts'],
    queryFn: () => fetch('/api/posts').then(r => r.json()),
  });
}

export function useUpdatePost() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: (data) => fetch(`/api/posts/${data.id}`, {
      method: 'PUT',
      body: JSON.stringify(data),
    }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['posts'] }); // Auto refresh
    },
  });
}

// ✅ Use Zustand for UI/client state only
export const useUIStore = create((set) => ({
  sidebarOpen: false,
  theme: 'light',
  toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
}));

Anti-Pattern 2: Not Memoizing Selectors

// ❌ Problem: Creating new selectors on every render
const completedTodos = useStore((state) =>
  state.todos.filter(todo => todo.completed) // New array every time
);

// ✅ Solution: Use shallow comparison
import { shallow } from 'zustand/shallow';
const completedTodos = useStore(
  (state) => state.todos.filter(todo => todo.completed),
  shallow
);

Why it’s wrong: Component re-renders even when filtered result is the same.


Anti-Pattern 3: Overusing Global State

// ❌ Problem: Form state in global store
export const useFormStore = create((set) => ({
  email: '', password: '',
  setEmail: (email) => set({ email }),
}));

// ✅ Solution: Use local state for component-specific data
export function LoginForm() {
  const [email, setEmail] = useState('');
  return <input value={email} onChange={(e) => setEmail(e.target.value)} />;
}

Why it’s wrong: Unnecessary global state; harder to test; overkill for component-scoped data.

For more anti-patterns and solutions, see references/patterns.md.


What This Skill Covers

  • Store creation with TypeScript
  • React integration with hooks
  • Middleware (persist, devtools, immer)
  • Async actions and selectors

For middleware, advanced patterns, and testing, see references/.


Basic Store

import { create } from 'zustand';

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

export 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 in Component

'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}>+</button>
    </div>
  );
}

For async actions, selectors, and advanced patterns, see references/patterns.md.


Quick Reference

// Create store
const useStore = create<Store>((set, get) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}));

// Use in component
const count = useStore((state) => state.count);

// Get state outside React
const count = useStore.getState().count;

// Set state outside React
useStore.setState({ count: 5 });

// Subscribe to changes
const unsubscribe = useStore.subscribe(
  (state) => state.count,
  (count) => console.log(count)
);

Learn More


External References


Maintained by dsmj-ai-toolkit