Squire state-management
git clone https://github.com/eddiebelaval/squire
skills/state-management/skill.mdname: state-management description: Expert guide for React state management with Zustand, Context, and modern patterns. Use when managing global state, forms, complex UI state, or optimizing re-renders. slug: state-management category: operations complexity: complex version: "1.0.0" author: "id8Labs" triggers:
- "state-management"
- "state management" tags:
- development
- tool-factory-retrofitted---
State Management Skill
Core Workflows
Workflow 1: Primary Action
- Analyze the input and context
- Validate prerequisites are met
- Execute the core operation
- Verify the output meets expectations
- Report results
Overview
This skill helps you choose and implement the right state management solution for your React/Next.js application. From local state to global stores, this covers all the patterns you need.
State Management Hierarchy
1. Local State (useState)
Use for component-specific state that doesn't need to be shared.
'use client' import { useState } from 'react' export function Counter() { const [count, setCount] = useState(0) return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ) }
2. URL State (useSearchParams)
Use for state that should be shareable via URL.
'use client' import { useSearchParams, useRouter } from 'next/navigation' export function SearchFilter() { const router = useRouter() const searchParams = useSearchParams() const category = searchParams.get('category') || 'all' const setCategory = (cat: string) => { const params = new URLSearchParams(searchParams) params.set('category', cat) router.push(`?${params.toString()}`) } return ( <select value={category} onChange={(e) => setCategory(e.target.value)}> <option value="all">All</option> <option value="tech">Tech</option> <option value="design">Design</option> </select> ) }
3. Server State (Server Components)
Use for data from your database/API that doesn't change client-side.
// Server Component (no 'use client') export default async function UserProfile({ userId }: { userId: string }) { const user = await db.users.findUnique({ where: { id: userId } }) return ( <div> <h1>{user.name}</h1> <p>{user.email}</p> </div> ) }
4. Context (React Context)
Use for simple global state (theme, user, settings) within a component tree.
'use client' import { createContext, useContext, useState, ReactNode } from 'react' type Theme = 'light' | 'dark' const ThemeContext = createContext<{ theme: Theme setTheme: (theme: Theme) => void }>({ theme: 'light', setTheme: () => {} }) export function ThemeProvider({ children }: { children: ReactNode }) { const [theme, setTheme] = useState<Theme>('light') return ( <ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider> ) } export function useTheme() { return useContext(ThemeContext) } // Usage in component function ThemeToggle() { const { theme, setTheme } = useTheme() return ( <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}> Current: {theme} </button> ) }
5. Zustand (Global Store)
Use for complex global state that needs to be accessed across many components.
Basic Store:
// stores/user-store.ts import { create } from 'zustand' interface UserState { user: User | null isLoading: boolean setUser: (user: User | null) => void fetchUser: (id: string) => Promise<void> logout: () => void } export const useUserStore = create<UserState>((set) => ({ user: null, isLoading: false, setUser: (user) => set({ user }), fetchUser: async (id) => { set({ isLoading: true }) try { const response = await fetch(`/api/users/${id}`) const user = await response.json() set({ user, isLoading: false }) } catch (error) { set({ isLoading: false }) } }, logout: () => set({ user: null }) })) // Usage in component 'use client' import { useUserStore } from '@/stores/user-store' export function UserProfile() { const { user, isLoading, fetchUser } = useUserStore() return ( <div> {isLoading ? <p>Loading...</p> : <p>{user?.name}</p>} </div> ) }
Persisted Store (localStorage):
import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' interface PreferencesState { theme: 'light' | 'dark' language: string setTheme: (theme: 'light' | 'dark') => void setLanguage: (lang: string) => void } export const usePreferencesStore = create<PreferencesState>()( persist( (set) => ({ theme: 'light', language: 'en', setTheme: (theme) => set({ theme }), setLanguage: (language) => set({ language }) }), { name: 'preferences-storage', storage: createJSONStorage(() => localStorage) } ) )
Sliced Stores (Organized):
// stores/slices/auth-slice.ts export const createAuthSlice = (set, get) => ({ token: null, isAuthenticated: false, login: async (credentials) => { const token = await loginAPI(credentials) set({ token, isAuthenticated: true }) }, logout: () => set({ token: null, isAuthenticated: false }) }) // stores/slices/cart-slice.ts export const createCartSlice = (set, get) => ({ items: [], addItem: (item) => set((state) => ({ items: [...state.items, item] })), removeItem: (id) => set((state) => ({ items: state.items.filter(item => item.id !== id) })), total: () => { const items = get().items return items.reduce((sum, item) => sum + item.price, 0) } }) // stores/app-store.ts import { create } from 'zustand' import { createAuthSlice } from './slices/auth-slice' import { createCartSlice } from './slices/cart-slice' export const useAppStore = create((...a) => ({ ...createAuthSlice(...a), ...createCartSlice(...a) }))
Immer Middleware (Immutable Updates):
import { create } from 'zustand' import { immer } from 'zustand/middleware/immer' interface TodoState { todos: Todo[] addTodo: (text: string) => void toggleTodo: (id: string) => void updateTodo: (id: string, text: string) => void } export const useTodoStore = create<TodoState>()( immer((set) => ({ todos: [], addTodo: (text) => set((state) => { 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 }), updateTodo: (id, text) => set((state) => { const todo = state.todos.find((t) => t.id === id) if (todo) todo.text = text }) })) )
Form State Management
React Hook Form (Recommended)
'use client' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { z } from 'zod' const schema = z.object({ name: z.string().min(2), email: z.string().email(), age: z.number().min(18) }) type FormData = z.infer<typeof schema> export function UserForm() { const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<FormData>({ resolver: zodResolver(schema) }) const onSubmit = async (data: FormData) => { await fetch('/api/users', { method: 'POST', body: JSON.stringify(data) }) } return ( <form onSubmit={handleSubmit(onSubmit)}> <input {...register('name')} /> {errors.name && <p>{errors.name.message}</p>} <input {...register('email')} type="email" /> {errors.email && <p>{errors.email.message}</p>} <input {...register('age', { valueAsNumber: true })} type="number" /> {errors.age && <p>{errors.age.message}</p>} <button type="submit" disabled={isSubmitting}> Submit </button> </form> ) }
Performance Optimization
Selective Store Subscription
// ❌ Bad - Component re-renders for any store change function BadExample() { const store = useUserStore() return <div>{store.user?.name}</div> } // ✅ Good - Only re-renders when user.name changes function GoodExample() { const userName = useUserStore((state) => state.user?.name) return <div>{userName}</div> } // ✅ Better - Use shallow comparison for multiple values import { shallow } from 'zustand/shallow' function BetterExample() { const { user, isLoading } = useUserStore( (state) => ({ user: state.user, isLoading: state.isLoading }), shallow ) return <div>{isLoading ? 'Loading...' : user?.name}</div> }
React.memo for Components
import { memo } from 'react' const ExpensiveComponent = memo(function ExpensiveComponent({ data }: { data: Data }) { // This only re-renders when data changes return <div>{/* Expensive rendering */}</div> })
useCallback for Functions
'use client' import { useCallback } from 'react' export function Parent() { const [count, setCount] = useState(0) // ❌ Bad - New function on every render const handleClick = () => { console.log('clicked') } // ✅ Good - Stable function reference const handleClickMemoized = useCallback(() => { console.log('clicked') }, []) return <Child onClick={handleClickMemoized} /> }
Advanced Patterns
Computed Values
import { create } from 'zustand' interface CartState { items: CartItem[] // Computed value - always fresh total: () => number subtotal: () => number tax: () => number } export const useCartStore = create<CartState>((set, get) => ({ items: [], total: () => { const items = get().items return items.reduce((sum, item) => sum + item.price * item.quantity, 0) }, subtotal: () => { return get().total() }, tax: () => { return get().subtotal() * 0.1 } })) // Usage - recalculates on every call function Cart() { const total = useCartStore((state) => state.total()) return <div>Total: ${total}</div> }
Async Actions
interface DataState { data: Data[] isLoading: boolean error: string | null fetchData: () => Promise<void> refetch: () => Promise<void> } export const useDataStore = create<DataState>((set, get) => ({ data: [], isLoading: false, error: null, fetchData: async () => { set({ isLoading: true, error: null }) try { const response = await fetch('/api/data') const data = await response.json() set({ data, isLoading: false }) } catch (error) { set({ error: error.message, isLoading: false }) } }, refetch: async () => { await get().fetchData() } }))
Middleware Composition
import { create } from 'zustand' import { persist } from 'zustand/middleware' import { immer } from 'zustand/middleware/immer' import { devtools } from 'zustand/middleware' export const useStore = create<State>()( devtools( persist( immer((set) => ({ // Your store })), { name: 'my-store' } ) ) )
Decision Tree
Do you need state? ├─ Only in this component? → useState ├─ Pass to 2-3 child components? → props ├─ Shareable URL? → useSearchParams ├─ From database/API? │ ├─ Static/rarely changes? → Server Component │ └─ Dynamic/frequent updates? → React Query/SWR ├─ Simple global (theme, user)? → Context └─ Complex global/many subscribers? → Zustand
Best Practices Checklist
- Start with local state (useState)
- Use URL state for shareable filters/tabs
- Prefer Server Components for DB data
- Use Context for simple global state
- Use Zustand for complex global state
- Select only needed store values
- Use shallow comparison for multiple values
- Persist user preferences to localStorage
- Handle loading and error states
- Use React Hook Form for complex forms
- Memoize expensive computations
- Use TypeScript for type safety
Common Mistakes to Avoid
- Over-using global state - Start local, move to global when needed
- Not selecting store values - Always use selectors to prevent unnecessary re-renders
- Storing derived values - Compute on-the-fly instead
- Not handling loading states - Always show feedback to users
- Putting everything in Zustand - Use the right tool for the job
When to Use This Skill
Invoke this skill when:
- Choosing a state management solution
- Setting up Zustand stores
- Optimizing component re-renders
- Managing form state
- Implementing global user settings
- Debugging state-related issues
- Migrating from Redux to Zustand
- Setting up persisted state