| name | frontend-react-state-management |
| description | Choosing state management solution |
React State Management
Scope: Context, Zustand, Jotai, Redux Toolkit, when to use each, state patterns
Lines: ~300
Last Updated: 2025-10-18
When to Use This Skill
Activate this skill when:
- Choosing state management solution
- Managing global application state
- Sharing state across components
- Optimizing state updates and re-renders
- Migrating between state libraries
- Debugging state management issues
Core Concepts
State Types
Local State - Component-specific
const [count, setCount] = useState(0);
Shared State - Multiple components
const theme = useContext(ThemeContext);
Global State - Application-wide
const user = useStore(state => state.user);
Server State - Data from API
const { data: posts } = useSWR('/api/posts');
When to Use Each Solution
Decision Tree
State needed by single component? → useState
↓
State needed by 2-3 nearby components? → Lift state up or useContext
↓
State needed across app (5+ components)? → Zustand or Jotai
↓
Complex state logic (reducers, middleware)? → Redux Toolkit
↓
Server data (API, caching, revalidation)? → SWR or React Query
Comparison Matrix
| Library | Complexity | Bundle Size | DevTools | Best For |
|---|
| Context | Low | 0kb (built-in) | No | Small apps, theming |
| Zustand | Low | 1.2kb | Yes | Most apps, simple global state |
| Jotai | Medium | 3kb | Yes | Atomic state, derived values |
| Redux Toolkit | High | 8kb | Excellent | Large apps, complex logic |
React Context
Basic Context Pattern
import { createContext, useContext, useState } from 'react';
type Theme = 'light' | 'dark';
const ThemeContext = createContext<{
theme: Theme;
setTheme: (theme: Theme) => void;
} | null>(null);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme must be used within ThemeProvider');
return context;
}
function App() {
(
);
}
() {
{ theme, setTheme } = ();
(
);
}
Context Performance Optimization
const UserContext = createContext<User | null>(null);
const UserActionsContext = createContext<{
login: (email: string, password: string) => void;
logout: () => void;
} | null>(null);
function UserProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const actions = useMemo(() => ({
login: async (email: string, password: string) => {
const user = await loginAPI(email, password);
setUser(user);
},
logout: () => setUser(null),
}), []);
return (
<UserContext.Provider value={user}>
{children}
);
}
() {
user = ();
;
}
() {
{ logout } = ()!;
;
}
When to use Context:
- Simple state (theme, locale, auth user)
- 2-5 consumers
- Infrequent updates
- Small apps
When NOT to use Context:
- Frequent updates (causes re-renders)
- Many consumers (performance issues)
- Complex state logic
Zustand
Basic Store
import { create } from 'zustand';
interface Todo {
id: string;
title: string;
completed: boolean;
}
interface TodoStore {
todos: Todo[];
addTodo: (title: string) => void;
toggleTodo: (id: string) => void;
deleteTodo: (id: string) => void;
}
export const useTodoStore = create<TodoStore>((set) => ({
todos: [],
addTodo: (title) =>
set((state) => ({
todos: [...state.todos, { id: crypto.randomUUID(), title, completed: false }],
})),
toggleTodo: (id) =>
set( ({
: state..(
todo. === id ? { ...todo, : !todo. } : todo
),
})),
:
( ({
: state..( todo. !== id),
})),
}));
() {
todos = ( state.);
toggleTodo = ( state.);
(
);
}
() {
addTodo = ( state.);
(
);
}
Selector optimization - Component only re-renders when selected value changes:
const store = useTodoStore();
const todos = useTodoStore((state) => state.todos);
Zustand with Immer (Immutability)
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
interface Store {
nested: {
deeply: {
value: number;
};
};
increment: () => void;
}
const useStore = create<Store>()(
immer((set) => ({
nested: { deeply: { value: 0 } },
increment: () =>
set((state) => {
state.nested.deeply.value += 1;
}),
}))
);
Zustand with Persistence
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface AuthStore {
user: User | null;
login: (user: User) => void;
logout: () => void;
}
export const useAuthStore = create<AuthStore>()(
persist(
(set) => ({
user: null,
login: (user) => set({ user }),
logout: () => set({ user: null }),
}),
{
name: 'auth-storage',
}
)
);
When to use Zustand:
- Most applications
- Simple global state
- Good performance (fine-grained subscriptions)
- Minimal boilerplate
Jotai
Atoms (Atomic State)
import { atom } from 'jotai';
export const countAtom = atom(0);
export const userAtom = atom<User | null>(null);
export const doubleCountAtom = atom((get) => get(countAtom) * 2);
export const incrementCountAtom = atom(
(get) => get(countAtom),
(get, set) => set(countAtom, get(countAtom) + 1)
);
export const postsAtom = atom(async () => {
const res = await fetch('/api/posts');
return res.json();
});
function Counter() {
const [count, setCount] = useAtom(countAtom);
const doubleCount = (doubleCountAtom);
(
);
}
Atom Families (Dynamic Atoms)
import { atomFamily } from 'jotai/utils';
const todoAtomFamily = atomFamily((id: string) =>
atom<Todo>({ id, title: '', completed: false })
);
function TodoItem({ id }: { id: string }) {
const [todo, setTodo] = useAtom(todoAtomFamily(id));
return (
<div>
<input
value={todo.title}
onChange={(e) => setTodo({ ...todo, title: e.target.value })}
/>
</div>
);
}
Jotai with Storage
import { atomWithStorage } from 'jotai/utils';
export const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light');
When to use Jotai:
- Bottom-up state (compose small pieces)
- Derived/computed state
- Atomic updates (fine-grained reactivity)
- TypeScript-first
Redux Toolkit
Store Setup
import { configureStore } from '@reduxjs/toolkit';
import todosReducer from './todosSlice';
import userReducer from './userSlice';
export const store = configureStore({
reducer: {
todos: todosReducer,
user: userReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Slice Definition
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface Todo {
id: string;
title: string;
completed: boolean;
}
interface TodosState {
items: Todo[];
loading: boolean;
}
const initialState: TodosState = {
items: [],
loading: false,
};
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
addTodo: (state, action: PayloadAction<string>) => {
state.items.push({
id: crypto.randomUUID(),
title: action.payload,
completed: false,
});
},
toggleTodo: (state, action: PayloadAction<string>) => {
const todo = state.items.find( t. === action.);
(todo) todo. = !todo.;
},
: {
state. = state..( t. !== action.);
},
},
});
{ addTodo, toggleTodo, deleteTodo } = todosSlice.;
todosSlice.;
Async Thunks
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const fetchUser = createAsyncThunk('user/fetch', async (userId: string) => {
const res = await fetch(`/api/users/${userId}`);
return res.json();
});
interface UserState {
user: User | null;
loading: boolean;
error: string | null;
}
const userSlice = createSlice({
name: 'user',
initialState: { user: null, loading: false, error: null } as UserState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => {
state.loading = true;
state.error = null;
})
.(fetchUser., {
state. = ;
state. = action.;
})
.(fetchUser., {
state. = ;
state. = action.. || ;
});
},
});
userSlice.;
Hooks
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from '../store/store';
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
function TodoList() {
const todos = useAppSelector((state) => state.todos.items);
const dispatch = useAppDispatch();
return (
<div>
{todos.map((todo) => (
<div key={todo.id} onClick={() => dispatch(toggleTodo(todo.id))}>
{todo.title}
</div>
))}
<button onClick={() => dispatch(addTodo('New Todo'))}>Add</button>
</div>
);
}
When to use Redux Toolkit:
- Large, complex applications
- Time-travel debugging needed
- Team familiar with Redux
- Extensive middleware needed
- Strict state management patterns
State Patterns
Optimistic Updates
const useStore = create<Store>((set) => ({
todos: [],
toggleTodo: async (id: string) => {
set((state) => ({
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
),
}));
try {
await fetch(`/api/todos/${id}/toggle`, { method: 'POST' });
} catch (error) {
set((state) => ({
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
),
}));
}
},
}));
Loading States
interface Store {
data: Data[];
loading: boolean;
error: string | null;
fetch: () => Promise<void>;
}
const useStore = create<Store>((set) => ({
data: [],
loading: false,
error: null,
fetch: async () => {
set({ loading: true, error: null });
try {
const res = await fetch('/api/data');
const data = await res.json();
set({ data, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
}));
Derived State
const todosAtom = atom<Todo[]>([]);
const completedTodosAtom = atom((get) =>
get(todosAtom).filter((todo) => todo.completed)
);
const activeTodosAtom = atom((get) =>
get(todosAtom).filter((todo) => !todo.completed)
);
const statsAtom = atom((get) => {
const todos = get(todosAtom);
return {
total: todos.length,
completed: get(completedTodosAtom).length,
active: get(activeTodosAtom).length,
};
});
const useStore = create<Store>((set) => ({
todos: [],
get completedTodos() {
return this.todos.filter((todo) => todo.completed);
},
}));
Migration Patterns
Context → Zustand
const TodoContext = createContext<{
todos: Todo[];
addTodo: (title: string) => void;
} | null>(null);
function TodoProvider({ children }) {
const [todos, setTodos] = useState<Todo[]>([]);
const addTodo = (title: string) => setTodos([...todos, { title }]);
return <TodoContext.Provider value={{ todos, addTodo }}>{children}</TodoContext.Provider>;
}
const useTodoStore = create<Store>((set) => ({
todos: [],
addTodo: (title) => set((state) => ({ todos: [...state.todos, { title }] })),
}));
const todos = useTodoStore((state) => state.todos);
addTodo = ( state.);
Quick Reference
Choosing State Management
Local component state → useState
2-3 components nearby → Lift state or Context
App-wide, simple → Zustand
Atomic, derived state → Jotai
Complex, large app → Redux Toolkit
Server data → SWR/React Query
Performance Tips
const store = useStore();
const todos = useStore((state) => state.todos);
const addTodo = useStore((state) => state.addTodo);
const deleteTodo = useStore((state) => state.deleteTodo);
const completedCount = useStore(
(state) => state.todos.filter((t) => t.completed).length
);
Common Anti-Patterns
❌ Storing server state in global store: Use SWR/React Query instead
✅ Keep server state separate from client state
❌ Too much global state: Most state should be local
✅ Only globalize what needs to be shared
❌ Not splitting context: One context causes all consumers to re-render
✅ Split into multiple contexts (data + actions)
❌ Derived state not memoized: Recalculates on every render
✅ Use useMemo, Jotai derived atoms, or Zustand getters
Level 3: Resources
Comprehensive Reference
- REFERENCE.md: Complete React state management guide (900+ lines)
- All state categories (local, shared, server, URL, form)
- React built-in hooks (useState, useReducer, useContext)
- External libraries (Zustand, Jotai, Redux Toolkit, MobX, Recoil)
- Server state (TanStack Query, SWR)
- URL state management
- Form state (React Hook Form, Formik)
- Decision trees and comparison matrices
- Performance optimization patterns
- State colocation strategies
- Immutability patterns
- Common patterns and anti-patterns
Executable Scripts
Located in resources/scripts/:
-
analyze_state.py - Analyze React component state patterns
- Detects state management approaches (useState, useReducer, Context)
- Identifies prop drilling issues
- Finds stale closure risks
- Detects unnecessary derived state
- Suggests optimization opportunities
- Reports state complexity metrics
./analyze_state.py ./src
./analyze_state.py ./src --json
-
benchmark_renders.js - Benchmark render performance
- Simulates component trees with different state patterns
- Measures render count and timing
- Compares useState, Context, Zustand, Jotai
- Reports performance metrics
./benchmark_renders.js
./benchmark_renders.js --depth 20 --updates 500
./benchmark_renders.js --json
-
detect_unnecessary_renders.js - Detect unnecessary re-renders
- Identifies components that should use React.memo
- Finds props that change on every render (object/array literals)
- Detects missing dependencies in useMemo/useCallback
- Finds unstable callback props
- Reports context overuse
./detect_unnecessary_renders.js ./src
./detect_unnecessary_renders.js ./components --json
TypeScript Examples
Located in resources/examples/typescript/:
-
zustand-store.ts - Complete Zustand patterns
- Basic store with actions
- Async operations
- Slices pattern for large stores
- Middleware (persist, immer)
- Selectors and performance
-
tanstack-query-hooks.ts - TanStack Query (React Query) patterns
- Basic queries and mutations
- Optimistic updates
- Pagination and infinite queries
- Dependent and parallel queries
- Query invalidation
- Cache management
-
context-provider.tsx - React Context patterns
- Basic Context
- Optimized Context (split state/dispatch)
- Context with useReducer
- Context composition
- Selector pattern
- Performance optimization
-
react-hook-form-example.tsx - Form state management
- Basic forms with Zod validation
- Complex nested fields
- Field arrays (dynamic forms)
- Controlled components
- Custom validation
- Async validation
-
immutable-updates.ts - Immutable state patterns
- Array operations (add, remove, update, sort)
- Object operations (shallow, nested)
- Complex state operations
- Using Immer for complex updates
- Performance considerations
- Common patterns
Related Skills
react-component-patterns.md - useState, useContext, custom hooks
react-data-fetching.md - SWR, React Query for server state
nextjs-app-router.md - Server Components (minimal client state)
frontend-performance.md - Re-render optimization
Last Updated: 2025-10-27
Format Version: 1.0 (Atomic)