Introduction to State Management
What is state management and why is it important in React applications
What is State Management?
State management is the practice of managing the data that controls how your application behaves and what it displays. In React, "state" refers to any data that can change over time and affects what's rendered on the screen.
As applications grow, managing state becomes increasingly complex. Data needs to be shared between components, synchronized with servers, and kept consistent across the UI. This is where state management solutions come in.
Types of State
- Local State — Component-specific data (form inputs, toggles)
- Global State — Data shared across many components (user auth, theme)
- Server State — Data from external APIs (fetched data, cache)
- URL State — Data in the URL (search params, routes)
- Form State — Form data, validation, submission status
Local State with useState
import { useState } from 'react';
function Counter() {
// Local state - only this component needs it
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
return (
<form>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
</form>
);
}
Complex State with useReducer
import { useReducer } from 'react';
// Reducer function handles state transitions
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, { id: Date.now(), text: action.text, done: false }];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.id ? { ...todo, done: !todo.done } : todo
);
case 'DELETE_TODO':
return state.filter(todo => todo.id !== action.id);
default:
return state;
}
}
function TodoList() {
const [todos, dispatch] = useReducer(todoReducer, []);
const addTodo = (text) => {
dispatch({ type: 'ADD_TODO', text });
};
const toggleTodo = (id) => {
dispatch({ type: 'TOGGLE_TODO', id });
};
return (
<div>
<button onClick={() => addTodo('New task')}>Add Todo</button>
{todos.map(todo => (
<div key={todo.id} onClick={() => toggleTodo(todo.id)}>
{todo.done ? '✓' : '○'} {todo.text}
</div>
))}
</div>
);
}
The Problem: Prop Drilling
// Without state management, data must be passed through every level
function App() {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
return (
<Layout user={user} theme={theme}>
<Header user={user} theme={theme} />
<Main user={user} theme={theme}>
<Sidebar user={user} theme={theme} />
<Content user={user} theme={theme}>
<UserProfile user={user} /> {/* Finally uses user! */}
</Content>
</Main>
</Layout>
);
}
// This is called "prop drilling" - passing props through
// many layers of components that don't use them
When Do You Need State Management?
| Scenario | Solution |
|---|---|
| Simple form inputs | useState |
| Complex component state | useReducer |
| Share state between siblings | Lift state up |
| Theme, auth, language | Context API |
| Large app, frequent updates | Redux, Zustand, Jotai |
| Server data with caching | TanStack Query, SWR |
State Management Landscape
🏠 Built-in React
- • useState - local component state
- • useReducer - complex state logic
- • Context API - share state globally
📦 External Libraries
- • Redux / Redux Toolkit - predictable state
- • Zustand - minimal and fast
- • Jotai - atomic state
- • MobX - reactive state
🌐 Server State
- • TanStack Query (React Query)
- • SWR by Vercel
- • RTK Query (Redux Toolkit)
📝 Form State
- • React Hook Form
- • Formik
- • Zod for validation
💡 Key Principles
- • Start simple - useState is often enough
- • Keep state close to where it's used
- • Lift state up only when needed
- • Separate client state from server state
- • Choose tools based on your app's needs, not hype