0

In nextJs I created a provider for useState, now I am able to add items to the cart, but that's all that I can do...

cart = [{product: "product name", count:1}]

When I want to add an item to this I can do

setCart([...cart, {product:'new one', count:1}])

But I am just not able to write code that will update the count of existing items in the cart...

I don't know how...

0stone0
  • 34,288
  • 4
  • 39
  • 64
BorisKlco
  • 7
  • 3
  • So in few words your goal is to edit an item inside your cart array of products? – Moussa Bistami Jul 10 '23 at 13:33
  • Shouldn't the count be a memoized length of the product array? – Mr. Polywhirl Jul 10 '23 at 13:35
  • Yes , I wanna just edit value inside my cart – BorisKlco Jul 10 '23 at 13:38
  • Does this answer your question? [How do I update states \`onChange\` in an array of object in React Hooks](https://stackoverflow.com/questions/55987953/how-do-i-update-states-onchange-in-an-array-of-object-in-react-hooks) – 0stone0 Jul 10 '23 at 13:39
  • Is "new one" the same product as "product name"? You are just incrementing the count? – Mr. Polywhirl Jul 10 '23 at 13:39
  • -0stone0 : Thank you, typescript just throwing errors and I dont know hot to fix it... -Mr. Polywhirl: That "New One" Is just example that I know how to add new thing into array... But I dont know how to write code for incrementing count of existing object inside array... So in etCart([...cart, {product:'product name', count:1}]) Will cjust add new object.. not update existing one- – BorisKlco Jul 10 '23 at 13:40

3 Answers3

1

You're probably better off using useReducer vs. useState, due to the complexity of your model. Try something like this:

function cartReducer(oldCart, action) {
    switch(action.type) {
        case 'new':
            // we don't have anything unique like id, so prevent collisions on name
            if (oldCart.some(item => item.name === action name)) return oldCart;
            return [...oldCart, {name: action.name, count: 1}];
        case 'increment':
            return oldCart.map((item) => item.name === action.name ? {...item, count: item.count + 1} : item);
        case 'decrement':
            return oldCart.map((item) => item.name === action.name ? {...item, count: item.count - 1} : item);
        default:
            return oldCart
    }
}

function YourComponent(props) {
    const [cart, dispatch] = useReducer(cartReducer, []);
    function createItem(name) {
        dispatch({type: 'new', name});
    }

    function incrementItemCount(item) {
        dispatch({type: 'increment', name: item.name});
    }

    function decrementItemCount(item) {
        dispatch({type: 'decrement', name: item.name});
    }
    //render logic here
}

However, useState has a form that lets you get at the old version of the state if you want to do it that way.

Amy Blankenship
  • 6,485
  • 2
  • 22
  • 45
  • Thank you , I will try it.. but if my model is in this example complex.. This thing is too hard for me.. I cant.. I am lost for 5 min every time when I saw 3 { next to 5 (... I hate this.. I going back to flipping burgers.. Respect for programmers that they understand this shiit called js.. – BorisKlco Jul 10 '23 at 14:54
  • Your model is only complex compared to the types of values useState is intended to manage. – Amy Blankenship Jul 10 '23 at 16:13
0

You cannot just spread the new item. You need to check the existing cart and update according.

In the React demo below, you can click the buttons to add the items to the cart.

const { useCallback, useEffect, useState } = React;

const fetchProducts = Promise.resolve([
  { name: 'Apple' }, { name: 'Banana' }, { name: 'Pear' }
]);

const App = () => {
  const [products, setProducts] = useState([]);
  const [cart, setCart] = useState([]);

  const addToCart = useCallback((e) => {
    const product = e.target.textContent;
    setCart((existingCart) => {
      const cartCopy = structuredClone(existingCart);
      let existing = cartCopy.find((item) => item.product === product);
      if (existing) {
        existing.count += 1;
      } else {
        cartCopy.push({ product, count: 1 });
      }
      return cartCopy;
    });
  }, []);

  useEffect(() => {
    fetchProducts.then(setProducts);
  }, []);

  return (
    <div>
      <h2>Products</h2>
      {products.map(({ name }) => (
        <button key={name} onClick={addToCart}>{name}</button>
      ))}
      <h2>Cart</h2>
      <ul>
        {cart.map(({ product, count }) => (
          <li key={product}>{`${product} ×${count}`}</li>
        ))}
      </ul>
    </div>
  );
};

ReactDOM
  .createRoot(document.getElementById("root"))
  .render(<App />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>
Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132
0

Because React uses Object.is to compare equality we need to ensure that the new object we pass to setState has a different reference.

I'd do this with a little custom hook. It could look something like so:

function useStateArray<T>(initialVal: T[]) {
  const [arr, setArr] = useState(initialVal);

  const push = React.useCallback(
    (val: T) => setArr((old) => [...old, val]),
    []
  );
  const edit = React.useCallback((val: Partial<T>, index: number) => {
    setArr((old) => {
      const newArr = [...old];
      newArr[index] = { ...newArr[index], ...val };
      // We need to return a new array to update the state!
      return newArr;
    });
  }, []);

  return [arr, push, edit, setArr];
}

The idea being that if you're using arrays in your useState a lot it might make sense to have a few helper functions ready to go.

You could then add some other utility functions (like delete, pop, etc).

Joaquim Esteves
  • 400
  • 2
  • 12