State is the heart of any interactive React application. It represents dynamic data that changes over time based on user interactions, API responses, or timers. React provides two core built-in hooks for managing local component state: useState and useReducer.
1. Atomic State Management with `useState`
The useState hook declares a state variable and a updater function. When state updates, React triggers a component re-render.
Functional Updaters & Immutability Rules
Always use functional updaters when the new state depends on the previous state to avoid stale closure bugs during concurrent updates:
export default function Counter() {
const [count, setCount] = useState(0);
const handleIncrementTriple = () => {
// Functional updaters guarantee correct sequential execution
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
};
return <button className="btn btn-primary" onClick={handleIncrementTriple}>Count: {count}</button>;
}
2. Complex State Transitions with `useReducer`
When state consists of complex nested objects, multiple sub-values, or interdependent logic (e.g., handling loading, success, and error states for a form submission), useReducer provides a cleaner state-machine pattern similar to Redux.
// 1. Initial State
const initialState = { loading: false, data: null, error: null };
// 2. Pure Reducer Function
function fetchReducer(state, action) {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true, error: null };
case 'FETCH_SUCCESS':
return { ...state, loading: false, data: action.payload };
case 'FETCH_ERROR':
return { ...state, loading: false, error: action.payload };
default:
return state;
}
}
// 3. Component Dispatch Usage
export default function UserLoader() {
const [state, dispatch] = useReducer(fetchReducer, initialState);
const loadData = async () => {
dispatch({ type: 'FETCH_START' });
try {
const res = await fetch('/api/user');
const data = await res.json();
dispatch({ type: 'FETCH_SUCCESS', payload: data });
} catch (err) {
dispatch({ type: 'FETCH_ERROR', payload: err.message });
}
};
return (
<div className="p-3 bg-light rounded border">
<button className="btn btn-success mb-2" onClick={loadData}>Load Profile</button>
{state.loading && <p className="text-info">Loading...</p>}
{state.error && <p className="text-danger">Error: {state.error}</p>}
{state.data && <h4>User: {state.data.name}</h4>}
</div>
);
}
3. When to Choose `useState` vs `useReducer`
| Scenario | Recommended Hook | Reason |
|---|---|---|
| Primitive values (strings, numbers, booleans) | useState |
Simple, straightforward, and minimal boilerplate. |
| Independent simple state properties | useState |
Easy to maintain separate atomic state variables. |
| Interdependent state updates (e.g., status + data + error) | useReducer |
Ensures state transitions occur atomically without inconsistent intermediate states. |
| Complex nested objects or arrays | useReducer |
Centralizes state modification logic in a pure reducer function. |
Build Interactive React Apps with Telugu IT Tutorials
Learn full-stack state management, Redux Toolkit, and Context API in our live interactive courses. Explore React Courses →