1

I am trying to setState of items on render using a function call and then watch the items state for changes to cause re-render if they change. Passing a reference to the object keys as per the suggested answer does not seem to change anything. I am trying to do this using the hook useEffect(). getCart() is an function to retrieve data from localStorage. Code:

const [items, setItems] = useState([]);

useEffect(() => {
    setItems(getCart());
}, [items]);

I am getting an error "Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render."

I understand how I am causing an infinite loop by effectively changing the items state on render and then this causes a re-render and so on. How would I get around this, is this possible using useEffect? Thanks.

Edit: Code that edits the localStorage

export const updateItem = (productId, count) => {
   let cart = [];
   if (typeof window !== 'undefined') {
        if (localStorage.getItem('cart')) {
            cart = JSON.parse(localStorage.getItem('cart'));
        }
        cart.map((product, index) => {
            if (product._id === productId) {
                cart[index].count = count;
            }
        })
        localStorage.setItem('cart', JSON.stringify(cart));
    }
}
CaeSea
  • 288
  • 1
  • 2
  • 11
  • 1
    Possible duplicate of [Infinite loop in useEffect](https://stackoverflow.com/questions/53070970/infinite-loop-in-useeffect) – Alexander Staroselsky Aug 18 '19 at 19:34
  • I have tried the methods in this answer and it has not solved the issue. To be clear I want the component to re-render when the items state changes, which can be triggered by a user. – CaeSea Aug 18 '19 at 19:50
  • Do you need to call `setItems` inside of `useEffect` **every time** when `items` change? – goto Aug 18 '19 at 19:52
  • Anytime you call setItems() and change the state the component will re-render, just like changing state without hooks using setState(). useEffect is only really need if you are trying to introduce side effects on mount, unMount, or re-render. You may need to clarify what “changed by the user” means exactly in terms of your code. You need to call setItems when you want a re-render, it’s as simple as that. Honestly, you may not even need useEffect at all, but nots clear with what has been provided so far. – Alexander Staroselsky Aug 18 '19 at 19:52
  • @goto1 I do yes. – CaeSea Aug 18 '19 at 19:54
  • @AlexanderStaroselskySo would it be better to not use useEffect and use a lifecycle method? – CaeSea Aug 18 '19 at 19:56
  • @CaeSea what updates the cart? Ideally, show the code that does it – goto Aug 18 '19 at 19:56
  • @goto1 If a user updates a quantity of an existing cart item – CaeSea Aug 18 '19 at 19:57
  • So why not just call `setItems` there? Whatever function/handler handles this action. – goto Aug 18 '19 at 19:57
  • code that updates localStorage moved to question** – CaeSea Aug 18 '19 at 19:59
  • Yes as @goto1 suggested whatever is handling the change to cart or similar, call setItems() there. Also put the code into the question, not the comments. – Alexander Staroselsky Aug 18 '19 at 20:00
  • @goto1 this function is within a separate file and imported – CaeSea Aug 18 '19 at 20:00
  • So why are you using `useEffect` with the `setItems` inside of it? How do you even know when it should be called when you're updating `localStorage` in the function above, which will not trigger `react` to rerender – goto Aug 18 '19 at 20:02
  • BTW, setting local storage is a perfect use of useEffect() as that is a side effect. – Alexander Staroselsky Aug 18 '19 at 20:04
  • Ok, thank you. I understand what you are saying and will take a look now to try and solve this issue. I am following a tutorial and this is my first time using hooks. I think the video may have skipped over something. – CaeSea Aug 18 '19 at 20:13
  • 1
    You need to rethink this a little bit. if you want to use `setItems`, then I'd suggest calling that `hook` from whatever is calling `updateItem`... The way you're using `useEffect` is incorrect, so you need to create handler that will call both `updateItem`, then get items from local storage, then call `setItems`... Don't do `setItems(getCart())` inside of `useEffect` unless you specify an empty array as a dependency so that it only calls `setItems` once to get cart items after it renders, then you have something else that will update your `items` state – goto Aug 18 '19 at 20:15
  • @goto1 Thank you for your help. In the end I created a new function that called setItems() and called it after each call of updateItem. Makes much more sense, could you put your comment as an answer so I could mark it as correct? – CaeSea Aug 18 '19 at 20:51
  • @CaeSea sure, thanks. Glad you were able to get it working. – goto Aug 18 '19 at 23:20

3 Answers3

2

As suggested in the comments, the solution is to move the setItems call out of useEffect and call it somewhere else along with the updateItem function that saves/updates data in localStorage:

// cart.js
export const updateItem = (productId, count) => {
  // see implementation above
}

// App.js
function App() {
  const [items, setItems] = useState([])

  useEffect(() => {
    const cartItems = getCart()
    setItems(cartItems)

  // pass an empty dependency array so that this hook
  // runs only once and gets data from `localStorage` when it first mounts
  }, [])

  const handleQuantityChange = (data) => {
    // this will update `localStorage`, get new cart state
    // from `localStorage`, and update state inside of this component

    updateItem(data.productId, data.count)
    const currentCart = getCart()
    setItems(currentCart)
  }

  return (
    <div>
      {...}
      <button onClick={() => handleQuantityChange(...)>
        Add more
      </button>
    </div>
  ) 
}

This way, calling setItems is fine since it will only get triggered when the button gets clicked.

Also, since useEffect takes an empty dependency array, it will no longer cause the maxiumum update depth exceeded error as it will only run once to get the initial state from localStorage as soon as it renders, then this component's local state will get updated inside the handleQuantityChange handler.

goto
  • 4,336
  • 15
  • 20
1

If you want to use an array inside useState, you need to make sure the array has a different reference every time its value changes.

You can try this in the console:

a = [1, 2, 3] // a = [1, 2, 3]
b = a         // b = [1, 2, 3]
a.push(4)     // a = b = [1, 2, 3, 4]
b === a       // true

Notice b is still equal to a even if the values inside a change. What happens is React.useState and React.useEffect uses a simple === to compare old to new state. To make sure it sees the a different array every time, use the rest operator to copy all the contents of a into b, like so:

a = [1, 2, 3] // a = [1, 2, 3]
b = [...a]    // b = [1, 2, 3]
a.push(4)     // a = [1, 2, 3, 4], b = [1, 2, 3]
b === a       // false

If you do this, useEffect will only be called if the data is really different.

Another thing you should be careful is to not call setItems inside useEffect as an effect of [items]. You should put the result of getCart() in another state variable.

Danilo Fuchs
  • 911
  • 8
  • 17
0

Remove 'items' from the dependency array. This way it will fetch the items from local storage on the first render only (which is what you want). Any other update to items should be done by the user and not triggered automatically by every re-render.

const [items, setItems] = useState([]);

useEffect(() => {
    setItems(getCart());
}, []);
  • The problem with this is that when items is changed by an action by the user the component does not re-render which is what I want – CaeSea Aug 18 '19 at 19:44
  • @CaeSea Try `useEffect(() => {}, [items]);` – Baruch Aug 18 '19 at 20:17
  • You can use multiple useEffect-hooks in the same component. For your case (which I don't really see the point of), you can add an empty useEffect function with items in the dependency array as @Baruch point out. – Robin Börjesson Aug 18 '19 at 20:56