Closure when reference to function state using useState Hook - javascript

This code is from react document:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // This effect depends on the `count` state
}, 1000);
return () => clearInterval(id);
}, []); // 🔴 Bug: `count` is not specified as a dependency
return <h1>{count}</h1>;
}
And they said that:
The empty set of dependencies, [], means that the effect will only run once when the component mounts, and not on every re-render. The problem is that inside the setInterval callback, the value of count does not change, because we’ve created a closure with the value of count set to 0 as it was when the effect callback ran. Every second, this callback then calls setCount(0 + 1), so the count never goes above 1.
But isn't the function in setInterval is closure so its count variable reference to count state of Counter, when setCount run Count will change and so function in setInterval must be able to get new value of count? Am i missing something?

But isn't the function in setInterval is closure so its count variable reference to count state of Counter, when setCount run Count will change and so function in setInterval must be able to get new value of count?
Not really. Closures close over individual variables, or variable environments. With React, when a component re-renders, the whole function runs again, creating new bindings for each variable inside.
For example, the first time Counter runs, it does:
const [count, setCount] = useState(0);
creating those two variables. Since the useEffect has an empty dependency array, its callback will only run once, on mount, so those two variables are what its closure will reference.
But then once the state changes, and the component re-renders, the function runs this line again:
const [count, setCount] = useState(0);
creating those two new variables again, in a new scope. This new count will be incremented, but the interval callback will not have closed over it.
Here's a live snippet that demonstrates the issue in vanilla JS:
let i = 0;
const getRenderValue = () => i++;
const Component = () => {
const thisRenderValue = getRenderValue();
console.log('Rendering with thisRenderValue of', thisRenderValue);
if (thisRenderValue === 0) {
setInterval(() => {
console.log('Interval callback sees:', thisRenderValue);
// Run component again:
Component();
}, 1000);
}
};
Component();

You should use prev state value for updating.
useEffect(() => {
const id = setInterval(() => {
setCount((prev) => prev + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);

Related

React, can't access updated value of state variable inside function passed to setInterval() in useEffect()

I am building a simple clock app with React. Currently the countDown() function works, but I would like the user to be able to stop/start the clock by pressing a button. I have a state boolean called paused that is inverted when the user clicks a button. The trouble is that after the value of paused is inverted, the reference to paused inside the countDown() function passed to setInterval() seems to be accessing the default value of paused, instead of the updated value.
function Clock(){
const [sec, setSecs] = useState(sessionLength * 60);
const [paused, setPaused] = useState(false);
const playPause = () => {
setPaused(paused => !paused);
};
const countDown = () => {
if(!paused){
setSecs(sec => sec - 1)
}
}
useEffect(() => {
const interval = setInterval(() => {
countDown();
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
I'm assuming it has something to do with the asynchronous nature of calls to setState() in React, and/or the nature of scoping/context when using regular expressions. However I haven't been able to determine what is going on by reading documentation related to these concepts.
I can think of some workarounds that would allow my app to function as desired. However I want to understand what is wrong with my current approach. I would appreciate any light anyone can shed on this!
In your code, the useEffect is called only once when mounting the component.
The countdown function registered inside will have its initial value at the time when the useEffect/setInterval is called. So paused will only have the value when you initially mount the component. Because you are not calling countDown directly or updating its value inside your useEffect, it is not updated.
I think you could solve this issue like this:
interval.current = useRef(null);
const countDown = () => {
if(!paused){
setSecs(sec => sec - 1)
}
}
useEffect(() => {
clearInterval(interval.current);
interval.current = setInterval(countDown, 1000);
return () => {
clearInterval(interval.current);
};
}, [paused]);
your useEffect is dependent on the value of paused as it needs to create a new interval (with a different countdown function). This will trigger the useEffect not only on mount but every time paused changes. So one solution would be to clear the interval and start a new one with a different callback function.
Edit: You could actually improve it as you only want the interval to be running if the countDown function actually does something so this should work too:
useEffect(() => {
clearInterval(interval.current);
if(!paused) {
interval.current = setInterval(countDown, 1000);
}
return () => {
clearInterval(interval.current);
};
}, [paused]);

Why react useEffect can memorize the state value?

Here's an example:
function Page() {
const [a, setA] = React.useState(0);
useEffect(() => {
const interval = setInterval(() => { console.log(a) }, 2000)
return () => clearInterval(interval)
}, []);
return (
<div>
<span>{a}</span>
<button onClick={() => setA(Math.random())}>button</button>
</div>
);
}
The invertal always log 0 despite the fact that count state variable has actually being increased by clicked the button a few times.
And many people say this is because the interval function capture a. But I really can not undersand it. When a variable can not be found in current environment, it will resolve to parent (or global).
And look at this example in contrast:
function Page() {
let a = 0;
useEffect(() => {
const interval = setInterval(() => { console.log(a) }, 2000)
return () => clearInterval(interval)
}, []);
return (
<div>
<button onClick={() => a = Math.random()}>button</button>
</div>
);
}
This is an obvious example for my expression. The interval closure capture the outer a variable. It will log the fresh value as it changes.
So how react useEffect implement this feature?
As my view (a bad pseudocode):
const hooks = [];
function useState(val) {
let state = hooks[0] || val;
hooks[0] = state;
function setVal(v) {
state = v;
hooks[0] = state;
}
return [state, setVal];
}
let cleanup = null;
function useEffect(callback) {
if (cleanup) cleanup()
cleanup = callback();
}
function Foo() {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => { console.log(count)}, 1000);
})
return setCount;
}
var setCount = Foo(); // log 0
setCount(1);
Foo(); // log 1
But this is not way react was implement. So how react implement this?
Each time the component is rendered, the function is called.
First a value is assigned to a. This is either 0 (the first time) or the current value from the state (in subsequent times).
const [a, setA] = React.useState(0);
Then there is a useEffect hook. This takes two arguments. A function and a dependency array.
The function will be called every time the values in the dependency array change.
In your example, the dependency array is []. So the values never change. This means the function will only be called on the first render.
During the first render, the function it called. The value of a is 0.
The function passes a function to to setInterval. This function reads a. It has closed over the a variable from the first render.
In subsequent renders of the function, there will be a new a variable (belonging to this call to the function) which is assigned whatever value is in the state. The effect hook doesn't run again though and the existing interval is still looking at the original a variable.
In your second example, you are mutating the value of the original a variable (which is the variable the interval has closed over).

React: weird stale closure issue with `useRef`

I am writing a useDebounce util hook.
function debounce(fn, delay) {
let timer = 0;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
fn(...args);
}, delay);
};
}
function useDebounce(fn, delay) {
const ref = React.useRef(fn);
React.useLayoutEffect(() => {
ref.current = fn;
}, [fn]);
return useMemo(() => debounce(ref.current, delay), [delay]);
}
I use ref to store the callback and update it using useLayoutEffect so the consumers of the API don't need to memoize their own callback. And also I wanted to preemptively answer that I know how useMemo works and I know you can memo the callback i.e. fn passed in useDebounce from outside but I don't want that burden on the users of the API so I did it myself.
Here is a live demo: https://codesandbox.io/s/closure-bug-xcvyd?file=/src/App.js
Now the function I want to denounce is
const increment = () => {
console.log(count);
setCount(count + 1);
};
so I just passed it in to useDebounce but it seems like the function ended up with stale closure over count because it only updates count from 0 -> 1 and then after that no matter how many times you click on the button it doesn't update anymore.
Yes I know I can write setCount(c => c + 1); to work around this problem.
But what perplexed me is that, if I rewrite useMemo(() => debounce(ref.current, delay), [delay]); to return useMemo(() => debounce((...args) => ref.current(...args), delay), [ delay ]); then this problem is fixed automatically.
I cannot seem to understand how (...args) => ref.current(...args) is fixing the problem.
Let's look what is happening step by step.
You are placing fn inside ref.
You are updating ref with new value
You are passing fn to debounce inside useMemo and this is where the error is.
On next render, you are again updating ref, but memoized function don't use it at all. It remembers reference to very first passed fn and this will change only when user of your hook will change delay.
In fixed example, with arrow function this is what happens:
You are placing fn inside ref
You are updating ref with new value
You are memoizing function that closures ref and will look inside it on each call, so it will pick the freshest fn value from ref.
function useDebounce(fn, delay) {
// storing function into ref
const ref = React.useRef(fn);
// updating function after memoization, and on each render when function changed
React.useLayoutEffect(() => {
ref.current = fn;
}, [fn]);
return useMemo(function() {
// here you are referencing current `fn`
// The very first `fn` that was passed into hook
// ref don't play role here - you are passing `ref.current`
let fnToDebounce = ref.current
return debounce(fnToDebounce, delay)
}, [delay]);
}
function useDebounce(fn, delay) {
// storing function into ref
const ref = React.useRef(fn);
// updating function after memoization, and on each render when function changed
React.useLayoutEffect(() => {
ref.current = fn;
}, [fn]);
return useMemo(function() {
// Here you are closuring reference to `ref` and `fnToDebounce` have no idea what is inside it.
// When it would be called, it will get ref and only at that point will retrieve `ref.current`, that will be the latest `fn`
let fnToDebounce = (...args) => ref.current(...args);
return debounce(fnToDebounce, delay);
}, [delay]);
}
This will pass ref.current to debounce.
useMemo(() => debounce(ref.current, delay), [delay]);
It's equivalent to this:
useMemo(() => debounce(fn, delay), [delay]);
The memoized function will only be created the first time you call the hook. The closure will have the original increment which encloses the original count, but ref is not enclosed.
In this version, however, you pass a lambda function with ref enlosed.
return useMemo(() => debounce((...args) => ref.current(...args))
Each time useDebounce is called, you change the increment function to a new one with the current count enclosed. useLayoutEffect will update ref, which is also enclosed in the memoized/debounced function.
So in the second case you have a nested chain of closures, which ensures that the debounce function will always have access to the latest count.
useMemo -> debounce -> (lambda) -> ref -> current -> increment -> count
You could simplify the code by just using the useCallback hook instead of making your own. But you must pass an updater function to setCount, to avoid a stale count value.
const increment = React.useCallback(
debounce(() => setCount((n) => n + 1), delay),
[setCount, delay]
)
Code Sandbox demo of this
This is a standard useDebounce
function useDebounce(value, delay) {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(
() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler);
};
},
[value, delay] // Only re-call effect if value or delay changes
);
return debouncedValue;
}

How to trigger a function at regular intervals of time using hooks and when certain criteria is met I want to clear the time interval?

I have a react component that performs a certain task at regular intervals of time after mounting. But I want to clear the interval once after a criterion is met. How can I achieve that?
My code
const [totalTime, setTotalTime] = React.useState(10000);
const foo = () => {
console.log("Here");
};
React.useEffect(() => {
const secondInterval = setInterval(() => {
if (totalTime > 0) setTotalTime(totalTime - 1000);
}, 1000);
return () => clearInterval(secondInterval);
});
React.useEffect(() => {
let originalInterval;
if (totalTime > 0)
originalInterval = setInterval(() => {
foo();
console.log(totalTime);
}, 5000);
return () => clearInterval(originalInterval);
}, []);
When I watch the console even after 10000ms It is still logging Here and also totalTime is always being 10000ms. I am not able to figure out what exactly is happening.
You may need to pass the older state as an argument to the setTotalTime updater function. You also may need to pass (another) state variable as a dependency to the useEffect hook so that the function is executed every time the state variable changes
React.useEffect(() => {
const secondInterval = setInterval(() => {
if (totalTime > 0) setTotalTime(totalTime => totalTime - 1000);
}, 1000);
return () => clearInterval(secondInterval);
}, [...]);
Depends on your criteria, and what you call a criteria, but you could just use another state and then useEffect on that another state:
const [criteriaIsMet,setCriteriaIsMet] = useState(false);
useEffect(() => { if(criteriaIsMet) {clearInterval(...)} },[criteriaIsMet])
And then somewhere when you do your actual "criteria logic" you just go ahead and do setCriteriaIsMet(true)
And how would you know Id of interval in above useEffect - again you could just create a special state that will store that id, and you could set it in your original useEffect
As for why your current code is not clearing the interval is because when you use useEffect with empty array as second argument, and return function in first function argument - that will be execute on component unmounting
And these are just one of numerous options you have actually :)

React useState does not update value

I am a bit confused as to why this component does not work as expected:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // This effect depends on the `count` state
}, 1000);
return () => clearInterval(id);
}, []); // 🔴 Bug: `count` is not specified as a dependency
return <h1>{count}</h1>;
}
but rewriting as below works:
function Counter() {
const [count, setCount] = useState(0);
let c = count;
useEffect(() => {
const id = setInterval(() => {
setCount(c++);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
React documentation says:
The problem is that inside the setInterval callback, the value of count does not change, because we’ve created a closure with the value of count set to 0 as it was when the effect callback ran. Every second, this callback then calls setCount(0 + 1), so the count never goes above 1.
But the explanation does not make sense. So why the first code does not update count correctly but the second does?
(Also declaring as let [count, setCount] = useState(0) then using setCount(count++) works fine too).
Why it looks like it doesn't work?
There are a couple hints that can help understand what's going on.
count is const, so it'll never change in its scope. It's confusing because it looks like it's changing when calling setCount, but it never changes, the component is just called again and a new count variable is created.
When count is used in a callback, the closure captures the variable and count stays available even though the component function is finished executing. Again, it's confusing with useEffect because it looks like the callbacks are created each render cycle, capturing the latest count value, but that's not what's happening.
For clarity, let's add a suffix to variables each time they're created and see what's happening.
At mount time
function Counter() {
const [count_0, setCount_0] = useState(0);
useEffect(
// This is defined and will be called after the component is mounted.
() => {
const id_0 = setInterval(() => {
setCount_0(count_0 + 1);
}, 1000);
return () => clearInterval(id_0);
},
[]);
return <h1>{count_0}</h1>;
}
After one second
function Counter() {
const [count_1, setCount_1] = useState(0);
useEffect(
// completely ignored by useEffect since it's a mount
// effect, not an update.
() => {
const id_0 = setInterval(() => {
// setInterval still has the old callback in
// memory, so it's like it was still using
// count_0 even though we've created new variables and callbacks.
setCount_0(count_0 + 1);
}, 1000);
return () => clearInterval(id_0);
},
[]);
return <h1>{count_1}</h1>;
}
Why does it work with let c?
let makes it possible to reassign to c, which means that when it is captured by our useEffect and setInterval closures, it can still be used as if it existed, but it is still the first one defined.
At mount time
function Counter() {
const [count_0, setCount_0] = useState(0);
let c_0 = count_0;
// c_0 is captured once here
useEffect(
// Defined each render, only the first callback
// defined is kept and called once.
() => {
const id_0 = setInterval(
// Defined once, called each second.
() => setCount_0(c_0++),
1000
);
return () => clearInterval(id_0);
},
[]
);
return <h1>{count_0}</h1>;
}
After one second
function Counter() {
const [count_1, setCount_1] = useState(0);
let c_1 = count_1;
// even if c_1 was used in the new callback passed
// to useEffect, the whole callback is ignored.
useEffect(
// Defined again, but ignored completely by useEffect.
// In memory, this is the callback that useEffect has:
() => {
const id_0 = setInterval(
// In memory, c_0 is still used and reassign a new value.
() => setCount_0(c_0++),
1000
);
return () => clearInterval(id_0);
},
[]
);
return <h1>{count_1}</h1>;
}
Best practice with hooks
Since it's easy to get confused with all the callbacks and timing, and to avoid any unexpected side-effects, it's best to use the functional updater state setter argument.
// ❌ Avoid using the captured count.
setCount(count + 1)
// ✅ Use the latest state with the updater function.
setCount(currCount => currCount + 1)
In the code:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
// I chose a different name to make it clear that we're
// not using the `count` variable.
const id = setInterval(() => setCount(currCount => currCount + 1), 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
There's a lot more going on, and a lot more explanation of the language needed to best explain exactly how it works and why it works like this, though I kept it focused on your examples to keep it simple.
More on closures.
useRef makes it easy
function Counter() {
const countRef = useRef(0);
useEffect(() => {
const id = setInterval(() => {
countRef.current++;
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{countRef.current}</h1>;
}

Categories