Squire state-management

install
source · Clone the upstream repo
git clone https://github.com/eddiebelaval/squire
manifest: skills/state-management/skill.md
source content

name: 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

  1. Analyze the input and context
  2. Validate prerequisites are met
  3. Execute the core operation
  4. Verify the output meets expectations
  5. 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

  1. Over-using global state - Start local, move to global when needed
  2. Not selecting store values - Always use selectors to prevent unnecessary re-renders
  3. Storing derived values - Compute on-the-fly instead
  4. Not handling loading states - Always show feedback to users
  5. 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