React State Management Comprehensive guide to managing state in React applications, from simple local state to complex global state management: Local State with…
React State Management
Comprehensive guide to managing state in React applications, from simple local state to complex global state management:
Local State with useState
useState is the foundation of React state management. It's perfect for component-level state that doesn't need to be shared.
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
return (
<div>
<h2>Count: {count}</h2>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(count - 1)}>-</button>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name"
/>
<p>Hello, {name || 'Anonymous'}!</p>
</div>
);
}
Complex Local State with useReducer
useReducer is ideal for managing complex state logic with multiple sub-values or when the next state depends on the previous one.
const initialState = {
todos: [],
filter: 'all',
loading: false,
error: null
};
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, {
id: Date.now(),
text: action.text,
completed: false
}]
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.id
? { ...todo, completed: !todo.completed }
: todo
)
};
case 'SET_FILTER':
return { ...state, filter: action.filter };
case 'SET_LOADING':
return { ...state, loading: action.loading };
case 'SET_ERROR':
return { ...state, error: action.error };
default:
return state;
}
}
function TodoApp() {
const [state, dispatch] = useReducer(todoReducer, initialState);
const addTodo = (text) => {
dispatch({ type: 'ADD_TODO', text });
};
const toggleTodo = (id) => {
dispatch({ type: 'TOGGLE_TODO', id });
};
const setFilter = (filter) => {
dispatch({ type: 'SET_FILTER', filter });
};
return (
<div>
<TodoForm onAdd={addTodo} />
<TodoFilter filter={state.filter} onFilterChange={setFilter} />
<TodoList
todos={state.todos}
filter={state.filter}
onToggle={toggleTodo}
/>
</div>
);
}
Context API for Global State
The Context API provides a way to pass data through the component tree without having to pass props down manually at every level.
// Create context
const ThemeContext = createContext();
const UserContext = createContext();
// Theme provider
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
const value = {
theme,
toggleTheme,
colors: theme === 'light' ? lightColors : darkColors
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
// User provider
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Load user from API
loadUser().then(userData => {
setUser(userData);
setLoading(false);
});
}, []);
const login = async (credentials) => {
const userData = await authenticate(credentials);
setUser(userData);
};
const logout = () => {
setUser(null);
};
return (
<UserContext.Provider value={{ user, login, logout, loading }}>
{children}
</UserContext.Provider>
);
}
// Custom hooks for consuming context
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
function useUser() {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
}
// Usage in components
function App() {
return (
<ThemeProvider>
<UserProvider>
<Header />
<MainContent />
<Footer />
</UserProvider>
</ThemeProvider>
);
}
function Header() {
const { theme, toggleTheme } = useTheme();
const { user, logout } = useUser();
return (
<header className={`header-${theme}`}>
<h1>My App</h1>
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'dark' : 'light'} mode
</button>
{user && (
<div>
<span>Welcome, {user.name}!</span>
<button onClick={logout}>Logout</button>
</div>
)}
</header>
);
}
Redux - Predictable State Container
Redux is a predictable state container for JavaScript apps. It helps you write applications that behave consistently and are easy to test.
// Store setup
import { createStore, combineReducers, applyMiddleware } from 'redux';
import { createSlice, configureStore } from '@reduxjs/toolkit';
// Redux Toolkit approach (recommended)
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;
},
},
});
const todosSlice = createSlice({
name: 'todos',
initialState: { items: [] },
reducers: {
addTodo: (state, action) => {
state.items.push({
id: Date.now(),
text: action.payload,
completed: false
});
},
toggleTodo: (state, action) => {
const todo = state.items.find(item => item.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
},
});
// Configure store
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
todos: todosSlice.reducer,
},
});
// React component with Redux
import { useSelector, useDispatch } from 'react-redux';
function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<span>{count}</span>
<button onClick={() => dispatch(counterSlice.actions.increment())}>
+
</button>
<button onClick={() => dispatch(counterSlice.actions.decrement())}>
-
</button>
</div>
);
}
Zustand - Lightweight State Management
Zustand is a small, fast and scalable state management solution with a comfy API based on hooks and isn't boilerplate-heavy.
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
// 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 middleware
const useTodoStore = create(
devtools(
persist(
(set, get) => ({
todos: [],
filter: 'all',
addTodo: (text) =>
set(
(state) => ({
todos: [
...state.todos,
{
id: Date.now(),
text,
completed: false,
createdAt: new Date(),
},
],
}),
false,
'addTodo'
),
toggleTodo: (id) =>
set(
(state) => ({
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
),
}),
false,
'toggleTodo'
),
setFilter: (filter) => set({ filter }, false, 'setFilter'),
getFilteredTodos: () => {
const { todos, filter } = get();
switch (filter) {
case 'active':
return todos.filter((todo) => !todo.completed);
case 'completed':
return todos.filter((todo) => todo.completed);
default:
return todos;
}
},
clearCompleted: () =>
set(
(state) => ({
todos: state.todos.filter((todo) => !todo.completed),
}),
false,
'clearCompleted'
),
}),
{
name: 'todo-storage',
partialize: (state) => ({ todos: state.todos }),
}
),
{
name: 'todo-store',
}
)
);
// Usage in components
function TodoApp() {
const {
todos,
filter,
addTodo,
toggleTodo,
setFilter,
getFilteredTodos,
clearCompleted,
} = useTodoStore();
const filteredTodos = getFilteredTodos();
return (
<div>
<TodoForm onAdd={addTodo} />
<TodoFilter filter={filter} onFilterChange={setFilter} />
<TodoList todos={filteredTodos} onToggle={toggleTodo} />
<button onClick={clearCompleted}>Clear Completed</button>
</div>
);
}
Jotai - Atomic State Management
Jotai takes a bottom-up approach to React state management with an atomic model inspired by Recoil. It's perfect for fine-grained reactivity.
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
// Basic atoms
const countAtom = atom(0);
const nameAtom = atom('');
// Derived atoms
const doubledCountAtom = atom((get) => get(countAtom) * 2);
const greetingAtom = atom((get) => {
const name = get(nameAtom);
return name ? `Hello, ${name}!` : 'Hello, Anonymous!';
});
// Async atoms
const userAtom = atom(async () => {
const response = await fetch('/api/user');
return response.json();
});
// Storage atoms
const themeAtom = atomWithStorage('theme', 'light');
const settingsAtom = atomWithStorage('settings', {
notifications: true,
language: 'en',
});
// Component usage
function Counter() {
const [count, setCount] = useAtom(countAtom);
const doubledCount = useAtomValue(doubledCountAtom);
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubledCount}</p>
<button onClick={() => setCount(c => c + 1)}>+</button>
<button onClick={() => setCount(c => c - 1)}>-</button>
</div>
);
}
function Greeting() {
const [name, setName] = useAtom(nameAtom);
const greeting = useAtomValue(greetingAtom);
return (
<div>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name"
/>
<p>{greeting}</p>
</div>
);
}
State Management Patterns
Lifting State Up
When multiple components need to share state, lift the state up to their common 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>
);
}
Custom Hooks for State Logic
Extract state logic into custom hooks to make it reusable across components.
// Custom hook for form state
function useForm(initialValues) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const handleChange = (name, value) => {
setValues(prev => ({ ...prev, [name]: value }));
// Clear error when user starts typing
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
};
const handleBlur = (name) => {
setTouched(prev => ({ ...prev, [name]: true }));
};
const validate = (validationRules) => {
const newErrors = {};
Object.keys(validationRules).forEach(field => {
const rule = validationRules[field];
const value = values[field];
if (rule.required && !value) {
newErrors[field] = `${field} is required`;
} else if (rule.minLength && value.length < rule.minLength) {
newErrors[field] = `${field} must be at least ${rule.minLength} characters`;
} else if (rule.pattern && !rule.pattern.test(value)) {
newErrors[field] = `${field} format is invalid`;
}
});
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const reset = () => {
setValues(initialValues);
setErrors({});
setTouched({});
};
return {
values,
errors,
touched,
handleChange,
handleBlur,
validate,
reset,
};
}
// Usage in component
function ContactForm() {
const {
values,
errors,
touched,
handleChange,
handleBlur,
validate,
reset,
} = useForm({
name: '',
email: '',
message: ''
});
const handleSubmit = (e) => {
e.preventDefault();
const isValid = validate({
name: { required: true, minLength: 2 },
email: { required: true, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ },
message: { required: true, minLength: 10 }
});
if (isValid) {
// Submit form
console.log('Form submitted:', values);
reset();
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
name="name"
value={values.name}
onChange={(e) => handleChange('name', e.target.value)}
onBlur={() => handleBlur('name')}
placeholder="Your name"
/>
{touched.name && errors.name && <span>{errors.name}</span>}
</div>
<div>
<input
name="email"
type="email"
value={values.email}
onChange={(e) => handleChange('email', e.target.value)}
onBlur={() => handleBlur('email')}
placeholder="Your email"
/>
{touched.email && errors.email && <span>{errors.email}</span>}
</div>
<div>
<textarea
name="message"
value={values.message}
onChange={(e) => handleChange('message', e.target.value)}
onBlur={() => handleBlur('message')}
placeholder="Your message"
/>
{touched.message && errors.message && <span>{errors.message}</span>}
</div>
<button type="submit">Send Message</button>
</form>
);
}
MobX - Simple Reactive State Management
MobX is a battle-tested library that makes state management simple and scalable by transparently applying functional reactive programming. It uses observables to automatically track state dependencies.
import { makeObservable, observable, action, computed } from 'mobx';
import { observer } from 'mobx-react-lite';
// Store class
class TodoStore {
todos = [];
filter = 'all';
constructor() {
makeObservable(this, {
todos: observable,
filter: observable,
addTodo: action,
toggleTodo: action,
setFilter: action,
filteredTodos: computed,
completedCount: computed,
});
}
addTodo(text) {
this.todos.push({
id: Date.now(),
text,
completed: false,
});
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
}
}
setFilter(filter) {
this.filter = filter;
}
get filteredTodos() {
switch (this.filter) {
case 'active':
return this.todos.filter(t => !t.completed);
case 'completed':
return this.todos.filter(t => t.completed);
default:
return this.todos;
}
}
get completedCount() {
return this.todos.filter(t => t.completed).length;
}
}
// Create store instance
const todoStore = new TodoStore();
// Observer component - automatically re-renders when observables change
const TodoApp = observer(() => {
const [inputValue, setInputValue] = useState('');
const handleAdd = () => {
if (inputValue.trim()) {
todoStore.addTodo(inputValue);
setInputValue('');
}
};
return (
<div>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Add todo"
/>
<button onClick={handleAdd}>Add</button>
<div>
<button onClick={() => todoStore.setFilter('all')}>All</button>
<button onClick={() => todoStore.setFilter('active')}>Active</button>
<button onClick={() => todoStore.setFilter('completed')}>Completed</button>
</div>
<ul>
{todoStore.filteredTodos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => todoStore.toggleTodo(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
</li>
))}
</ul>
<p>Completed: {todoStore.completedCount} / {todoStore.todos.length}</p>
</div>
);
});
TanStack Query (React Query) - Server State Management
TanStack Query (formerly React Query) is a powerful library for fetching, caching, and updating server state in React applications. It eliminates boilerplate and provides automatic background refetching, caching, and synchronization.
Basic Usage
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// Fetch data with useQuery
function UserProfile({ userId }) {
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch user');
return response.json();
},
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
<button onClick={() => refetch()}>Refresh</button>
</div>
);
}
Mutations
import { useMutation, useQueryClient } from '@tanstack/react-query';
function TodoForm() {
const [text, setText] = useState('');
const queryClient = useQueryClient();
const addTodoMutation = useMutation({
mutationFn: async (newTodo) => {
const response = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTodo),
});
return response.json();
},
onSuccess: () => {
// Invalidate and refetch todos
queryClient.invalidateQueries({ queryKey: ['todos'] });
setText('');
},
onError: (error) => {
alert(`Error: ${error.message}`);
},
});
const handleSubmit = (e) => {
e.preventDefault();
if (text.trim()) {
addTodoMutation.mutate({ text, completed: false });
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Add todo"
disabled={addTodoMutation.isPending}
/>
<button type="submit" disabled={addTodoMutation.isPending}>
{addTodoMutation.isPending ? 'Adding...' : 'Add Todo'}
</button>
</form>
);
}
Advanced Patterns
import { useQuery, useQueries, useInfiniteQuery } from '@tanstack/react-query';
// Parallel queries
function Dashboard({ userId }) {
const userQuery = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
const postsQuery = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchPosts(userId),
});
const statsQuery = useQuery({
queryKey: ['stats', userId],
queryFn: () => fetchStats(userId),
});
if (userQuery.isLoading || postsQuery.isLoading || statsQuery.isLoading) {
return <div>Loading...</div>;
}
return (
<div>
<UserInfo user={userQuery.data} />
<PostsList posts={postsQuery.data} />
<StatsPanel stats={statsQuery.data} />
</div>
);
}
// Infinite scroll
function InfinitePosts() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: async ({ pageParam = 1 }) => {
const response = await fetch(`/api/posts?page=${pageParam}`);
return response.json();
},
getNextPageParam: (lastPage, pages) => {
return lastPage.hasMore ? pages.length + 1 : undefined;
},
});
return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? 'Loading more...' : 'Load More'}
</button>
)}
</div>
);
}
Choosing the Right State Management Solution
useState: Simple component state, form inputs, UI toggles
useReducer: Complex state logic, multiple related state values
Context API: Theme, authentication, user preferences
Redux: Large applications, complex state, time-travel debugging
Zustand: Simple global state, minimal boilerplate
Jotai: Fine-grained reactivity, atomic state updates
MobX: Reactive programming, automatic dependency tracking
TanStack Query: Server state management, caching, background sync