Understand useEffect second param - reactjs

i doing something that is good for my case, but i dont understand how its work, i will be happy to explanation comments.
i try to do function that every 5 seconds change the background image.
1.my first try been:
(i have mainImages array with 3 images for example.)
const [index, setIndex] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setIndex(index+1)
(index > mainImages.length-1)? console.log("Bigger",index):console.log("smaller","i="i)
}, 5000);
return () => clearInterval(interval);
}, []);
in this case.
every 5 sec i get -"smaller",i=0 - for each interval even after 3 or more intervals
the weird part: the Images is changes like expected. even the index still 0.
2.my second try: i get what i Expected also in the index and the images.
the only changes between its tha i put [index] to use effect sec param.
useEffect(() => {
thesame....
}, [index]);
in this case.
every 5 sec i get the right console (
every 5 sec i get -"smaller",i=0
"smaller",i=1
"smaller",i=2
"Bigger",i=3.......
so my question: what is happend around behind the scenes ther, and how its affect like that? how its working?

The dependency array is used to decide when to throw out the old effect and start a new one. So with an empty array, the effect only runs once on mount, then does its teardown on unmount. That means you set up the interval once. The index const in the closure has a value of 0, and that value will never change.
When you changed it to have index in the dependency array, now every time the component renders, if the index changed it will teardown the old interval and start a new one. For each of these, the value of index in the closure is whatever the value was when the effect ran. So 0 for the first one, then 1 for the second, etc.
Note that with this second approach, the intervals will only ever go off once each. So really, they're more like timeouts instead of intervals. If you wanted, you could change it to setTimeout, and the result would be basically the same.
But for your specific case, there's an even better approach: use the callback version of setIndex. Rather than relying on the closure to know the value of index, you can let react tell you what the latest value of index is:
const [index, setIndex] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setIndex(prevIndex => {
(prevIndex > mainImages.length-1)
? console.log("Bigger", prevIndex)
: console.log("smaller","i="i);
return prevIndex + 1;
});
}, []);
return () => clearInterval(interval);
}, []);
// If you don't need the logging, then setting the state can be simply:
// setIndex(prevIndex => prevIndex + 1)

Related

React useState(), what is the correct way to wait for multiple setState() calls?

In a function inside a functional react component I may update multiple state variables. How can I ensure all state changes have taken place before running the next function.
Below, for example, imagine that calculateScore() is triggered on some event (perhaps some clicks in a game) and adds to the value of either a, b or both. I then wan't to run determineWinner(). How can I run determineWinner() in a way which ensures both setA and setB have completed their asynchronous runs so that both a and b are up-to-date (after a single calculateScore() run)?
I didn't find this in the docs or while searching around for a while here on stackOverflow.
Example
function MyComponent() {
let [a, setA] = useState(0);
let [b, setB] = useState(0);
let [winnerText, setWinnerText] = useState("")
function determineWinner() {
if (a > b) {
// Note, I don't want this to happen if the score ends up equal after both setA and setB from calculateScore() completes.
setWinnerText("A won!")
}
}
function calculateScore(aShouldIncrease, bShouldIncrease) {
if (aShouldIncrease) setA(prevA => prevA + 1);
if (bShouldIncrease) setB(prevB => prevB + 1);
determineWinner(); // Doesn't work to run it here, since setA and setB are asynchronous.
}
useEffect(() => {
determineWinner(); // Not guaranteed to work here, since perhaps only setA has completed so far and not setB? Right?
}, [a, b])
determineWinner(); // Even if we imagined I had some other condition to prevent it from being run prematurely, it may run when only a or only b has changed, right?
return <h1>{ winnerText }</h1>
}
I realize I could solve it by making all the calculations within the calculateScore without setting state, but I wonder if there is some point in react where I can be sure all setState() functions started from within a single function call will be completed.
Semantically it sounds like you want to perform an action when "the game score" is updated, not when any individual value therein is updated. So there's a level of encapsulation not accounted for in the current code.
Keep the "game state" in one state object rather than two. For example, consider this:
const [scores, setScores] = useState({a: 0, b: 0});
Then updating that state would be a single "set" operation:
function calculateScore(aShouldIncrease, bShouldIncrease) {
const newScores = { ...scores };
if (aShouldIncrease) {
newScores.a++;
}
if (bShouldIncrease) {
newScores.b++;
}
if (aShouldIncrease || bShouldIncrease) {
setScores(newScores);
}
}
Then since only one state object was updated, you can trigger the effect with just that dependency:
useEffect(() => {
determineWinner();
}, [scores]);
function calculateScore(aShouldIncrease, bShouldIncrease) {
unstable_batchedUpdates(() => {
if (aShouldIncrease) setA(prevA => prevA + 1);
if (bShouldIncrease) setB(prevB => prevB + 1);
determineWinner();
});
}

How setState affects setInterval?

I've coded simple timer, but when I try to console.log(time)(It is in handleStart), I get the same output 0, even if setTime() is called.
Here's the part of code, where I console.log(time) (It is in handleStart, you should click the Start button in order to see console.log):
const handleStart = () => {
setIsCounting(true);
timerIntervalRef.current = setInterval(() => {
console.log(time);
setTime((prevState) => {
localStorage.setItem("time", `${prevState + 1}`);
return prevState + 1;
});
}, 1000);
};
Link to Sandbox
Please, explain me, why it works that way, cause I think, that, the callback in setInterval has a reference to a time, which is defined above, so every time this callback gets called, it should climb through closure to the the point, where time is defined, so it should get renewed value.
time is a local const, which your interval function closes over. As a const, it can never change, so you're always logging out the original value. Even if you used let the behavior would be the same, because calling setTime does not change the value in the local variable. Rather it asks react to rerender your component. On that new render, a new local variable will be created with the new value, but code in the old render (including the code in the setInterval) still only has the old variable in its scope and cannot access the new one.
If you'd like to verify that the component is rerendering, you can move your log statement into the body of the component.
console.log('rendering with', time); // moved outside of handle start
const handleStart = () => {
// ... normal handleStart function, minus the console.log
}
Or if you want a log statement at the time you set the state, you could move it inside the set state function, since that gets passed the latest value of the state and doesn't depend on a closure variable:
setTime((prevState) => {
console.log(prevState);
localStorage.setItem("time", `${prevState + 1}`);
return prevState + 1;
});
inside of settime you are getting the renewed value (prevState), the "time" is referencing to the initial time, consoling will obviously refer to initial value, I think you should console.log(prevState)

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 ?

Strange behavior in React hook state updates in combination with setInterval()

The below code correctly updates the count state, but when outputting the count value with console.log, it is showing very strange behavior if called from a function within an setInterval inside useEffect() hook.
You would expect to see an incremental number in the console.log but the output from the fetchTimelineItems() function is bizar. When the count is 1, the output alternates between 0 and 1. When the count is 2 or more it outputs all the numbers in random order.
See codesandbox link to reproduce this behavior.
The expected behavior is to see the correct count value in the fetchTimelineItems() function.
Thanks in advance for pointing me in the right direction to get this fixed.
const Example = ({ title }) => {
const [count, setCount] = useState(0);
const handleCount = () => {
setCount(count + 1);
console.log(count);
};
function fetchTimelineItems() {
console.log("count from within fetch function: " + count);
}
useEffect(() => {
setInterval(() => {
fetchTimelineItems();
}, 3000)
},[count]);
return (
<div>
<p>{title}</p>
<button onClick={handleCount}>Increase count</button>
</div>
);
};
https://codesandbox.io/s/count-update-s5z94?file=/src/index.js
The useEffect hooks, runs after mounting and updating (depending on your dependency array) of your functional component.
So, it keeps running whenever you update your count.
Now, once you update count for the first time, the useEffect will again run, thus creating a new Interval because of setInterval. This is why you have multiple output statement.
Now, finally, each Interval you create is creating what is called a closure inside it. Inside this closure there is the fetchTimelineItems function along the value of count at that point of time.
So, for every update of count you are creating new intervals like this.
Mount -> Closure with fetchTimelineItems and count = 0,Update count once -> Closure with fetchTimelineItems and count = 1,
Update count again -> Closure with fetchTimelineItems and count = 2,
This is why you have all the values printing in the console.
Why it is printing old values is because that's how closures work in javascript. They remember the values at the time of their creation.

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.

Resources