
state-management
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.
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.
State Management Skill
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
You Might Also Like
Related Skills

cache-components
Expert guidance for Next.js Cache Components and Partial Prerendering (PPR). **PROACTIVE ACTIVATION**: Use this skill automatically when working in Next.js projects that have `cacheComponents: true` in their next.config.ts/next.config.js. When this config is detected, proactively apply Cache Components patterns and best practices to all React Server Component implementations. **DETECTION**: At the start of a session in a Next.js project, check for `cacheComponents: true` in next.config. If enabled, this skill's patterns should guide all component authoring, data fetching, and caching decisions. **USE CASES**: Implementing 'use cache' directive, configuring cache lifetimes with cacheLife(), tagging cached data with cacheTag(), invalidating caches with updateTag()/revalidateTag(), optimizing static vs dynamic content boundaries, debugging cache issues, and reviewing Cache Component implementations.
vercel
component-refactoring
Refactor high-complexity React components in Dify frontend. Use when `pnpm analyze-component --json` shows complexity > 50 or lineCount > 300, when the user asks for code splitting, hook extraction, or complexity reduction, or when `pnpm analyze-component` warns to refactor before testing; avoid for simple/well-structured components, third-party wrappers, or when the user explicitly wants testing without refactoring.
langgenius
web-artifacts-builder
Suite of tools for creating elaborate, multi-component claude.ai HTML artifacts using modern frontend web technologies (React, Tailwind CSS, shadcn/ui). Use for complex artifacts requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX artifacts.
anthropics
frontend-design
Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.
anthropics
react-modernization
Upgrade React applications to latest versions, migrate from class components to hooks, and adopt concurrent features. Use when modernizing React codebases, migrating to React Hooks, or upgrading to latest React versions.
wshobson
tailwind-design-system
Build scalable design systems with Tailwind CSS v4, design tokens, component libraries, and responsive patterns. Use when creating component libraries, implementing design systems, or standardizing UI patterns.
wshobson