zustand
npx skills add https://github.com/dsantiagomj/dsmj-ai-toolkit --skill zustand
Agent 安装分布
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
- Middleware & Advanced: references/middleware.md – Persist, devtools, immer, slices pattern
- Performance & Testing: references/performance.md – Optimization, SSR, testing patterns
External References
Maintained by dsmj-ai-toolkit