zustand

Zustand - Minimalist State Management

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" with this command: npx skills add dsantiagomj/dsmj-ai-toolkit/dsantiagomj-dsmj-ai-toolkit-zustand

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

  • Zustand Documentation

  • Zustand GitHub

Maintained by dsmj-ai-toolkit

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

patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

performance

No summary provided by upstream source.

Repository SourceNeeds Review
General

caching

No summary provided by upstream source.

Repository SourceNeeds Review
General

vercel-ai-sdk

No summary provided by upstream source.

Repository SourceNeeds Review