plugins/react-master/skills/react-state-management/SKILL.md
Complete React state management system. PROACTIVELY activate for: (1) Context API patterns and optimization, (2) Zustand store setup and usage, (3) Jotai atomic state, (4) TanStack Query (React Query) for server state, (5) SWR data fetching, (6) useState vs useReducer decisions, (7) State normalization, (8) Avoiding prop drilling. Provides: Store configuration, context optimization, server state caching, optimistic updates, infinite queries. Ensures scalable state architecture with proper tool selection.
npx skillsauth add JosiahSiegel/claude-plugin-marketplace react-state-managementInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
| Library | Best For | Install |
|---------|----------|---------|
| Context | Small apps, themes | Built-in |
| Zustand | Simple global state | npm i zustand |
| Jotai | Atomic/granular state | npm i jotai |
| TanStack Query | Server state/caching | npm i @tanstack/react-query |
| SWR | Data fetching | npm i swr |
| Scenario | Recommended |
|----------|-------------|
| Simple local state | useState |
| Complex local state | useReducer |
| Shared state (small app) | Context + useReducer |
| Shared state (large app) | Zustand or Jotai |
| Server state | TanStack Query or SWR |
Use for state management decisions:
For React hooks basics: see react-hooks-complete
'use client';
import { useState } from 'react';
function ShoppingCart() {
const [items, setItems] = useState<CartItem[]>([]);
const [isOpen, setIsOpen] = useState(false);
const addItem = (product: Product) => {
setItems((prev) => {
const existing = prev.find((item) => item.id === product.id);
if (existing) {
return prev.map((item) =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
}
return [...prev, { ...product, quantity: 1 }];
});
};
const total = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>
Cart ({items.length}) - ${total.toFixed(2)}
</button>
{isOpen && <CartDropdown items={items} />}
</div>
);
}
'use client';
import { useReducer, Dispatch, createContext, useContext } from 'react';
// Types
interface CartState {
items: CartItem[];
isLoading: boolean;
error: string | null;
}
type CartAction =
| { type: 'ADD_ITEM'; payload: Product }
| { type: 'REMOVE_ITEM'; payload: string }
| { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
| { type: 'CLEAR_CART' }
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_ERROR'; payload: string };
// Reducer
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'ADD_ITEM': {
const existing = state.items.find(
(item) => item.id === action.payload.id
);
if (existing) {
return {
...state,
items: state.items.map((item) =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
),
};
}
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }],
};
}
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter((item) => item.id !== action.payload),
};
case 'UPDATE_QUANTITY':
return {
...state,
items: state.items.map((item) =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
),
};
case 'CLEAR_CART':
return { ...state, items: [] };
case 'SET_LOADING':
return { ...state, isLoading: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload };
default:
return state;
}
}
// Context
const CartContext = createContext<{
state: CartState;
dispatch: Dispatch<CartAction>;
} | null>(null);
// Provider
export function CartProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(cartReducer, {
items: [],
isLoading: false,
error: null,
});
return (
<CartContext.Provider value={{ state, dispatch }}>
{children}
</CartContext.Provider>
);
}
// Hook
export function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within CartProvider');
}
return context;
}
import { createContext, useContext, useState, ReactNode } from 'react';
// Theme context
interface Theme {
colors: { primary: string; secondary: string; background: string };
spacing: { sm: number; md: number; lg: number };
}
interface ThemeContextType {
theme: Theme;
setTheme: (theme: Theme) => void;
toggleDarkMode: () => void;
isDark: boolean;
}
const ThemeContext = createContext<ThemeContextType | null>(null);
const lightTheme: Theme = {
colors: { primary: '#3b82f6', secondary: '#8b5cf6', background: '#ffffff' },
spacing: { sm: 8, md: 16, lg: 24 },
};
const darkTheme: Theme = {
colors: { primary: '#60a5fa', secondary: '#a78bfa', background: '#1f2937' },
spacing: { sm: 8, md: 16, lg: 24 },
};
export function ThemeProvider({ children }: { children: ReactNode }) {
const [isDark, setIsDark] = useState(false);
const [theme, setTheme] = useState<Theme>(lightTheme);
const toggleDarkMode = () => {
setIsDark((prev) => !prev);
setTheme(isDark ? lightTheme : darkTheme);
};
return (
<ThemeContext.Provider value={{ theme, setTheme, toggleDarkMode, isDark }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
import { createContext, useContext, useMemo, useCallback, useState } from 'react';
// Split context to prevent unnecessary re-renders
const UserContext = createContext<User | null>(null);
const UserActionsContext = createContext<{
login: (email: string, password: string) => Promise<void>;
logout: () => void;
updateProfile: (data: Partial<User>) => Promise<void>;
} | null>(null);
export function UserProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = useCallback(async (email: string, password: string) => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
const userData = await response.json();
setUser(userData);
}, []);
const logout = useCallback(() => {
setUser(null);
}, []);
const updateProfile = useCallback(async (data: Partial<User>) => {
const response = await fetch('/api/profile', {
method: 'PATCH',
body: JSON.stringify(data),
});
const updated = await response.json();
setUser(updated);
}, []);
// Memoize actions object
const actions = useMemo(
() => ({ login, logout, updateProfile }),
[login, logout, updateProfile]
);
return (
<UserContext.Provider value={user}>
<UserActionsContext.Provider value={actions}>
{children}
</UserActionsContext.Provider>
</UserContext.Provider>
);
}
// Separate hooks for data and actions
export function useUser() {
return useContext(UserContext);
}
export function useUserActions() {
const context = useContext(UserActionsContext);
if (!context) {
throw new Error('useUserActions must be used within UserProvider');
}
return context;
}
import { create } from 'zustand';
import { persist, devtools } from 'zustand/middleware';
interface CartStore {
items: CartItem[];
addItem: (product: Product) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
clearCart: () => void;
total: () => number;
}
export const useCartStore = create<CartStore>()(
devtools(
persist(
(set, get) => ({
items: [],
addItem: (product) =>
set((state) => {
const existing = state.items.find((item) => item.id === product.id);
if (existing) {
return {
items: state.items.map((item) =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
),
};
}
return { items: [...state.items, { ...product, quantity: 1 }] };
}),
removeItem: (id) =>
set((state) => ({
items: state.items.filter((item) => item.id !== id),
})),
updateQuantity: (id, quantity) =>
set((state) => ({
items: state.items.map((item) =>
item.id === id ? { ...item, quantity } : item
),
})),
clearCart: () => set({ items: [] }),
total: () =>
get().items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
),
}),
{ name: 'cart-storage' }
)
)
);
// Usage in component
function CartButton() {
const items = useCartStore((state) => state.items);
const total = useCartStore((state) => state.total());
return (
<button>
Cart ({items.length}) - ${total.toFixed(2)}
</button>
);
}
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
interface TodoStore {
todos: Todo[];
addTodo: (text: string) => void;
toggleTodo: (id: string) => void;
deleteTodo: (id: string) => void;
}
export const useTodoStore = create<TodoStore>()(
immer((set) => ({
todos: [],
addTodo: (text) =>
set((state) => {
state.todos.push({
id: crypto.randomUUID(),
text,
completed: false,
});
}),
toggleTodo: (id) =>
set((state) => {
const todo = state.todos.find((t) => t.id === id);
if (todo) {
todo.completed = !todo.completed;
}
}),
deleteTodo: (id) =>
set((state) => {
const index = state.todos.findIndex((t) => t.id === id);
if (index !== -1) {
state.todos.splice(index, 1);
}
}),
}))
);
Detailed patterns for Jotai atom-based state and TanStack Query server-state management — atoms, derived atoms, async atoms, query clients, mutations, invalidation, optimistic updates, and cache tuning — live in references/jotai-and-tanstack-query.md. Load that reference when choosing atom composition or production-grade server-state caching.
import useSWR, { SWRConfig } from 'swr';
const fetcher = (url: string) => fetch(url).then((res) => res.json());
function App() {
return (
<SWRConfig
value={{
fetcher,
refreshInterval: 0,
revalidateOnFocus: true,
dedupingInterval: 2000,
}}
>
<Dashboard />
</SWRConfig>
);
}
function Dashboard() {
const { data, error, isLoading, mutate } = useSWR('/api/dashboard');
if (error) return <div>Failed to load</div>;
if (isLoading) return <div>Loading...</div>;
return (
<div>
<h1>Dashboard</h1>
<p>Total Users: {data.totalUsers}</p>
<button onClick={() => mutate()}>Refresh</button>
</div>
);
}
import useSWRMutation from 'swr/mutation';
async function createUser(url: string, { arg }: { arg: CreateUserInput }) {
const res = await fetch(url, {
method: 'POST',
body: JSON.stringify(arg),
});
return res.json();
}
function CreateUserForm() {
const { trigger, isMutating } = useSWRMutation('/api/users', createUser);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
await trigger({
name: formData.get('name') as string,
email: formData.get('email') as string,
});
};
return (
<form onSubmit={handleSubmit}>
<input name="name" required />
<input name="email" type="email" required />
<button disabled={isMutating}>
{isMutating ? 'Creating...' : 'Create'}
</button>
</form>
);
}
| Scenario | Recommended | |----------|-------------| | Simple local state | useState | | Complex local state | useReducer | | Shared state (small app) | Context + useReducer | | Shared state (large app) | Zustand or Jotai | | Server state | TanStack Query or SWR |
// Instead of passing props through many levels
<Parent user={user}>
<Child user={user}>
<GrandChild user={user} />
</Child>
</Parent>
// Use context or state management
<UserProvider>
<Parent>
<Child>
<GrandChild /> {/* Access user via useUser() */}
</Child>
</Parent>
</UserProvider>
// Instead of nested objects
const badState = {
posts: [
{ id: 1, title: 'Post 1', author: { id: 1, name: 'Alice' } },
{ id: 2, title: 'Post 2', author: { id: 1, name: 'Alice' } },
],
};
// Use normalized structure
const goodState = {
posts: {
byId: { 1: { id: 1, title: 'Post 1', authorId: 1 } },
allIds: [1, 2],
},
authors: {
byId: { 1: { id: 1, name: 'Alice' } },
allIds: [1],
},
};
For detailed patterns and advanced use cases, see:
references/zustand-patterns.md - Advanced Zustand patterns including slices, middleware, and testingdevelopment
Use for Clerk sessions, tokens, webhooks, orgs, and security. PROACTIVELY activate for session tokens, JWT templates, getToken(), custom claims, pending sessions, multi-session UX, organizations, roles, permissions, system vs custom permissions, features/plans, MFA/passkeys/password policy/bot protection, Clerk webhooks, Svix signatures, verifyWebhook(), user/org sync, retries/replays, environment variables, custom domains, secret rotation, logs, and auth security reviews. Provides token semantics, webhook idempotency, authorization defaults, and hardening checklist.
tools
Use for Clerk in Next.js. PROACTIVELY activate for @clerk/nextjs setup, App Router auth()/currentUser(), clerkMiddleware(), proxy.ts/middleware.ts, createRouteMatcher(), protected pages/layouts/Route Handlers/Server Actions/API routes/tRPC, auth.protect() role/permission/token checks, ClerkProvider placement, server-only clerkClient, Link prefetch, redirects, 401/404 auth failures, custom domains, __clerk proxy paths, and deployment gotchas. Provides file patterns, server/client boundary rules, matcher templates, and production checks.
development
Use for Clerk frontend auth flows. PROACTIVELY activate for React, JavaScript, Vue, Nuxt, Astro, Expo, React Router, TanStack React Start, or SPA setup; ClerkProvider and publishable-key wiring; SignIn/SignUp/UserButton/UserProfile/OrganizationSwitcher; custom useUser/useAuth/useClerk/useSignIn/useSignUp/useSession/useOrganization flows; multi-session UX; cross-origin getToken() fetches; loading states, redirects, routing, CORS/cookies, or hydration bugs. Provides SDK selection, UI patterns, token-fetch templates, and frontend gotchas.
development
Use for Clerk dev/prod readiness, deployment, and multi-language implementation planning. PROACTIVELY activate for environment variables, pk_test/sk_test vs pk_live/sk_live, local dev, preview/staging/prod instances, domains/DNS, redirects, OAuth credentials, custom domains/proxy, authorizedParties, CSP, CORS/cookies, webhooks/tunnels, Vercel/Netlify/Cloudflare/API gateways, monitoring/troubleshooting, and backends in Node/Express/Fastify, Python/FastAPI/Django/Flask, Go, Ruby/Rails, Java/Spring, .NET, PHP/Laravel. Provides checklists, rollout plans, and language-portable patterns.