最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - useEffect and watching state - Stack Overflow

programmeradmin2浏览0评论

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 ponent 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));
    }
}

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 ponent 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));
    }
}
Share Improve this question edited Aug 18, 2019 at 20:05 CaeSea asked Aug 18, 2019 at 19:24 CaeSeaCaeSea 2981 gold badge2 silver badges12 bronze badges 18
  • 1 Possible duplicate of Infinite loop in useEffect – Alexander Staroselsky Commented Aug 18, 2019 at 19:34
  • I have tried the methods in this answer and it has not solved the issue. To be clear I want the ponent to re-render when the items state changes, which can be triggered by a user. – CaeSea Commented Aug 18, 2019 at 19:50
  • Do you need to call setItems inside of useEffect every time when items change? – goto Commented Aug 18, 2019 at 19:52
  • Anytime you call setItems() and change the state the ponent 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 Commented Aug 18, 2019 at 19:52
  • 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 Commented Aug 18, 2019 at 20:15
 |  Show 13 more ments

3 Answers 3

Reset to default 2

As suggested in the ments, 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 ponent

    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 ponent's local state will get updated inside the handleQuantityChange handler.

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 pare 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.

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());
}, []);
发布评论

评论列表(0)

  1. 暂无评论