Infinite loop in useEffect fix? - reactjs

I am currently pulling data from a firebase realtime database and the data will initial populate. When I refresh the page though, the contents disappear. I added an empty array at the end of the UseEffect() to stop the infinite loop issue that we were having, but it seems to stop updating our array when refreshed.
useEffect(() => {
let jobs = [];
firebase.database().ref("1h5GOL1WIfNEOtcxJVFQ0x_bgJxsPN5zJgVJOePmgJOY/Jobs").on("value", snapshot => {
snapshot.forEach(snap => {
jobs.push(snap.val());
});
})
populateJobs(jobs);
},[]);

As ray commented, it does matter how populateJobs is defined. But at a first guess, you'll need to call that from inside the callback:
useEffect(() => {
firebase.database().ref("1h5GOL1WIfNEOtcxJVFQ0x_bgJxsPN5zJgVJOePmgJOY/Jobs").on("value", snapshot => {
let jobs = [];
snapshot.forEach(snap => {
jobs.push(snap.val());
});
populateJobs(jobs);
})
},[]);

I assume populateJobs is function declared in your scope.
If so, you may want to wrap it in useCallback to ensure the function reference doesn't change.
const populateJobsFixed= useCallback(populateJobs, [])
useEffect(() => {
...
populateJobsFixed(jobs);
},[populateJobsFixed]);

populateJobs is a dependency of the useEffect
You need to have the dependency list be
},[populateJobs]);
Instead of an empty array

The second argument of useEffect(()=>{},[]) which takes an array of argument tells react on what changes your callback passes there should run.
If passed an empty array it runs only once, behaving like componentdidmount method
When a variable is passed, it runs every time the value of the variable is changed.
Also if we pass an object as second parameter it will check for the reference change too.

Related

How React setState different syntax outputs different results?

I have a state the is an array of strings, containing all the messages recived. And a useEffect that triggers when a new message is recived.
So I try to use set state to append the new message to the message board but it generates a starnge result.
Original idea: spreading the contents to the state as usual.
useEffect(() => {
socket.on("recive_message", (data) => {
setBoard((chats) => [data.message, ...chats])
})
}, [])
but this way there is no appending and the chat board first element is being replaced with the incoming message..
So I tried another way I saw on the internet:
useEffect(() => {
socket.on("recive_message", (data) => {
setBoard([data.message, ...board])
})
}, [])
And this works just fine.
What is the difference between the two syntax?
the first example is correct, are you initializing the state with an empty array? if not that can lead to some issue (you can't spread undefined).
the second example is incorrect because you don't have the variable board as useEffect dependency, so it won't update and will always have the initial value.

React context not being up to date in a Timeout

I am using react app context to store an array of "alerts objects" which is basically any errors that might occur and I would want to show in the top right corner of the website. The issue I am having is that the context is not being up to date inside a timeout. What I have done for testing is gotten a button to add an alert object to the context when clicked and another component maps through that array in the context and renders them. I want them to disappear after 5 seconds so I have added a timeout which filters the item that got just added and removes it. The issue is that inside the timeout the context.alerts array seems to have the same value as 5 seconds ago instead of using the latest value leading to issues and elements not being filtered out. I am not sure if there's something wrong with my logic here or am I using the context for the wrong thing?
onClick={() => {
const errorPopup = getPopup(); // Get's the alert object I need
context.setAlerts([errorPopup, ...context.alerts]);
setTimeout(() => {
context.setAlerts([
...context.alerts.filter(
(element) => element.id !== errorPopup.id,
),
]);
}, 5000);
}}
onClick={() => {
const errorPopup = getPopup(); // Get's the alert object I need
context.setAlerts([errorPopup, ...context.alerts]);
setTimeout(() => {
context.setAlerts(alerts => [
...alerts.filter(
(element) => element.id !== errorPopup.id,
),
]);
}, 5000);
}}
This should fix it. Until react#17 the setStates in an event handler are batched ( in react#18 all setStates are batched even the async ones ), hence you need to use the most fresh state to make the update in second setAlerts.
To be safe it's a good practice using the cb syntax in the first setState as well.
I think the fix would be to move context.setAlerts(...) to a separate function (say removePopupFromContext(id:string)) and then call this function inside the setTimeout by passing the errorPopup.Id as parameter.
I'm not sure of your implementation of context.setAlerts, but if it's based on just setState function, then alternatively, you could do also something similar to how React let's you access prevState in setState using a function which will let you skip the creation of the extra function which may lightly translate to:
setContext(prevContextState =>({
...prevContextState,
alerts: prevContextState.alerts.filter(your condition)
)})

useEffect not triggering when object property in dependence array

I have a context/provider that has a websocket as a state variable. Once the socket is initialized, the onMessage callback is set. The callback is something as follows:
const wsOnMessage = (message: any) => {
const data = JSON.parse(message.data);
setProgress(merge(progress, data.progress));
};
Then in the component I have something like this:
function PVCListTableRow(props: any) {
const { pvc } = props;
const { progress } = useMyContext();
useEffect(() => {
console.log('Progress', progress[pvc.metadata.uid])
}, [progress[pvc.metadata.uid]])
return (
{/* stuff */}
);
}
However, the effect isn't triggering when the progress variable gets updated.
The data structure of the progress variable is something like
{
"uid-here": 0.25,
"another-uid-here": 0.72,
...etc,
}
How can I get the useEffect to trigger when the property that matches pvc.metadata.uid gets updated?
Or, how can I get the component to re-render when that value gets updated?
Quoting the docs:
The function passed to useEffect will run after the render is
committed to the screen.
And that's the key part (that many seem to miss): one uses dependency list supplied to useEffect to limit its invokations, but not to set up some conditions extra to that 'after the render is committed'.
In other words, if your component is not considered updated by React, useEffect hooks just won't be called!
Now, it's not clear from your question how exactly your context (progress) looks like, but this line:
setProgress(merge(progress, data.progress));
... is highly suspicious.
See, for React to track the change in object the reference of this object should change. Now, there's a big chance setProgress just assignes value (passed as its parameter) to a variable, and doesn't do any cloning, shallow or deep.
Yet if merge in your code is similar to lodash.merge (and, again, there's a huge chance it actually is lodash.merge; JS ecosystem is not that big these days), it doesn't return a new object; instead it reassigns values from data.progress to progress and returns the latter.
It's pretty easy to check: replace the aforementioned line with...
setProgress({ ...merge(progress, data.progress) });
Now, in this case a new object will be created and its value will be passed to setProgress. I strongly suggest moving this cloning inside setProgress though; sure, you can do some checks there whether or not you should actually force value update, but even without those checks it should be performant enough.
There seems to be no problem... are you sure pvc.metadata.uid key is in the progress object?
another point: move that dependency into a separate variable after that, put it in the dependency array.
Spread operator create a new reference, so it will trigger the render
let updated = {...property};
updated[propertyname] =value;
setProperty(()=>updated);
If you use only the below code snippet, it will not re-render
let updated = property; //here property is the base object
updated[propertyname] = value;
setProperty(()=>updated);
Try [progress['pvc.metadata.uid']]
function PVCListTableRow(props: any) {
const { pvc } = props;
const { progress } = useMyContext();
useEffect(() => {
console.log('Progress', progress[pvc.metadata.uid])
}, [progress['pvc.metadata.uid']])
return (
{/* stuff */}
);
}

useEffect infinite loop when getting data from database

In my project, for sending a request for getting my user data and show them. I wrote the above code but i realised that if i pass the "people" to useEffect's dependency (second parameter) react sends infinite request to my firebase but if i delete and keep the second parameter empty the useEffect works correct what is the difference between these two?
Here is the code that goes to infinite loop:
const [people, setPeople]=useState([])
useEffect(() => {
const unsubscribe=database.collection("people").onSnapshot(snapshot=>
setPeople(snapshot.docs.map(doc=>doc.data()))
)
return () => {
unsubscribe()
}
}, [people]) // if i change the second parameter with an empty list this problem solved.
return (
<div>
<h1>TinderCards</h1>
<div className="tinderCards_cardContainer">
{people.map(person =>
<TinderCard
className="swipe"
key={person.name}
preventSwipe={["up","down"]}
>
<div style={{backgroundImage: `url(${person.url})`}} className="card">
<h3>{person.name}</h3>
</div>
</TinderCard>
)}
</div>
</div>
)
Essentially, the useEffect hook runs the inner function code every time any of the dependencies in the dependency array (second parameter) change.
Since setPeople changes people, the effect keeps running in an infinite loop:
useEffect(() => {
... setPeople() ... // <- people changed
}, [people]); // <- run every time people changes
If you needed somehow the value of people and you need to have it in the dependency array, one way to check is if people is not defined:
useEffect(() => {
if (!people) {
// ... do something
setPeople(something);
}
}, [people]);
As you correctly pointed out, simply taking off the people dependency tells the effect to only run once, when the component is "mounted".
On an extra note, you may be wondering why people is changing if you are fetching the same exact results. This is because the comparison is shallow, and every time an array is created, it's a different object:
const a = [1,2,3];
const b = [1,2,3];
console.log(a === b); // <- false
You would need to do deep equality checks for that.
The issue is after you set state in useEffect, the people value will be changed which will trigger another useEffect call hence an infinite loop.
You can modify it to this:-
useEffect(() => {
const unsubscribe=database.collection("people").onSnapshot(snapshot=>
setPeople(snapshot.docs.map(doc=>doc.data()))
)
return () => {
unsubscribe()
}
}, [])
PROBLEM
useEffect runs every time when any one of values given to dependency array changes. Since, you're updating your people after the db call. The reference to array people changes, hence triggering an infinite loop on useEffect
SOLUTION
You do not need to put people in the dependency array.
Your useEffect function doesn't depend on people.
useEffect(() => {
const unsubscribe=database.collection("people").onSnapshot(snapshot=>
setPeople(snapshot.docs.map(doc=>doc.data()))
)
return () => {
unsubscribe()
}
}, [])
main problem is that the people array which is being created at every set-call is not the same. The object is completely different.
I also had this trouble as i want to display the contents as soon as the some new "people" is added to the database from the admin panel, but it turns out that without refreshing this thing cannot be solved otherwise u can make your own hook with PROPER comparisons .
Maybe u can try by comparing the length of the PEOPLE array. I haven't tried it yet but i think it will work.

useState not updating correctly with socket.io

I am pulling data via socket.io but when listening for incoming messages from the server my state is not updating correctly.
I can see the data is being pulled correctly from the socket (50 reqs peer second) but setQuotes just replaces the existing item with the new item returned from the server (so my state always has a length of one).
const [quotes, setQuotes] = useState([])
const subscribe = () => {
let getQuote = 'quote.subscribe';
socket.emit(getQuote, {});
socket.on('listenAction', function (msg) {
setQuotes([...quotes, msg]) // This just replace the entire state instead of adding items to the existing array
});
}
Subscribe // Open web socket stream
You need to move the listener outside of the subscribe function.
Since it is a sideEffect you should wrap it in React.useEffect
It's also a good idea to use functional setstate to read previous values.
React.useEffect(() => {
socket.on('listenAction', function (msg) {
setQuotes((quotes) => [...quotes, msg])
});
}, []);
In your example you use setState like this
setQuotes([...quotes, msg])
And every time you get message, javascript engine tries to find "quotes" variable in scope of this function
function (msg) {
It can not find it here and move to the scope, where this function was defined ( scope of component function).
For every function call, there is new scope. And react calls component function for each render. So, your function with "msg" in arguments, where you use setQuotes, every time uses "quotes" state from first render.
In first render you have an empty array.
Then you have [...firstEmptyArray, firstMessage]
Then you have [...firstEmptyArray, secondMessage]
Then you have [...firstEmptyArray, thirdMessage].
Probably, you can fix it if you will use setQuotes like;
setQuotes(oldQuotes => [...oldQuotes, msg]);
In this way it will use previousValue for calclulating new one.
PS. Be aware of not to call your subscribe function directly in each render and put it in useEffect with correct dependency array as second argument.
You can use the concat func over here
setQuotes(prevState => prevState.concat(msg))

Resources