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:

import { useState } from 'react';

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.

import { useReducer } from 'react';

// 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 →