How React setState different syntax outputs different results? - reactjs

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.

Related

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

React effect that changes state and depends on state

I have a React app that displays stock quotes, the user can add and remove stocks from the list etc. I have an effect to fetch the prices, since it depends on the quotes array, when I add a new quote it'll run the fetch manually. But I also want the prices to be continually updated after a short interval....roughly like below :
function StockApp() {
const [quotes, setQuotes] = useState([]);
// an entry in the quotes array is an object like below
// They come like this straight from the backend which returns
// a simple JSON format
// {
// symbol: 'MSFT',
// price: 298.12
// dayChange: 1.2
// }
useEffect(() => {
const timer = setTimeout(async () => {
const newQuotes = await fetchPrices(quotes);
// this will be a new array
setQuotes(newQuotes);
}, TICKER_REFRESH_INTERVAL);
return () => clearTimeout(timer);
}, [quotes]);
... render function etc
Although the app works fine, my query is about the recursive nature of my useEffect function....as I understand it I'm saying here "Whenever quotes changes, run the function" but the effect function is also changing quotes, it seems like the setTimeout prevents it recursing. I feel like I am not thinking about the problem in the correct way, how can I avoid this cyclic dependency ? Do I need to ?

Infinite loop in useEffect fix?

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.

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