Use the useReducer Hook in Next.js for Better State Management

When building complex applications in Next.js, managing state efficiently is essential for maintaining performance and readability. While React’s useState hook works well for basic state management, useReducer offers a more structured approach when your state logic gets more intricate.

In this tutorial, we’ll build a shopping cart application using the useReducer hook in Next.js. The cart will allow users to:

  • Add products to the cart
  • Remove products from the cart
  • Adjust product quantities
  • View the total price

By using useReducer, we can manage the cart’s state in a structured, predictable way.


What is useReducer?

The useReducer hook is a React hook that helps manage more complex state. It works similarly to useState, but instead of directly updating the state, you dispatch actions that describe how the state should change. This makes it a perfect tool for handling more complex state transitions, especially in large applications.

useReducer follows the Reducer pattern, often used in libraries like Redux, where actions modify the state in a predictable manner.


When Should You Use useReducer in Next.js?

You should consider using useReducer in the following cases:

  • When your state logic is complex and involves multiple variables.
  • When the next state depends on the previous one, making direct updates less practical.
  • When you want to structure your state updates using actions, like in a Redux-like approach.

For simpler state management, useState might still be a better choice. But useReducer shines when the complexity grows.


First, if you don’t have a Next.js app set up yet, follow these steps:

1.Create a Next.js app using the latest version:

npx create-next-app@latest shopping-cart-nextjs
cd shopping-cart-nextjs

2. Run the development server:

Your app should now be running at http://localhost:3000.


Step 1: The useReducer Hook Syntax

const [state, dispatch] = useReducer(reducer, initialState);
  • state: The current state of your app.
  • dispatch: A function used to send actions to the reducer.
  • reducer: A function that returns a new state based on the action.
  • initialState: The state object at the initial render.

Step 2: Define the Cart State Structure

In a real-world application, the cart’s state would typically involve several properties:

  • items: An array of products currently in the cart.
  • totalPrice: The total price of all products in the cart.

Each product might have:

  • id: A unique identifier for the product.
  • name: The name of the product.
  • price: The price of the product.
  • quantity: The number of units of the product in the cart.

Step 3: Set Up the useReducer Hook

We’ll create a reducer to manage the cart’s state and handle the following actions:

  • Add an item to the cart.
  • Remove an item from the cart.
  • Update the quantity of an item in the cart.
  • Calculate the total price based on the cart’s contents.

Here’s how to set it up.

  1. Open src/app/page.js.
  2. Replace its content with the following code:
"use client";
import { useReducer } from "react";

const initialState = {
  items: [],
  totalPrice: 0,
};

// Action types
const ADD_ITEM = "ADD_ITEM";
const REMOVE_ITEM = "REMOVE_ITEM";
const UPDATE_QUANTITY = "UPDATE_QUANTITY";

// Reducer function
function reducer(state, action) {
  switch (action.type) {
    case ADD_ITEM: {
      const existingItem = state.items.find(
        (item) => item.id === action.payload.id
      );
      if (existingItem) {
        // Update quantity if the item already exists in the cart
        return {
          ...state,
          items: state.items.map((item) =>
            item.id === action.payload.id
              ? { ...item, quantity: item.quantity + 1 }
              : item
          ),
        };
      } else {
        // Add new item to the cart
        return {
          ...state,
          items: [...state.items, { ...action.payload, quantity: 1 }],
        };
      }
    }
    case REMOVE_ITEM:
      return {
        ...state,
        items: state.items.filter((item) => item.id !== action.payload.id),
      };
    case UPDATE_QUANTITY: {
      const newQuantity =
        isNaN(action.payload.quantity) || action.payload.quantity < 1
          ? 1 // Set to 1 if the quantity is invalid (NaN) or less than 1
          : action.payload.quantity;
      return {
        ...state,
        items: state.items.map((item) =>
          item.id === action.payload.id
            ? { ...item, quantity: newQuantity }
            : item
        ),
      };
    }
    default:
      return state;
  }
}

export default function Home() {
  // Use useReducer to manage the cart state
  const [state, dispatch] = useReducer(reducer, initialState);

  // Sample product data
  const products = [
    { id: 1, name: "Laptop", price: 1000 },
    { id: 2, name: "Headphones", price: 100 },
    { id: 3, name: "Keyboard", price: 50 },
  ];

  // Handle adding item to cart
  const addToCart = (product) => {
    dispatch({ type: ADD_ITEM, payload: product });
  };

  // Handle removing item from cart
  const removeFromCart = (id) => {
    dispatch({ type: REMOVE_ITEM, payload: { id } });
  };

  // Handle updating item quantity
  const updateQuantity = (id, quantity) => {
    dispatch({ type: UPDATE_QUANTITY, payload: { id, quantity } });
  };

  // Calculate total price
  const calculateTotal = () => {
    return state.items.reduce(
      (total, item) => total + item.price * item.quantity,
      0
    );
  };

  return (
    <div style={{ textAlign: "center", padding: "20px" }}>
      <h1>Shopping Cart</h1>

      <div>
        <h2>Products</h2>
        {products.map((product) => (
          <div key={product.id} style={{ marginBottom: "20px" }}>
            <span>
              {product.name} - ${product.price}
            </span>
            <button onClick={() => addToCart(product)}>Add to Cart</button>
          </div>
        ))}
      </div>

      <div>
        <h2>Cart</h2>
        {state.items.length === 0 ? (
          <p>Your cart is empty.</p>
        ) : (
          <div>
            <ul>
              {state.items.map((item) => (
                <li key={item.id}>
                  {item.name} - ${item.price} x {item.quantity}{" "}
                  <button onClick={() => removeFromCart(item.id)}>
                    Remove
                  </button>
                  <button
                    onClick={() =>
                      updateQuantity(item.id, Math.max(1, item.quantity - 1))
                    }
                  >
                    -
                  </button>
                  <span>{item.quantity}</span>
                  <button
                    onClick={() => updateQuantity(item.id, item.quantity + 1)}
                  >
                    +
                  </button>
                </li>
              ))}
            </ul>
            <p>Total: ${calculateTotal()}</p>
          </div>
        )}
      </div>
    </div>
  );
}

Explanation of the Code:

  1. State Structure: The initial state consists of:
    • items: An array of cart items, each with an id, name, price, and quantity.
    • totalPrice: The total price of all items in the cart, which is recalculated whenever the state changes.
  2. Actions:
    • ADD_ITEM: Adds an item to the cart. If the item already exists, it simply increments its quantity.
    • REMOVE_ITEM: Removes an item from the cart.
    • UPDATE_QUANTITY: Updates the quantity of a specific item.
  3. Reducer:
    • The reducer function processes actions based on their type and updates the state accordingly. Each action type (e.g., ADD_ITEM, REMOVE_ITEM) corresponds to a state update logic.
  4. Calculate Price:
    • The calculateTotal function is used to calculate the cart’s total price by summing up the price of each item multiplied by its quantity.
  5. UI:
    • We display a list of available products, each with an “Add to Cart” button.
    • The cart section shows the items in the cart, allowing users to remove items or adjust their quantity.
    • The total price of the cart is dynamically calculated and displayed.

Conclusion

In this tutorial, we built a simple shopping cart app in Next.js using the useReducer hook. The cart can handle:

  • Adding, removing, and updating products.
  • Calculating the total price.

We also saw how useReducer provides a more structured and predictable approach to handling complex state in comparison to useState, especially in real-world applications like e-commerce platforms.


Posted

in

by

Post’s tag:

Advertisement