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 ofuseEffect
every time whenitems
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 thathook
from whatever is callingupdateItem
... The way you're usinguseEffect
is incorrect, so you need to create handler that will call bothupdateItem
, then get items from local storage, then callsetItems
... Don't dosetItems(getCart())
inside ofuseEffect
unless you specify an empty array as a dependency so that it only callssetItems
once to get cart items after it renders, then you have something else that will update youritems
state – goto Commented Aug 18, 2019 at 20:15
3 Answers
Reset to default 2As 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());
}, []);