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

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.

Related

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)

Understand useEffect second param

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)

I am confused as to how my dependencies change every render?

// We are only running this at initial render and anytime
// yelpResults gets updated (only once per food and location submit)
useEffect(() => {
// Creating a temp array so our restaurantIndexes is immutable
let tempArray = [];
// Concatenating the value of each index into our state
Object.keys(yelpResults).map((index) => tempArray.push(index));
// Saving the results of our restaurant indexes
setRestaurantIndexes(tempArray);
}, [yelpResults, restaurantIndexes]);
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
I think what you are doing wrong is you are re-rendering things when one of these values changes [yelpResults, restaurantIndexes] because useEffect renders again when value changes.So a better solution would be to put an if statement which will check if the value has changed or not.
const [restaurantIndexes, setRestaurantIndexes] = useState("")
useEffect(() => {
// Creating a temp array so our restaurantIndexes is immutable
let tempArray = [];
// Concatenating the value of each index into our state
Object.keys(yelpResults).map((index) => tempArray.push(index));
console.log(tempArray);
//Check if tempArray has changed
if(restaurantIndexes !== tempArray) return
// Set the RestaurantIndex state
setRestaurantIndexes(tempArray);
}, [yelpResults,restaurantIndexes]);

Correct way to use useEffect to update Child Component after replacing state array

I have a parent component "Checkout", where I call my api for the events tickets and save the data to state. I have a simple function that takes the existing ticket information and adds one to the ticket the user selected.
Checkout Component
const handleTicketAdd = (e) => {
// create new ticket array
const newTickets = tickets;
//
newTickets[e.target.id].count++;
console.log(newTickets);
setTickets(newTickets);
console.log(tickets);
}
This function is passed as a prop to the child component "Tickets" and is called in a mapped row.
The function works fine and is updating the count state in the console, but the child component is not re-rendering and the values on screen are staying at the inital value.
I have been researching and have found that componentWillReceiveProps has been replaced with the useEffect hook. I have been trying to get it to work in my child component without sucesss:
Tickets Component
useEffect(()=>{
console.log("Tickets Changed", props.tickets);
}, [props.tickets]);
The log doesn't fire when props.tickets changes, so I know that I am not handling this correctly. Can someone point me in the right direction.
Update
Based on the below feedback I have revised my code and it's almost complete. The problem is that it's adding a new field the array with the count value of the object instead of just updating the count field of the object.
setTickets(prevTickets => ([...prevTickets, prevTickets[e.target.id].count = prevTickets[e.target.id].count + 1])
)
If I try something like poping the last value, I get even more new fields added to the array. How would I achieve removing the additional field that's being generated. The following is not working:
setTickets(prevTickets => ([...prevTickets, prevTickets[e.target.id].count = prevTickets[e.target.id].count + 1], prevTickets.pop()),
)
setTickets(prevTickets => ([...prevTickets, prevTickets[e.target.id].count = prevTickets[e.target.id].count + 1], prevTickets.slice(-1)[0),
)
Alright, so it looks like the way to edit your array's in state should be done with a map function or loop using the previous state. I was able to work it out based on Shubham's feedback.
// update count based on previous state
setTickets(prevTickets => (prevTickets.map((ticket, index) => {
console.log(index, e.target.id);
// if mapped object's id equals the arrays index, it's the value to edit
if (e.target.id == index) {
console.log(index);
const count = ticket.count + 1;
// return object with updated count values
return { ...ticket, count }
}
return ticket;
})
))
This gives me an edit of the object value that I want in my array without any additional fields or values.
You shouldn't be mutating the state while updating, instead you need to clone and update the state
const handleTicketAdd = (e) => {
const id = e.target.id;
setTickets(prevTickets => ([
...prevTickets.slice(0, id),
{
...prevTickets[id],
count: prevTickets[id].count + 1
}
...prevTickets.slice(id + 1);
]));
}

How does React Hooks useCallback "freezes" the closure?

I'd like to know how does React "freezes" the closure while using the useCallback hook (and with others as well), and then only updates variables used inside the hook when you pass them into the inputs parameter.
I understand that the "freeze" may not be very clear, so I created a REPL.it that shows what I mean: https://repl.it/repls/RudeMintcreamShoutcast. Once you open the code, open your web browser console and start clicking on the count button.
How come the value outside compared to the one inside, for the same variable, is different, if they're under the same closure and referencing the same thing? I'm not familiar with React codebase and so I suppose I'm missing an under the hood implementation detail here, but I tried to think how that could work for several minutes but couldn't come up with a good understanding on how React is achieving that.
The first time the component is rendered, the useCallback hook will take the function that is passed as its argument and stores it behind the scenes. When you call the callback, it will call your function. So far, so good.
The second time that the component is rendered, the useCallback hook will check the dependencies you passed in. If they have not changed, the function you pass in is totally ignored! When you call the callback, it will call the function you passed in on the first render, which still references the same values from that point in time. This has nothing to do with the values you passed in as dependencies - it's just normal JavaScript closures!
When the dependencies change, the useCallback hook will take the function you pass in and replace the function it has stored. When you call the callback, it will call the new version of the function.
So in other words, there's no "frozen"/conditionally updated variables - it's just storing a function and then re-using it, nothing more fancy than that :)
EDIT: Here's an example that demonstrates what's going on in pure JavaScript:
// React has some component-local storage that it tracks behind the scenes.
// useState and useCallback both hook into this.
//
// Imagine there's a 'storage' variable for every instance of your
// component.
const storage = {};
function useState(init) {
if (storage.data === undefined) {
storage.data = init;
}
return [storage.data, (value) => storage.data = value];
}
function useCallback(fn) {
// The real version would check dependencies here, but since our callback
// should only update on the first render, this will suffice.
if (storage.callback === undefined) {
storage.callback = fn;
}
return storage.callback;
}
function MyComponent() {
const [data, setData] = useState(0);
const callback = useCallback(() => data);
// Rather than outputting DOM, we'll just log.
console.log("data:", data);
console.log("callback:", callback());
return {
increase: () => setData(data + 1)
}
}
let instance = MyComponent(); // Let's 'render' our component...
instance.increase(); // This would trigger a re-render, so we call our component again...
instance = MyComponent();
instance.increase(); // and again...
instance = MyComponent();
I came here with a similar, rather vague uncertainty about the way useCallback works, its interaction with closures, and the way they are "frozen" by it. I'd like to expand a bit on the accepted answer by proposing to look at the following setup, which shows the working of useCallback (the important aspect is to ignore the linter's warning, for pedagogical reasons):
function App() {
const [a, setA] = useState(0)
const incrementWithUseCallback = useCallback(() => {
// As it closes on the first time `App` is called, the closure is "frozen" in an environment where a=0, forever
console.log(a)
setA(a + 1)
}, []) // but.. the linter should complain about this, saying that `a` should be included!
const incrementWithoutUseCallback = () => {
// This will see every value of a, as a new closure is created at every render (i.e. every time `App` is called)
console.log(a)
setA(a + 1)
}
return (
<div>
<button onClick={incrementWithUseCallback}>Increment with useCallback</button>
<button onClick={incrementWithoutUseCallback}>Increment without useCallback</button>
</div>
)
}
So we clearly see that useCallback effectively "freezes" its closure at a certain moment in time, which is a concept that must be understood clearly, in order to avoid confusing problems, which are sometimes also referred as "stale closures". This article probably does a better job of explaining it than me: https://tkdodo.eu/blog/hooks-dependencies-and-stale-closures
Here's a slightly another view on example code provided by Joe Clay, which emphasizes closure context in which callback is called.
//internal store for states and callbacks
let Store = { data: "+", callback: null };
function functionalComponent(uniqClosureName) {
const data = Store.data;//save value from store to closure variable
const callback = Store.callback = Store.callback || (() => {
console.log('Callback executed in ' + uniqClosureName + ' context');
return data;
});
console.log("data:", data, "callback():", callback());
return {
increase: () => Store.data = Store.data + "+"
}
}
let instance = functionalComponent('First render');
instance.increase();
instance = functionalComponent('Second render');
instance.increase();
instance = functionalComponent('Third render');
As you see, callback without dependencies will be always executed in the closure where it was memorized by useCallback, thus 'freezing' closure.
It happens because when function for callback is created, it is created only once, during first 'render'. Later this function is re-used, and use value of data which was recorded from Store.data during first call.
In the next example you can see the closure 'freezing' logic "in essence".
let globalX = 1;
const f = (() => {
let localX = globalX; return () => console.log(localX); }
)();
globalX = 2;//does not affect localX, it is already saved in the closure
f();//prints 1

Resources