Oct 18, 2025

useState vs useReducer Explained: React State Management Made Simple

Tags

State management is at the heart of every React application. It helps your app track changes in data, user input, and UI updates efficiently. Whether you're handling a simple form or managing complex data structures, understanding how to control and update state is key to building dynamic, responsive UIs.


In this guide, we’ll explore the basics of state management using useState and useReducer, understand when to use each, and go through practical examples that make these concepts easy to apply.


usestate-vs-usereducer




What is State in React?


State is data that determines a component’s behaviour and how it renders. When the state changes, React automatically re-renders the component to reflect the new data.

There are two types of state in React:

1- Local State: Managed within a single component using useState.
2- Global State: Shared across components using tools like Context API or external libraries (Redux, Zustand, Recoil).


What is useState in React?


The useState Hook is a built-in function that allows functional components to have local state. It returns a pair of the current state value and a function to update that value.


// Basic Syntax 
const [state, setState] = useState(initialValue);


Example: Simple Counter App

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  const increase = () => setCount(count + 1);
  const decrease = () => setCount(count - 1);

  return (
    <div>
       <h2>Counter: {count} </h2>
       <button onclick="{increase}">+</button>
       <button onclick="{decrease}">-</button>
     </div>
  );
}

export default Counter;

How it works: Every time you click a button, React updates the state with setCount and re-renders the component.


What is useReducer in React?


useReducer is another built-in hook used to manage more complex state logic. It works similarly to Redux by using a reducer function that takes the current state and an action, and returns a new state.


// Basic Syntax 
const [state, dispatch] = useReducer(reducer, initialState);


Example: Complex Counter with Multiple Actions

import React, { useReducer } from 'react';

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    default:
      throw new Error('Unknown action');
  }
}

function CounterReducer() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <h2>Count: {state.count}</h2>
      <button onclick="{()"> dispatch({ type: 'increment' })}>+</button>
      <button onclick="{()"> dispatch({ type: 'decrement' })}>-</button>
      <button onclick="{()"> dispatch({ type: 'reset' })}>Reset</button>
    </div>
  );
}

export default CounterReducer;

Key insight: useReducer centralises state transitions, making complex updates easier to track and debug.


useState vs useReducer


Feature
useState
useReducer
Used For
Simple states
Complex states with multiple transitions
Syntax
Simple and concise
Requires reducer function
State Updates
Direct via setter
Managed via dispatch actions
Best For
Independent values
Interconnected logic

Example: If your state only tracks a counter, use useState. If you're updating nested objects or have multiple state transitions (like async loading, success, error), use useReducer.


Handling Multiple Form Inputs with useReducer



import React, { useReducer } from 'react';

const initialForm = { name: '', email: '', password: '' };

function reducer(state, action) {
  return { ...state, [action.field]: action.value };
}

function SignUpForm() {
  const [formState, dispatch] = useReducer(reducer, initialForm);

  const handleChange = (e) => {
    dispatch({ field: e.target.name, value: e.target.value });
  };

  return (
    <form>
      <input name="name" value={formState.name} onChange={handleChange} placeholder="Name" />
      <input name="email" value={formState.email} onChange={handleChange} placeholder="Email" />
      <input name="password" value={formState.password} onChange={handleChange} placeholder="Password" />
      <pre>{JSON.stringify(formState, null, 2)}</pre>
    </form>
  );
}
export default SignUpForm;

This pattern keeps all form state updates controlled and consolidated.


Custom Hook with useReducer


You can wrap useReducer in a custom hook for reusability:

import { useReducer } from 'react';

function useFormReducer(initialState) {
  function reducer(state, action) {
    return { ...state, [action.name]: action.value };
  }

  const [form, dispatch] = useReducer(reducer, initialState);
  const handleChange = (e) => dispatch(e.target);
  
  return [form, handleChange];
}

export default useFormReducer;

Use this hook across multiple forms without repeating logic.



Combining useState and useReducer


Some applications need both. Use useState for UI states (like modal toggles) and useReducer for managing complex data or API state.

const [isModalOpen, setIsModalOpen] = useState(false);
const [dataState, dispatch] = useReducer(dataReducer, initialData);


When to Use useState vs useReducer


Use useState when:
1- State updates are simple and independent.
2- The component controls one or two state variables.

Use useReducer when:
1- State transitions are complex (e.g., multiple conditions or nested updates).
2- Multiple values in the state depend on each other.
3- You need predictable and testable state updates.


Summary


State management is a critical skill in React development. Use useState for simple, isolated pieces of state and useReducer when logic becomes complex or tightly coupled. By mastering both, you can handle any state scenario efficiently, from a basic counter to scalable applications with multiple data layers.

Tip: Always start simple. Use useState until you feel your logic getting messy, then reach for useReducer!


EmoticonEmoticon