3

I am making a simple e-commerce website but I've ran into an issue where useEffect() won't fire after making a state change. This code snippet I'll include is for the "shopping cart" of the website and uses localStorage to store all items in the cart. My state will change when quantity changes in the QuantChange() function but will not trigger useEffect(). When I refresh the page after changing an item's quantity, the new quantity won't persist and the old quantity is shown instead. What am I doing wrong? Thanks in advance.

import React, { useState, useEffect } from 'react';
import { SetQuantity } from '../utils/Variables';
import { CartItem } from './CartItem';

const CartView = () => {
    const [state, setState] = useState(
        JSON.parse(localStorage.getItem('cart-items'))
            ? JSON.parse(localStorage.getItem('cart-items'))
            : []
    );

    useEffect(() => {
        console.log('Updating!');
        updateLocalStorage();
    });

    const updateLocalStorage = () => {
        localStorage.setItem('cart-items', JSON.stringify(state));
    };

    const quantChange = (event) => {
        setState((prevState) => {
            prevState.forEach((item, index) => {
                if (item._id === event.target.id) {
                    item.quantity = SetQuantity(parseInt(event.target.value), 0);
                    prevState[index] = item;
                }
            });

            return prevState;
        });
    };

    const removeItem = (id) => {
        setState((prevState) => prevState.filter((item) => item._id != id));
    };

    // Fragments need keys too when they are nested.
    return (
        <>
            {state.length > 0 ? (
                state.map((item) => (
                    <CartItem
                        key={item._id}
                        ID={item._id}
                        name={item.name}
                        quantity={item.quantity}
                        changeQuant={quantChange}
                        delete={removeItem}
                    />
                ))
            ) : (
                <h1 className="text-center">Cart is Empty</h1>
            )}
        </>
    );
};

export default CartView;

import React, { Fragment } from 'react';
import { MAX_QUANTITY, MIN_QUANTITY } from '../utils/Variables';

export const CartItem = (props) => {
    return (
        <>
            <h1>{props.name}</h1>
            <input
                id={props.ID}
                type="number"
                max={MAX_QUANTITY}
                min={MIN_QUANTITY}
                defaultValue={props.quantity}
                onChange={props.changeQuant}
            />
            <button onClick={() => props.delete(props.ID)} value="Remove">
                Remove
            </button>
        </>
    );
};
export const MIN_QUANTITY = 1;

export const MAX_QUANTITY = 99;

// Makes sure the quantity is between MIN and MAX
export function SetQuantity(currQuant, Increment) {
    if (Increment >= 0) {
        if (currQuant >= MAX_QUANTITY || (currQuant + Increment) > MAX_QUANTITY) {
            return MAX_QUANTITY;
        } else {
            return currQuant + Increment;
        }
    } else {
        if (currQuant <= MIN_QUANTITY || (currQuant + Increment) < MIN_QUANTITY) {
            return MIN_QUANTITY;
        } else {
            return currQuant + Increment;
        }
    }
}
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
G T
  • 31
  • 3
  • 2
    Unrelated to your question, but couldn't `JSON.parse(localStorage.getItem('cart-items')) ? JSON.parse(localStorage.getItem('cart-items')) : []` be rewritten more simply as `JSON.parse(localStorage.getItem('cart-items')) || []`? – Alexander Nied Dec 02 '21 at 05:31
  • @AlexanderNied Yes I suppose it could. Thanks – G T Dec 02 '21 at 06:27

1 Answers1

4

You are not returning new state, you are forEach'ing over it and mutating the existing state and returning the current state. Map the previous state to the next state, and for the matching item by id create and return a new item object reference.

const quantChange = (event) => {
  const { id, value } = event.target;
  setState((prevState) => {
    return prevState.map((item) => {
      if (item._id === id) {
        return {
          ...item,
          quantity: SetQuantity(parseInt(value), 0)
        };
      }
      return item;
    });
  });
};

Then for any useEffect hook callbacks you want triggered by this updated state need to have the state as a dependency.

useEffect(() => {
  console.log('Updating!');
  updateLocalStorage();
}, [state]);
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • The reason this will work is that React will check to see if the state has changed by plugging the values into `Object.is(prevState, newState)`. You need to create a new reference so React will know something changed, usually by creating a new object. – windowsill Dec 02 '21 at 08:05