Skip to main content

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);