usereducer and useReducer vs useState

Table of contents

useReducer β†’ alternative of useState hook

for small app β†’ use useState

for larger apps β†’ use useReducer along with contextApi

import "./styles.css";
import { useReducer } from "react";
export default function App() {
  // state -> means current state
  const reducer = (state, action) => {
    console.log("action", action);
    console.log("state", state);

    if (action.type === "INCREMENT") {
      return state + 1;
    } else if (action.type === "DECREMENT") {
      return state - 1;
    }
  };

  const [count, dispatch] = useReducer(reducer, 0);
  console.log("useReducer", useReducer(reducer, 0));

  return (
    <div className="App">
      {count}
      <button onClick={() => dispatch({ type: "INCREMENT" })}>
        Increment{" "}
      </button>
      <button onClick={() => dispatch({ type: "DECREMENT" })}>
        {" "}
        Decrement{" "}
      </button>
    </div>
  );
}

Q) Why to use useReducerHook ? what beneifits its provides over useState? is there any re-rendering benefits.

Why Use useReducer Hook?

The useReducer hook is an alternative to useState for managing state in React. It is particularly useful when:

  1. State Logic is Complex – When state transitions depend on the previous state or involve multiple sub-states.

  2. Multiple Related State Updates – When multiple state values need to be updated together (e.g., form handling).

  3. Better Performance – In some cases, useReducer can help avoid unnecessary re-renders by batching updates.

  4. Easier Debugging & Testing – The reducer function encapsulates state transitions, making it easier to test and maintain.


Benefits Over useState

FeatureuseStateuseReducer
Complex State LogicNot Ideal – State logic is often scattered across multiple setState calls.Better – Centralized state logic inside a reducer function.
Multiple State UpdatesMay cause multiple re-renders.Can batch updates within a single dispatch call, reducing re-renders.
Derived StateRequires multiple useEffect or calculations inside the component.Derived state can be computed inside the reducer.
Readability & MaintainabilitySimple for basic states.More structured and scalable for complex state logic.
PerformanceCan trigger unnecessary re-renders if not handled properly.Helps reduce unnecessary re-renders when used correctly.

Does useReducer Reduce Re-Renders?

Yes, but not always. Here’s why:

  • When using useState, updating state always causes a re-render, even if the new state is the same as the old one.

  • With useReducer, React re-renders only if the returned state is different from the previous one.

  • If the reducer function returns the same state (i.e., return state), React does not trigger a re-render.

  • dispatch does not change across renders, meaning child components relying on dispatch won’t cause unnecessary re-renders.


When to Use useReducer Instead of useState

Use useReducer when:

  1. The state has multiple sub-values that change together (e.g., form state, complex UI state).

  2. State transitions are complex (e.g., state depends on the previous state).

  3. You want to optimize performance and avoid unnecessary re-renders.

  4. You prefer a more structured way of handling state updates.

Use useState when:

  1. The state is simple (boolean, string, number, or small objects).

  2. There’s no complex logic for state transitions.


Example: useState vs. useReducer

Using useState

import { useState } from "react";

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

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

Using useReducer

import { useReducer } from "react";

const initialState = 0;
const reducer = (state: number, action: { type: string }) => {
  switch (action.type) {
    case "increment":
      return state + 1;
    default:
      return state;
  }
};

function Counter() {
  const [count, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch({ type: "increment" })}>Increment</button>
    </div>
  );
}

Key Differences in the Example

  • useState: Directly modifies state (setCount(count + 1)).

  • useReducer: Uses a centralized state update function (dispatch({ type: "increment" })).

  • useReducer keeps state transitions predictable and separate from the UI.

Final Thought

If state updates are simple, useState is fine. But for complex logic, performance optimizations, and better organization, useReducer is a great choice! πŸš€


Meaning of this
Multiple Related State Updates – When multiple state values need to be updated together (e.g., form handling). ?

How useReducer Batches State Updates to Reduce Re-renders

One key advantage of useReducer over useState is that it can batch multiple state updates within a single dispatch call, thereby reducing unnecessary re-renders.


Example: Using useState (Multiple Re-renders)

When using useState, each setState call triggers a re-render. If we update multiple state variables in sequence, React will re-render after every update, which may hurt performance.

import { useState } from "react";

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

  const handleIncrement = () => {
    setCount(count + 1);  // Causes 1st re-render
    setDoubleCount((count + 1) * 2); // Causes 2nd re-render
  };

  return (
    <div>
      <p>Count: {count}</p>
      <p>Double Count: {doubleCount}</p>
      <button onClick={handleIncrement}>Increment</button>
    </div>
  );
}

Issue

  • When handleIncrement is called:

    • setCount(count + 1) updates count, triggering a re-render.

    • Then setDoubleCount((count + 1) * 2) updates doubleCount, causing another re-render.

  • Result: Two re-renders for one user action.


Example: Using useReducer (Single Re-render)

With useReducer, we can batch updates in the reducer function, so React will only re-render once after all state changes.

import { useReducer } from "react";

type State = {
  count: number;
  doubleCount: number;
};

type Action = { type: "increment" };

const initialState: State = { count: 0, doubleCount: 0 };

const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case "increment":
      const newCount = state.count + 1;
      return { count: newCount, doubleCount: newCount * 2 }; // Both updates happen together
    default:
      return state;
  }
};

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

  return (
    <div>
      <p>Count: {state.count}</p>
      <p>Double Count: {state.doubleCount}</p>
      <button onClick={() => dispatch({ type: "increment" })}>Increment</button>
    </div>
  );
}

Why is This Better?

  1. Only One Re-render – Both count and doubleCount are updated together inside the reducer function.

  2. State Updates Are Grouped – The state is updated in a structured way rather than through separate function calls.

  3. Better Performance – Reducing unnecessary re-renders can make the app smoother, especially for complex UI.


Conclusion

  • useState can cause multiple re-renders if multiple state updates are called in sequence.

  • useReducer groups all related state updates in a single function, ensuring only one re-render.

  • This is especially useful when managing complex state that depends on multiple values.


Please Explain this one
Derived State*Requires multiple useEffect or calculations inside the component.Derived state can be computed inside the reducer.

Understanding Derived State: useState vs. useReducer

Derived state is when one part of the state depends on another. With useState, you often need to use useEffect to recalculate it whenever a state value changes. However, useReducer allows you to compute derived state within the reducer function, avoiding extra re-renders.


πŸš€ Example: Using useState with useEffect (Multiple Re-renders)

Consider a scenario where we track the number of items in a cart and need to compute the total price based on item quantity and price.

import { useState, useEffect } from "react";

function ShoppingCart() {
  const [items, setItems] = useState([{ name: "Apple", price: 10, qty: 1 }]);
  const [totalPrice, setTotalPrice] = useState(0);

  useEffect(() => {
    // Recalculate total price whenever items change
    const total = items.reduce((acc, item) => acc + item.price * item.qty, 0);
    setTotalPrice(total);
  }, [items]);

  const addItem = () => {
    setItems([...items, { name: "Banana", price: 5, qty: 2 }]); // Adds a new item
  };

  return (
    <div>
      <h3>Total Price: {totalPrice}</h3>
      <button onClick={addItem}>Add Item</button>
    </div>
  );
}

Problems with useState + useEffect

  1. Extra Re-renders: Every time items change, useEffect triggers another state update (setTotalPrice), causing an additional re-render.

  2. Unnecessary Side Effects: The logic to compute totalPrice is separate from the state update (setItems), making it harder to manage.


βœ… Using useReducer (Better Performance)

With useReducer, we calculate the totalPrice inside the reducer function, avoiding extra re-renders caused by useEffect.

import { useReducer } from "react";

// Define initial state
const initialState = {
  items: [{ name: "Apple", price: 10, qty: 1 }],
  totalPrice: 10,
};

// Define actions
type Action = { type: "ADD_ITEM"; item: { name: string; price: number; qty: number } };

// Reducer function
const cartReducer = (state: typeof initialState, action: Action) => {
  switch (action.type) {
    case "ADD_ITEM":
      const updatedItems = [...state.items, action.item];
      const newTotal = updatedItems.reduce((acc, item) => acc + item.price * item.qty, 0);
      return { items: updatedItems, totalPrice: newTotal };
    default:
      return state;
  }
};

function ShoppingCart() {
  const [state, dispatch] = useReducer(cartReducer, initialState);

  return (
    <div>
      <h3>Total Price: {state.totalPrice}</h3>
      <button onClick={() => dispatch({ type: "ADD_ITEM", item: { name: "Banana", price: 5, qty: 2 } })}>
        Add Item
      </button>
    </div>
  );
}

Why is useReducer Better Here?

βœ… No Need for useEffect β†’ The totalPrice is derived inside the reducer, so we don’t need extra useEffect logic.
βœ… Fewer Re-renders β†’ Both items and totalPrice update in a single state update, reducing unnecessary renders.
βœ… Centralized State Logic β†’ The reducer cleanly handles both state updates and derived state, making the code more maintainable.


Final Thoughts

  • If you only track individual values, useState is fine.

  • If state depends on other state values, useReducer is a cleaner and more efficient approach.

  • useReducer reduces unnecessary re-renders by computing derived state inside the reducer function instead of using useEffect.


dispatch` does not change across renders, meaning child components relying on dispatch won’t cause unnecessary re-renders.

Understanding How dispatch Prevents Unnecessary Re-renders in useReducer

One key optimization of useReducer over useState is that the dispatch function never changes between renders. This helps prevent unnecessary re-renders in child components that depend on dispatch.


πŸš€ Problem with useState: setState Changes on Every Render

With useState, the setState function changes across renders, which can cause unnecessary re-renders in components that receive it as a prop.

Example: useState Causes Unnecessary Re-renders

import { useState } from "react";

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

  console.log("Parent re-rendered");

  return (
    <div>
      <h3>Count: {count}</h3>
      <Child onClick={() => setCount(count + 1)} />
    </div>
  );
}

const Child = ({ onClick }: { onClick: () => void }) => {
  console.log("Child re-rendered");
  return <button onClick={onClick}>Increment</button>;
};

export default Parent;

πŸ”΄ Issue

  • Each time setCount updates the state, Parent re-renders.

  • Since onClick={() => setCount(count + 1)} creates a new function reference on every render, Child re-renders unnecessarily.


βœ… Solution with useReducer: dispatch Stays the Same Across Renders

In useReducer, the dispatch function never changes between renders, preventing unnecessary re-renders in child components.

Example: useReducer Prevents Unnecessary Re-renders

import { useReducer } from "react";

const reducer = (state: number, action: { type: "increment" }) => {
  switch (action.type) {
    case "increment":
      return state + 1;
    default:
      return state;
  }
};

function Parent() {
  const [count, dispatch] = useReducer(reducer, 0);

  console.log("Parent re-rendered");

  return (
    <div>
      <h3>Count: {count}</h3>
      <Child onClick={() => dispatch({ type: "increment" })} />
    </div>
  );
}

const Child = ({ onClick }: { onClick: () => void }) => {
  console.log("Child re-rendered");
  return <button onClick={onClick}>Increment</button>;
};

export default Parent;

🟒 Why useReducer is Better Here

  • dispatch never changes between renders, unlike setState, which creates a new function reference every time.

  • Even though Parent re-renders when count updates, the dispatch function remains the same, preventing Child from re-rendering.


Final Takeaway

  • With useState, function references change on every render, leading to unnecessary child re-renders.

  • With useReducer, dispatch remains the same, reducing unnecessary re-renders and improving performance.


why people say using useReducer with context is more good then using useState with context ?

Using useReducer with Context API is generally preferred over useState in global state management because of performance optimizations and better state management structure. Let’s break it down.


πŸš€ Why useReducer with Context API is Better than useState with Context API?

1️⃣ Prevents Unnecessary Re-renders

With useState + Context, every time the state updates, all components that consume the context re-render, even if they don't need the updated state.

πŸ‘‰ With useReducer, we can pass the dispatch function down to components instead of the state itself, which means components don’t need to re-render when the state updates.


πŸ›‘ Problem with useState + Context API

import React, { createContext, useState, useContext } from "react";

// Create a Context
const CounterContext = createContext<{ count: number; setCount: React.Dispatch<React.SetStateAction<number>> } | null>(null);

const CounterProvider = ({ children }: { children: React.ReactNode }) => {
  const [count, setCount] = useState(0);

  return <CounterContext.Provider value={{ count, setCount }}>{children}</CounterContext.Provider>;
};

// Child component that uses the context
const DisplayCounter = () => {
  console.log("DisplayCounter Re-rendered");
  const { count } = useContext(CounterContext)!;
  return <h3>Count: {count}</h3>;
};

// Child component that increments count
const IncrementButton = () => {
  console.log("IncrementButton Re-rendered");
  const { setCount } = useContext(CounterContext)!;
  return <button onClick={() => setCount((prev) => prev + 1)}>Increment</button>;
};

// Main App
const App = () => (
  <CounterProvider>
    <DisplayCounter />
    <IncrementButton />
  </CounterProvider>
);

export default App;

πŸ”΄ Problem: Every Component Re-renders on State Change

  • When setCount updates the state, both DisplayCounter and IncrementButton re-render, even though IncrementButton does not depend on count.

  • This leads to unnecessary re-renders and worsens performance.


βœ… Solution: useReducer with Context API (Optimized)

Instead of passing the state and setState, we pass state and dispatch, which does not cause components to re-render unnecessarily.

import React, { createContext, useReducer, useContext } from "react";

// Define the reducer function
const reducer = (state: number, action: { type: "increment" }) => {
  switch (action.type) {
    case "increment":
      return state + 1;
    default:
      return state;
  }
};

// Create a Context
const CounterContext = createContext<{ state: number; dispatch: React.Dispatch<{ type: "increment" }> } | null>(null);

// Context Provider
const CounterProvider = ({ children }: { children: React.ReactNode }) => {
  const [state, dispatch] = useReducer(reducer, 0);

  return <CounterContext.Provider value={{ state, dispatch }}>{children}</CounterContext.Provider>;
};

// Child component that only reads the count
const DisplayCounter = () => {
  console.log("DisplayCounter Re-rendered");
  const { state } = useContext(CounterContext)!;
  return <h3>Count: {state}</h3>;
};

// Child component that only triggers state change
const IncrementButton = () => {
  console.log("IncrementButton Re-rendered");
  const { dispatch } = useContext(CounterContext)!;
  return <button onClick={() => dispatch({ type: "increment" })}>Increment</button>;
};

// Main App
const App = () => (
  <CounterProvider>
    <DisplayCounter />
    <IncrementButton />
  </CounterProvider>
);

export default App;

🟒 Why is useReducer with Context API Better?

βœ… IncrementButton does NOT re-render when count changes, because it only has access to dispatch (which never changes).
βœ… Only DisplayCounter re-renders when count updates.
βœ… More structured approach to managing complex state changes (good for bigger applications).


🎯 When to Use useReducer with Context API?

βœ… When managing complex state with multiple related state values.
βœ… When avoiding unnecessary re-renders is important.
βœ… When state transitions have multiple actions (e.g., ADD_ITEM, REMOVE_ITEM, UPDATE_ITEM).

Would you like an example with a more complex state, like a shopping cart? πŸš€

0
Subscribe to my newsletter

Read articles from Surya Prakash Singh directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Surya Prakash Singh
Surya Prakash Singh

A full stack developer from India. Html | Css | Javascript | React | Redux | Tailwind | Node | Express | Mongodb | Sql | Nosql | Socket.io