useEffect causing component to re-render infinitely - javascript

I am adding items to the wishlist and once it is added I am trying to re-render the Heart icon to have a number on it to indicate that the Item has been added to the wishlist. What I want to achieve is similar to Twitter's like button, when someone likes the post the number of likes increases immediately. So, in my case when someone likes (wishlists) the product it will increase the number on top of Heart icon. I am using local storage to get the items that are added to the wishlist, and I can re-render the Wishlist component when a new Item is added inside the effect but it makes the component to re-render infinitely which is giving me a warning of Warning: Maximum update depth exceeded
Here is my effect;
const [wishlist, setWishlist] = React.useState([]);
React.useEffect(() => {
if (typeof window !== "undefined") {
const wishlistStorage =
localStorage.getItem("wishlist") === null
? []
: [...JSON.parse(localStorage.getItem("wishlist"))];
if (wishlistStorage.length > 0) {
setWishlist(wishlistStorage);
}
}
}, [wishlist]);
If I remove the wishlist from dependancy array, I need to refresh the page to see the correct number of items. What am I doing wrong here and what would be the best way to approach this. Thank you!

You can remove the wishlist variable from the dependency array.
When you call setWishlist, it'll update the state, the dependency array will check that it has changed and will call the useEffect once more, thus the infinite cycle.
With an empty dependency array, it'll only run when your component mounts, fetching the data from your local storage and initializing your app. From there on, all you need is to handle the state locally and updating the localstorage with any new wishlist items, but that useEffect seems to be for initialization only.
UPDATE #1
The core issue here was that OP was using LocalStorage to transfer data between components, which is not the correct way of doing things. If you happen to stumble upon OP's problem and you're doing that, make sure to study a little bit about sharing data between components.
The most common ways are Context API, Redux and passing data down as props.
You can read more about context here.

Here you have add wishlist in dependency array of useEffect and in that you have added condition if (wishlistStorage.length > 0) { setWishlist(wishlistStorage); } , because setting again the wishlist it will cause re-render and again useEffect will be triggered.

just remove the wishlist from the dependencies
React.useEffect(() => {
if (typeof window !== "undefined") {
const wishlistStorage =
localStorage.getItem("wishlist") === null
? []
: [...JSON.parse(localStorage.getItem("wishlist"))];
if (wishlistStorage.length > 0) {
setWishlist(wishlistStorage);
}
}
}, []);

If I understand correctly, and you need this only for the initial render, then you can simply remove wishlist from the dependency array.
const [wishlist, setWishlist] = React.useState([]);
React.useEffect(() => {
if (typeof window !== "undefined") {
const wishlistStorage =
localStorage.getItem("wishlist") === null
? []
: [...JSON.parse(localStorage.getItem("wishlist"))];
if (wishlistStorage.length > 0) {
setWishlist(wishlistStorage);
}
}
}, []);

You use wishlist as the deps of useEffect hooks, when the wishlist cache exists, wishlistStorage is an object parsed from the local storage JSON string. Then, you call setWishlist(wishlistStorage), cause the hook re-render, the wishlist will have a new reference. The useEffect will execute again when its dependencies changes. That's why re-render infinitely.
You could use lazy initial state
The initialState argument is the state used during the initial render. In subsequent renders, it is disregarded. If the initial state is the result of an expensive computation, you may provide a function instead, which will be executed only on the initial render
Use the cache from the local storage to initial your state if it exists, otherwise, returns a default value.
E.g:
const [wishlist, setWishlist] = React.useState(() => {
if (typeof window !== "undefined") {
const wishlistStorage =
localStorage.getItem("wishlist") === null
? []
: [...JSON.parse(localStorage.getItem("wishlist"))];
return wishlistStorage;
}
return [];
);

you are using setWishlist in useEffect that's observe wishlist, that's why it's lead you to infinity loop cuz when wishlist value changed it will execute the code in useEffect again and again so for my solution i saw localStorage in useEffect i would think it's would be only checked in first render, so you can simply remove [wishlist] -> []
const [wishlist, setWishlist] = React.useState([]);
React.useEffect(() => {
if (typeof window !== "undefined") {
const wishlistStorage =
localStorage.getItem("wishlist") === null
? []
: [...JSON.parse(localStorage.getItem("wishlist"))];
if (wishlistStorage.length > 0) {
setWishlist(wishlistStorage);
}
}
}, []);

Related

How to update or add an array in react.js?

I'm using react native and my component is based on class base function. I'm facing difficulty in updating or adding object in an array..
My case :
I have an array:
this.state = {
dayDeatil:[]
}
now i want to add an obj in it but before that i want check if that object exist or not.
obj = { partition :1, day:"sunday","start_time","close_time",full_day:false}
in condition i will check partition and day if they both not match. then add an object if exist then update.
here is function in which i'm trying to do that thing.
setTimeFunc =(time)=>{
try{
console.log("time.stringify() ")
let obj = {
partition:this.state.activePartition,
day:this.state.selectedDay.name,
full_day:false,
start_time:this.state.key==="start_time"?time.toString():null
close_time:this.state.key==="close_time"?time.toString():null
}
let day = this.state.dayDetails.filter((item)=>item.day===obj.day&&item.partition===obj.partition)
if (day.length!==0) {
day[this.state.key]=time.toString()
this.setState({...this.state.dayDetail,day})
} else {
console.log("2")
this.setState({
dayDetails: [...this.state.dayDetails, obj]
})
}
this.setState({ ...this.state, clockVisiblity: false });
}
catch(e){
console.log("error -> ",e)
}
}
To check if the object exists or not, you can use Array.find() method, if it doesn't exists, the method will return undefined.
Now to update the state, the easier way would be to create a new array and set it as the new dayDetails state like this:
const { dayDetails } = this.state;
dayDetails.push(newData);
this.setState({ dayDetails: [...dayDetails] })
You should use the spread operator when setting the state because React uses shallow comparision when comparing states for a component update, so when you do [...dayDetails] you're creating a new reference for the array that will be on the state, and when React compares the oldArray === newArray, it will change the UI.
Also, after your else statement, you're updating the state with the state itself, it's good to remember that React state updates are asynchronous, so they won't be availabe right after the setState function call, this may be causing you bugs too.

How to make props equal to state that does not exist yet?

Solved Thank you for your help
I am setting props of component
<Component myprops={state_variable}/>
The problem is that when I am creating the component and setting the props the state variable does not exist yet and my code breaks. What can I do to solve this problem? In addition when I change the state the prop is not updated.
<ServiceTicket
showOverlay={Tickets_disabled_withError[ticket_num]?.isDisabled}
showSelectedError={Tickets_disabled_withError[ticket_num]?.showError}
/>
My intial state initial variable:
const [Tickets_disabled_withError,setTickets_disabled_withError] = useState({})
I am trying to call function that will update state and change value that props is equal to.
const OverLayOnAll = (enable) =>
{
let tempobject = Tickets_disabled_withError
for (let key in tempobject)
{
if (enable == "true")
{
tempobject[key].isDisabled = true
}
else if (enable == "false")
{
tempobject[key].isDisabled = false
}
}
setTickets_disabled_withError(tempobject)
}
I fixed the issue. Thank you so much for your help. I had to set use optional chaining ?. and also re render the component.
The value exists. It's just that the value itself is undefined. You need to set an initial value when defining your state
const [statevariable, setstatevariable] = useState({
somekey: {
isDisabled: false // or whatever the initial value should be
}
}) // or whatever else you need it to be
For your second problem, you are using the same pointer. JavaScript does equality by reference. You've transformed the existing value, so React doesn't detect a change. The easiest way to fix this is to create a shallow copy before you start transforming
let tempobject = {...Tickets_disabled_withError}
Your question isn't very clear to me, but there's a problem in your setTickets_disabled_withError call.
When you update a state property (ticketsDisabledWithError) using its previous value, you need to use the callback argument.
(See https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous)
overlayAll = (enable)=> {
setTicketsDisabledWithError((ticketsDisabledWithError)=> {
return Object.keys(ticketsDisabledWithError).reduce((acc,key)=> {
acc[key].isDisabled = (enabled=="true");
return acc;
}, {}); // initial value of reduce acc is empty object
})
}
Also, please learn JS variable naming conventions. It'll help both you, and those who try to help you.

Getting previous State of useState([{}]) (array of objects)

I am struggling to get the real previous state of my inputs.
I think the real issue Which I have figured out while writing this is my use of const inputsCopy = [...inputs] always thinking that this creates a deep copy and i won't mutate the original array.
const [inputs, setInputs] = useState(store.devices)
store.devices looks like this
devices = [{
name: string,
network: string,
checked: boolean,
...etc
}]
I was trying to use a custom hook for getting the previous value after the inputs change.
I am trying to check if the checked value has switched from true/false so i can not run my autosave feature in a useEffect hook.
function usePrevious<T>(value: T): T | undefined {
// The ref object is a generic container whose current property is mutable ...
// ... and can hold any value, similar to an instance property on a class
const ref = useRef<T>();
// Store current value in ref
useEffect(() => {
ref.current = value;
}); // Only re-run if value changes
// Return previous value (happens before update in useEffect above)
return ref.current;
}
I have also tried another custom hook that works like useState but has a third return value for prev state. looked something like this.
const usePrevStateHook = (initial) => {
const [target, setTarget] = useState(initial)
const [prev, setPrev] = useState(initial)
const setPrevValue = (value) => {
if (target !== value){ // I converted them to JSON.stringify() for comparison
setPrev(target)
setTarget(value)
}
}
return [prev, target, setPrevValue]
}
These hooks show the correct prevState after I grab data from the api but any input changes set prev state to the same prop values.
I think my issue lies somewhere with mobx store.devices which i am setting the initial state to or I am having problems not copying/mutating the state somehow.
I have also tried checking what the prevState is in the setState
setInputs(prev => {
console.log(prev)
return inputsCopy
})
After Writing this out I think my issue could be when a value changes on an input and onChange goes to my handleInputChange function I create a copy of the state inputs like
const inputsCopy = [...inputs]
inputsCopy[i][prop] = value
setInputs(inputsCopy)
For some reason I think this creates a deep copy all the time.
I have had hella issues in the past doing this with redux and some other things thinking I am not mutating the original variable.
Cheers to all that reply!
EDIT: Clarification on why I am mutating (not what I intended)
I have a lot of inputs in multiple components for configuring a device settings. The problem is how I setup my onChange functions
<input type="text" value={input.propName} name="propName" onChange={(e) => onInputChange(e, index)} />
const onInputChange = (e, index) => {
const value = e.target.value;
const name = e.target.name;
const inputsCopy = [...inputs]; // problem starts here
inputsCopy[index][name] = value; // Mutated obj!?
setInputs(inputsCopy);
}
that is What I think the source of why my custom prevState hooks are not working. Because I am mutating it.
my AUTOSAVE feature that I want to have the DIFF for to compare prevState with current
const renderCount = useRef(0)
useEffect(() => {
renderCount.current += 1
if (renderCount.current > 1) {
let checked = false
// loop through prevState and currentState for checked value
// if prevState[i].checked !== currentState[i].checked checked = true
if (!checked) {
const autoSave = setTimeout(() => {
// SAVE INPUT DATA TO API
}, 3000)
return () => {
clearTimeout(autoSave)
}
}
}
}, [inputs])
Sorry I had to type this all out from memory. Not at the office.
If I understand your question, you are trying to update state from the previous state value and avoid mutations. const inputsCopy = [...inputs] is only a shallow copy of the array, so the elements still refer back to the previous array.
const inputsCopy = [...inputs] // <-- shallow copy
inputsCopy[i][prop] = value // <-- this is a mutation of the current state!!
setInputs(inputsCopy)
Use a functional state update to access the previous state, and ensure all state, and nested state, is shallow copied in order to avoid the mutations. Use Array.prototype.map to make a shallow copy of the inputs array, using the iterated index to match the specific element you want to update, and then also use the Spread Syntax to make a shallow copy of that element object, then overwrite the [prop] property value.
setInputs(inputs => inputs.map(
(el, index) => index === i
? {
...el,
[prop] = value,
}
: el
);
Though this is a Redux doc, the Immutable Update Patterns documentation is a fantastic explanation and example.
Excerpt:
Updating Nested Objects
The key to updating nested data is that every level of nesting must be
copied and updated appropriately. This is often a difficult concept
for those learning Redux, and there are some specific problems that
frequently occur when trying to update nested objects. These lead to
accidental direct mutation, and should be avoided.

React hooks, set return value of a function to state causes infinite loop

Infinite loop on setting state
I have a array of objects testData, which I want to filter through to get another array with results:
const testData = [
{
time: "2020-01-23T13:16:53+01:00",
amount: "0.010000000000000000",
txid: "7b1f6aa63910618913c1c1c2902671d2e4f074a8c77ecfd3d16994a05fbf952d"
},
{
time: "2020-01-31T09:09:13+01:00",
amount: "-0.012739560000000000",
txid: "7df38cb2d7538f794d725e4c7e68a3e1e7ee6fd570c3575c53776808c0200145"
},
{
time: "2020-01-31T09:09:24+01:00",
amount: "0.010000000000000000",
txid: "db47ba29a36bd2343af287bd75f489c2f39d7ce1edcf24176c555233b0e24286"
}
];
Code below works almost good, but I cannot set the return value to a state. When I try to useState in a function it gives me infinite loop.
How can I set state of historyResult() return value, so each time when value changes function will call and give me different results.
import React, { useState, useEffect } from 'react';
const History = () => {
const [filteredArray, setFilteredArray] = useState();
// values are coming from Formik. When changed, function should run once more.
const historyResult = values => {
let tType = values && values.transactionType; // Checking if the value exists, saving transactionType of values and saving it to variable
// testData is an array of objects that I want to filter through
let filteredData = testData.filter(item => {
const withdraw = item.amount < 0;
const admission = item.amount > 0;
// Checking which tType was returned and do instructions
const type = tType == 1 ? withdraw : tType == 2 ? admission : console.log('no value');
return type;
});
console.log(filteredData); // This log works beautifully - it gives me data I want (array of objects)
setFilteredArray(filteredData); // I tried to set state on this but got infinite loop. Why's that?
return filteredData;
};
}
How can I set return value of historyResult to a state without infinite loop?
I tried useEffect but I think I got it all wrong and also got infinite loop.
You can use:
setFilteredArray([...filteredData]);
Also initialize your state with initial value for e.g. empty array
const [filteredArray, setFilteredArray] = useState([]);
You don't need to return a value from function. You never do it in the React world, instead you just set your state with the desired results and use that state where you want. (keep in mind that setting state is an async call)
Reason for infinite loop in useEffect:
useEffect calls every time the component state changes and whenever state changes again useEffect gets call and this cycle continues. Here is good guide to prevent infinite loop in the useEffect

How to modify a 'value' prop in VueJS before `$emit('input')` finishes updating it

I have a question about creating VueJS components that are usable with v-model which utilise underlying value prop and $emit('input', newVal).
props: {
value: Array
},
methods: {
moveIdToIndex (id, newIndex) {
const newArrayHead = this.value
.slice(0, newIndex)
.filter(_id => _id !== id)
const newArrayTail = this.value
.slice(newIndex)
.filter(_id => _id !== id)
const newArray = [...newArrayHead, id, ...newArrayTail]
return this.updateArray(newArray)
},
updateArray (newArray) {
this.$emit('input', newArray)
}
}
In the above code sample, if I do two modifications in quick succession, they will both be executed onto the "old array" (the non-modified value prop).
moveIdToIndex('a', 4)
moveIdToIndex('b', 2)
In other words, I need to wait for the value to be updated via the $emit('input') in order for the second call to moveIdToIndex to use that already modified array.
Bad solution 1
One workaround is changing updateArray to:
updateArray (newArray) {
return new Promise((resolve, reject) => {
this.$emit('input', newArray)
this.$nextTick(resolve)
})
}
and execute like so:
await moveIdToIndex('a', 4)
moveIdToIndex('b', 2)
But I do not want to do this, because I need to execute this action on an array of Ids and move them all to different locations at the same time. And awaiting would greatly reduce performance.
Bad solution 2
A much better solution I found is to just do this:
updateArray (newArray) {
this.value = newArray
this.$emit('input', newArray)
}
Then I don't need to wait for the $emit to complete at all.
However, in this case, VueJS gives a console error:
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "value"
Does anyone have any better solution?
OK. These are your options as far as I understand your use case and application.
First of all, don't mutate the props directly save the props internally and then modify that value.
props: {
value: Array
},
data() {
return {
val: this.value
}
}
If the next modification to the array is dependent on the previous modification to the array you can't perform them simultaneously. But you need it to happen fairly quickly ( i will assume that you want the user to feel that it's happening quickly ). What you can do is perform the modification on the val inside the component and not make it dependent on the prop. The val variable is only initialized when the component is mounted. This way you can modify the data instantly in the UI and let the database update in the background.
In other words, your complete solution would look like this:
props: {
value: Array
},
data () {
return {val: this.value}
},
methods: {
moveIdToIndex (id, newIndex) {
const newArrayHead = this.val
.slice(0, newIndex)
.filter(_id => _id !== id)
const newArrayTail = this.val
.slice(newIndex)
.filter(_id => _id !== id)
const newArray = [...newArrayHead, id, ...newArrayTail]
return this.updateArray(newArray)
},
updateArray (newArray) {
this.val = newArray
this.$emit('input', newArray)
}
}
This solution fixes your problem and allows you to execute moveIdToIndex in quick succession without having to await anything.
Now if the array is used in many places in the application next best thing would be to move it to a store and use it as a single point of truth and update that and use that to update your component. Your state will update quickly not simultaneously and then defer the update to the database for a suitable time.
Emit a message to the parent to change the prop.
Put a watcher on the prop (in the child) and put your code to use the new value there.
This keeps the child from mutating the data it does not own, and allows it to avoid using nextTick. Now your code is asynchronous and reactive, without relying on non-deterministic delays.
How about making copy of the value ?
moveIdToIndex (id, newIndex) {
const valueCopy = [...this.value]
const newArrayHead = this.valueCopy
.slice(0, newIndex)
.filter(_id => _id !== id)
const newArrayTail = this.valueCopy
.slice(newIndex)
.filter(_id => _id !== id)
const newArray = [...newArrayHead, id, ...newArrayTail]
return this.updateArray(newArray)

Categories

Resources