State Management
React state management patterns, Context API, Redux, and external state management solutions for scalable applications.
Local State Management
useState Patterns
import { useState } from 'react';
// Simple state
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(prev => prev + 1)}>+ (functional)</button>
</div>
);
}
// Object state
function UserProfile() {
const [user, setUser] = useState({
name: '',
email: '',
preferences: {
theme: 'light',
notifications: true,
},
});
const updateUser = updates => {
setUser(prev => ({ ...prev, ...updates }));
};
const updatePreferences = prefUpdates => {
setUser(prev => ({
...prev,
preferences: { ...prev.preferences, ...prefUpdates },
}));
};
return (
<div>
<input
value={user.name}
onChange={e => updateUser({ name: e.target.value })}
/>
<label>
<input
type="checkbox"
checked={user.preferences.notifications}
onChange={e => updatePreferences({ notifications: e.target.checked })}
/>
Enable notifications
</label>
</div>
);
}
// Array state
function TodoList() {
const [todos, setTodos] = useState([]);
const addTodo = text => {
setTodos(prev => [
...prev,
{
id: Date.now(),
text,
completed: false,
},
]);
};
const toggleTodo = id => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const deleteTodo = id => {
setTodos(prev => prev.filter(todo => todo.id !== id));
};
return <div>{/* Todo components */}</div>;
}
useReducer for Complex State
import { useReducer } from 'react';
// Action types
const ACTIONS = {
SET_LOADING: 'SET_LOADING',
SET_DATA: 'SET_DATA',
SET_ERROR: 'SET_ERROR',
ADD_ITEM: 'ADD_ITEM',
UPDATE_ITEM: 'UPDATE_ITEM',
DELETE_ITEM: 'DELETE_ITEM',
};
// Reducer function
function dataReducer(state, action) {
switch (action.type) {
case ACTIONS.SET_LOADING:
return { ...state, loading: action.payload, error: null };
case ACTIONS.SET_DATA:
return { ...state, data: action.payload, loading: false, error: null };
case ACTIONS.SET_ERROR:
return { ...state, error: action.payload, loading: false };
case ACTIONS.ADD_ITEM:
return {
...state,
data: [...state.data, action.payload],
};
case ACTIONS.UPDATE_ITEM:
return {
...state,
data: state.data.map(item =>
item.id === action.payload.id
? { ...item, ...action.payload.updates }
: item
),
};
case ACTIONS.DELETE_ITEM:
return {
...state,
data: state.data.filter(item => item.id !== action.payload),
};
default:
return state;
}
}
// Component using reducer
function DataManager() {
const [state, dispatch] = useReducer(dataReducer, {
data: [],
loading: false,
error: null,
});
const fetchData = async () => {
dispatch({ type: ACTIONS.SET_LOADING, payload: true });
try {
const response = await fetch('/api/data');
const data = await response.json();
dispatch({ type: ACTIONS.SET_DATA, payload: data });
} catch (error) {
dispatch({ type: ACTIONS.SET_ERROR, payload: error.message });
}
};
const addItem = item => {
dispatch({ type: ACTIONS.ADD_ITEM, payload: item });
};
const updateItem = (id, updates) => {
dispatch({ type: ACTIONS.UPDATE_ITEM, payload: { id, updates } });
};
const deleteItem = id => {
dispatch({ type: ACTIONS.DELETE_ITEM, payload: id });
};
return (
<div>
{state.loading && <div>Loading...</div>}
{state.error && <div>Error: {state.error}</div>}
{state.data.map(item => (
<div key={item.id}>
{item.name}
<button onClick={() => updateItem(item.id, { name: 'Updated' })}>
Update
</button>
<button onClick={() => deleteItem(item.id)}>Delete</button>
</div>
))}
<button onClick={fetchData}>Fetch Data</button>
</div>
);
}
Context API
Basic Context
import { createContext, useContext, useState } from 'react';
// Create context
const ThemeContext = createContext();
// Provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => (prev === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Custom hook for using context
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
// Component using context
function ThemedButton() {
const { theme, toggleTheme } = useTheme();
return (
<button className={`btn btn-${theme}`} onClick={toggleTheme}>
Toggle Theme
</button>
);
}
Multiple Context Values
// User context
const UserContext = createContext();
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const login = async credentials => {
setLoading(true);
try {
const user = await authService.login(credentials);
setUser(user);
} finally {
setLoading(false);
}
};
const logout = () => {
setUser(null);
};
return (
<UserContext.Provider
value={{
user,
loading,
login,
logout,
isAuthenticated: !!user,
}}
>
{children}
</UserContext.Provider>
);
}
// App settings context
const SettingsContext = createContext();
function SettingsProvider({ children }) {
const [settings, setSettings] = useState({
language: 'en',
timezone: 'UTC',
notifications: true,
});
const updateSettings = updates => {
setSettings(prev => ({ ...prev, ...updates }));
};
return (
<SettingsContext.Provider value={{ settings, updateSettings }}>
{children}
</SettingsContext.Provider>
);
}
// Combine providers
function AppProviders({ children }) {
return (
<UserProvider>
<SettingsProvider>
<ThemeProvider>{children}</ThemeProvider>
</SettingsProvider>
</UserProvider>
);
}
Context with Reducer
import { createContext, useContext, useReducer } from 'react';
// Shopping cart context
const CartContext = createContext();
const cartReducer = (state, action) => {
switch (action.type) {
case 'ADD_ITEM':
const existingItem = state.items.find(
item => item.id === action.payload.id
);
if (existingItem) {
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: [] };
default:
return state;
}
};
function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, {
items: [],
total: 0,
});
// Derived state
const total = state.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const itemCount = state.items.reduce(
(count, item) => count + item.quantity,
0
);
const addItem = item => {
dispatch({ type: 'ADD_ITEM', payload: item });
};
const removeItem = id => {
dispatch({ type: 'REMOVE_ITEM', payload: id });
};
const updateQuantity = (id, quantity) => {
dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } });
};
const clearCart = () => {
dispatch({ type: 'CLEAR_CART' });
};
return (
<CartContext.Provider
value={{
items: state.items,
total,
itemCount,
addItem,
removeItem,
updateQuantity,
clearCart,
}}
>
{children}
</CartContext.Provider>
);
}
export const useCart = () => {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within a CartProvider');
}
return context;
};
Redux Toolkit
Store Setup
import { configureStore, createSlice } from '@reduxjs/toolkit';
// Counter slice
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0,
},
reducers: {
increment: state => {
state.value += 1;
},
decrement: state => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});
// Users slice
const usersSlice = createSlice({
name: 'users',
initialState: {
list: [],
loading: false,
error: null,
},
reducers: {
fetchUsersStart: state => {
state.loading = true;
state.error = null;
},
fetchUsersSuccess: (state, action) => {
state.list = action.payload;
state.loading = false;
},
fetchUsersFailure: (state, action) => {
state.error = action.payload;
state.loading = false;
},
addUser: (state, action) => {
state.list.push(action.payload);
},
updateUser: (state, action) => {
const index = state.list.findIndex(user => user.id === action.payload.id);
if (index !== -1) {
state.list[index] = { ...state.list[index], ...action.payload.updates };
}
},
deleteUser: (state, action) => {
state.list = state.list.filter(user => user.id !== action.payload);
},
},
});
// Configure store
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
users: usersSlice.reducer,
},
});
// Export actions
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export const {
fetchUsersStart,
fetchUsersSuccess,
fetchUsersFailure,
addUser,
updateUser,
deleteUser,
} = usersSlice.actions;
export default store;
Using Redux in Components
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, addUser } from './store';
function Counter() {
const count = useSelector(state => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
</div>
);
}
function UserList() {
const { list: users, loading, error } = useSelector(state => state.users);
const dispatch = useDispatch();
const handleAddUser = () => {
const newUser = {
id: Date.now(),
name: 'New User',
email: 'user@example.com',
};
dispatch(addUser(newUser));
};
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<button onClick={handleAddUser}>Add User</button>
{users.map(user => (
<div key={user.id}>
{user.name} - {user.email}
</div>
))}
</div>
);
}
Async Actions with RTK Query
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
// Define API slice
const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api/',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token;
if (token) {
headers.set('authorization', `Bearer ${token}`);
}
return headers;
},
}),
tagTypes: ['User', 'Post'],
endpoints: builder => ({
getUsers: builder.query({
query: () => 'users',
providesTags: ['User'],
}),
getUserById: builder.query({
query: id => `users/${id}`,
providesTags: (result, error, id) => [{ type: 'User', id }],
}),
addUser: builder.mutation({
query: user => ({
url: 'users',
method: 'POST',
body: user,
}),
invalidatesTags: ['User'],
}),
updateUser: builder.mutation({
query: ({ id, ...updates }) => ({
url: `users/${id}`,
method: 'PUT',
body: updates,
}),
invalidatesTags: (result, error, { id }) => [{ type: 'User', id }],
}),
deleteUser: builder.mutation({
query: id => ({
url: `users/${id}`,
method: 'DELETE',
}),
invalidatesTags: ['User'],
}),
}),
});
// Export hooks
export const {
useGetUsersQuery,
useGetUserByIdQuery,
useAddUserMutation,
useUpdateUserMutation,
useDeleteUserMutation,
} = apiSlice;
// Add to store
const store = configureStore({
reducer: {
api: apiSlice.reducer,
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(apiSlice.middleware),
});
External State Management
Zustand
import { create } from 'zustand';
// Simple store
const useCounterStore = create(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
decrement: () => set(state => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
// Complex store with async actions
const useUserStore = create((set, get) => ({
users: [],
loading: false,
error: null,
fetchUsers: async () => {
set({ loading: true, error: null });
try {
const response = await fetch('/api/users');
const users = await response.json();
set({ users, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
addUser: user =>
set(state => ({
users: [...state.users, user],
})),
updateUser: (id, updates) =>
set(state => ({
users: state.users.map(user =>
user.id === id ? { ...user, ...updates } : user
),
})),
deleteUser: id =>
set(state => ({
users: state.users.filter(user => user.id !== id),
})),
getUserById: id => get().users.find(user => user.id === id),
}));
// Usage in component
function UserComponent() {
const { users, loading, fetchUsers, addUser } = useUserStore();
useEffect(() => {
fetchUsers();
}, [fetchUsers]);
if (loading) return <div>Loading...</div>;
return (
<div>
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
<button onClick={() => addUser({ id: Date.now(), name: 'New User' })}>
Add User
</button>
</div>
);
}
Jotai (Atomic State)
import { atom, useAtom } from 'jotai';
// Basic atoms
const countAtom = atom(0);
const nameAtom = atom('');
// Derived atom
const doubleCountAtom = atom(get => get(countAtom) * 2);
// Write-only atom
const incrementAtom = atom(null, (get, set) =>
set(countAtom, get(countAtom) + 1)
);
// Async atom
const userAtom = atom(null);
const fetchUserAtom = atom(null, async (get, set, userId) => {
try {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
set(userAtom, user);
} catch (error) {
console.error('Failed to fetch user:', error);
}
});
// Usage in components
function Counter() {
const [count, setCount] = useAtom(countAtom);
const [doubleCount] = useAtom(doubleCountAtom);
const [, increment] = useAtom(incrementAtom);
return (
<div>
<p>Count: {count}</p>
<p>Double: {doubleCount}</p>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={increment}>Increment</button>
</div>
);
}
function UserProfile({ userId }) {
const [user] = useAtom(userAtom);
const [, fetchUser] = useAtom(fetchUserAtom);
useEffect(() => {
fetchUser(userId);
}, [userId, fetchUser]);
return user ? <div>{user.name}</div> : <div>Loading...</div>;
}
State Management Patterns
Lifting State Up
// Shared state in parent component
function TodoApp() {
const [todos, setTodos] = useState([]);
const [filter, setFilter] = useState('all');
const addTodo = text => {
setTodos(prev => [...prev, { id: Date.now(), text, completed: false }]);
};
const toggleTodo = id => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
return (
<div>
<TodoForm onAdd={addTodo} />
<TodoFilter filter={filter} onFilterChange={setFilter} />
<TodoList todos={filteredTodos} onToggle={toggleTodo} />
</div>
);
}
Compound State Pattern
// Managing related state together
function useFormState(initialValues) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const setValue = (name, value) => {
setValues(prev => ({ ...prev, [name]: value }));
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
};
const setError = (name, error) => {
setErrors(prev => ({ ...prev, [name]: error }));
};
const setTouched = name => {
setTouched(prev => ({ ...prev, [name]: true }));
};
const reset = () => {
setValues(initialValues);
setErrors({});
setTouched({});
setIsSubmitting(false);
};
return {
values,
errors,
touched,
isSubmitting,
setValue,
setError,
setTouched,
setIsSubmitting,
reset,
};
}
Best Practices
State Structure
// ✅ Good: Flat state structure
const [user, setUser] = useState({
id: 1,
name: 'John',
email: 'john@example.com',
});
const [preferences, setPreferences] = useState({
theme: 'light',
notifications: true,
});
// ❌ Bad: Deeply nested state
const [appState, setAppState] = useState({
user: {
profile: {
personal: {
name: 'John',
email: 'john@example.com',
},
},
},
});
State Updates
// ✅ Good: Immutable updates
const updateUser = (id, updates) => {
setUsers(prev =>
prev.map(user => (user.id === id ? { ...user, ...updates } : user))
);
};
// ❌ Bad: Mutating state
const updateUser = (id, updates) => {
const user = users.find(u => u.id === id);
user.name = updates.name; // Mutating
setUsers([...users]);
};
Context Performance
// ✅ Good: Split contexts by update frequency
const UserContext = createContext(); // Rarely changes
const NotificationContext = createContext(); // Changes frequently
// ❌ Bad: Single large context
const AppContext = createContext(); // Everything together
Quick Reference
Local State
// Simple state
const [value, setValue] = useState(initialValue);
// Complex state
const [state, dispatch] = useReducer(reducer, initialState);
Context
// Create and provide context
const MyContext = createContext();
<MyContext.Provider value={value}>{children}</MyContext.Provider>;
// Use context
const value = useContext(MyContext);
Redux Toolkit
// Slice
const slice = createSlice({
name: 'feature',
initialState,
reducers: {
/* reducers */
},
});
// Use in component
const state = useSelector(state => state.feature);
const dispatch = useDispatch();
External Libraries
// Zustand
const useStore = create(set => ({
/* state and actions */
}));
// Jotai
const myAtom = atom(initialValue);
const [value, setValue] = useAtom(myAtom);