Nijaz
Nijaz

Reputation: 188

React useReducer bug while updating state array

I haven't been in React for a while and now I am revising. Well I faced error and tried debugging it for about 2hours and couldn't find bug. Well, the main logic of program goes like this:

  1. There is one main context with cart object.
  2. Main property is cart array where I store all products
  3. If I add product with same name (I don't compare it with id's right now because it is small project for revising) it should just sum up old amount of that product with new amount

Well, I did all logic for adding but the problem started when I found out that for some reason when I continue adding products, it linearly doubles it up. I will leave github link here if you want to check full aplication. Also, there I will leave only important components. Maybe there is small mistake which I forget to consider. Also I removed logic for summing up amount of same products because that's not neccesary right now. Pushing into state array is important.

Github: https://github.com/AndNijaz/practice-react-

Site

//Context

import React, { useEffect, useReducer, useState } from "react";

const CartContext = React.createContext({
  cart: [],
  totalAmount: 0,
  totalPrice: 0,
  addToCart: () => {},
  setTotalAmount: () => {},
  setTotalPrice: () => {},
});

const cartAction = (state, action) => {
  const foodObject = action.value;
  const arr = [];
  console.log(state.foodArr);
  if (action.type === "ADD_TO_CART") {
    arr.push(foodObject);
    state.foodArr = [...state.foodArr, ...arr];
    return { ...state };
  }
  return { ...state };
};

export const CartContextProvider = (props) => {
  const [cartState, setCartState] = useReducer(cartAction, {
    foodArr: [],
    totalAmount: 0,
    totalPrice: 0,
  });

  const addToCart = (foodObj) => {
    setCartState({ type: "ADD_TO_CART", value: foodObj });
  };

  return (
    <CartContext.Provider
      value={{
        cart: cartState.foodArr,
        totalAmount: cartState.totalAmount,
        totalPrice: cartState.totalAmount,
        addToCart: addToCart,
      }}
    >
      {props.children}
    </CartContext.Provider>
  );
};

export default CartContext;

//Food.js

import React, { useContext, useState, useRef, useEffect } from "react";
import CartContext from "../../context/cart-context";
import Button from "../ui/Button";
import style from "./Food.module.css";

const Food = (props) => {
  const ctx = useContext(CartContext);
  const foodObj = props.value;
  const amountInput = useRef();

  const onClickHandler = () => {
    const obj = {
      name: foodObj.name,
      description: foodObj.description,
      price: foodObj.price,
      value: +amountInput.current.value,
    };
    console.log(obj);
    ctx.addToCart(obj);
  };

  return (
    <div className={style["food"]}>
      <div className={style["food__info"]}>
        <p>{foodObj.name}</p>
        <p>{foodObj.description}</p>
        <p>{foodObj.price}$</p>
      </div>
      <div className={style["food__form"]}>
        <div className={style["food__form-row"]}>
          <p>Amount</p>
          <input type="number" min="0" ref={amountInput} />
        </div>
        <Button type="button" onClick={onClickHandler}>
          +Add
        </Button>
      </div>
    </div>
  );
};

export default Food;

//Button import style from "./Button.module.css";

const Button = (props) => {
  return (
    <button
      type={props.type}
      className={style["button"]}
      onClick={props.onClick}
    >
      {props.children}
    </button>
  );
};

export default Button;

Upvotes: 1

Views: 1002

Answers (1)

Drew Reese
Drew Reese

Reputation: 202677

Issue

The React.StrictMode component is exposing an unintentional side-effect.

See Detecting Unexpected Side Effects

Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:

  • Class component constructor, render, and shouldComponentUpdate methods
  • Class component static getDerivedStateFromProps method
  • Function component bodies
  • State updater functions (the first argument to setState)
  • Functions passed to useState, useMemo, or useReducer <-- here

The function passed to useReducer is double invoked.

const cartAction = (state, action) => {
  const foodObject = action.value;

  const arr = [];

  console.log(state.foodArr);
  if (action.type === "ADD_TO_CART") {
    arr.push(foodObject); // <-- mutates arr array, pushes duplicates!

    state.foodArr = [...state.foodArr, ...arr]; // <-- duplicates copied

    return { ...state };
  }
  return { ...state };
};

Solution

Reducer functions are to be considered pure functions, taking the current state and an action and compute the next state. In the sense of pure functionality, the same next state should result from the same current state and action. The solution is only add the new foodObject object once, based on the current state.

Note also for the default "case" just return the current state object. Shallow copying the state without changing any data will unnecessarily trigger rerenders.

I suggest also renaming the reducer function to cartReducer so its purpose is more clear to future readers of your code.

const cartReducer = (state, action) => {
  switch(action.type) {
    case "ADD_TO_CART":
      const foodObject = action.value;
      return {
        ...state, // shallow copy current state into new state object
        foodArr: [
          ...state.foodArr, // shallow copy current food array
          foodObject, // append new food object
        ],
      };

    default:
      return state;
  }
};

...

useReducer(cartReducer, initialState);

Edit react-usereducer-bug-while-updating-state-array

Additional Suggestions

  1. When adding an item to the cart, first check if the cart already contains that item, and if so, shallow copy the cart and the matching item and update the item's value property which appears to be the quantity.
  2. Cart/item totals are generally computed values from existing state. As such these are considered derived state and they don't belong in state, these should computed when rendering. See Identify the minimal (but complete) representation of UI state. They can be memoized in the cart context if necessary.

Upvotes: 3

Related Questions