I am aware of the issue of stale state in nested functions or callbacks (e.g. setTimeout) inside a React component. For example, in the code below (take from React docs) we see the issue of stale state:
"If you first click “Show alert” and then increment the counter, the
alert will show the count variable at the time you clicked the “Show
alert” button."
function Example() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
I know that we can create a mutable reference to the most up to date/current value of state by using useRef:
function Example() {
const [count, setCount] = useState(0);
const stateRef = useRef()
stateRef.current = count
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + stateRef.current);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
My question is, why does creating a reference to our useRef variable (stateRef) solve the problem of stale state, and why does this get around the issue of closure? Surely by just referencing count directly inside setTimeout, once the component re-renders the count value will have updated, and therefore our reference to it would return the up to date value.
I am struggling to understand why referencing stateRef as opposed to count gets around the issue of stale state, given they are both declared in the same lexical scope from setTimeouts point of view.
Try imagine a timeline, each re-render of your component creates a "snapshot" and leaves a mark on this timeline.
So called "stale state" is more often referred as "stale closure", which more accurately describes the real problem.
How precisely closure causes trouble?
Each time a component re-render, the component function itself is re-executed from start to end, and during this execution:
all the hook calls along the way are called
all the local variables are re-initialzed
and all the nested functions inside the component function are re-declared
and MOST IMPORTANTLY, if any of these nested functions reference the local variables within the component function, such reference is really pointing to a "closure", or a "snapshot" that captures the current states of those local variables, created on-the-fly and stored somewhere inside memory.
If you call setTimeout and send a callback function to it, and that function references a local variable, it's really stored inside one of these snapshots on the rendering timeline.
As the component re-renders, new snapshots are added to the timeline and only the latest one is up-to-date, while as the callback function is still referencing a stale snapshot/closure.
By the time setTimeout decide to execute the callback, the callback look at its own version of snapshot/closure and say "OK, the 'count' variable is 0" unaware of it's stale.
How useRef solves the problem?
Cus const ref = useRef() always returns the same object reference across re-rendering. So even if callback look at a stale snapshot/closure, it still sees the same object ref as in the latest snapshot. And since each re-execution of the component function always sets the ref.value = someValue property with latest value, the callback got a way to access the latest value.
Surely by just referencing count directly inside setTimeout, once the
component re-renders the count value will have updated, and therefore
our reference to it would return the up to date value.
No, when you clicked on the button, the function useSetTimeout used the callback at hand at that time. And count is NOT a reference to your variable. When the component rerenders, the callback won’t know you changed the value at all.
useRef is different in this that it returns an object and objects work a bit differently than other types of variables. And since it’s always the same object, you’ll always deal with the same variable.
By the way, when you call setCount, be sure to always do setCount(prevState => ...). Otherwise you’re not guaranteed to have the latest value.
Related
I'm a beginner to React JS.
I faced this weird situation,
const [counter, setCounter] = useState(0);
const incrementCounter = () => {
setCounter(counter + 1);
console.log(counter, "Log inside the function");
};
useEffect(() => {
console.log(counter, "Log inside the useEffect");
}, [counter]);
return (
<div>
<h1>{counter}</h1>
<button onClick={incrementCounter}>Increment</button>
</div>
);
So when incrementCounter function is triggered counter value will be increased by 1 and right after that I'm logging the value in the console. But it displays the value as 0. When I use the useEffect hook to check the changes of count state it I'm getting the correct value as 1 when I log the count value inside the useEffect scope. What is the reason for not displaying in incrementCounter function and the reason for displaying the correct value inside the useEffect hook.
according to React docs
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. In the rare case that you need to force the DOM update to be applied synchronously, you may wrap it in flushSync, but this may hurt performance.
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.
States value will change only after the component has rendered.
In this case the log inside the function will keep the state value even after the SetCounter, once it completes the component will render again.
The useEffect is triggered when the state has changed on such render so it shows the new value.
A new incrementCounter function object is being redefined with every render. Each time it is defined, its own internal counter is based on the value of counter state at that point. The counter within that particular incrementCounter function object is stale and doesn't get updated anymore.
This is because the setCounter is asynchronous.
If you use setTimeOut you can clearly observe it
setTimeout(() => {
console.log(counter, "Log inside the function");
}, 2000);
I was going through useEffect from reactjs docs and I've found this statement here
Experienced JavaScript developers might notice that the function
passed to useEffect is going to be different on every render.
We are passing a function to useEffect and this function is said to be different for each render. useEffect has access to state and props since it's inside the function component and when either of these changes, we can see that change in the function of useEffect(because of closure) right? This is not clear, because in the next line the doc states
This is intentional. In fact, this is what lets us read the count
value from inside the effect without worrying about it getting stale.
To counter this, assume we have a function
function foo(n) {
bar = () => {
setTimeout(() => console.log({n}), 50);
return n;
}
setTimeout(() => {n = 10}, 0);
setTimeout(() => {n = 20}, 100);
setTimeout(() => {n = 30}, 150);
return bar;
}
baz = foo(1);
baz(); //prints 10
setTimeout(baz, 300); //prints 30
It seems that when the closure value(n) is changed, we can see that change in the setTimeout's callback (and this callback isn't changed over time). So, how can the closured value(state/props) in useEffect's function become stale as mentioned in docs?
Am I missing something here? I think it's more of a JS question compared to React, so I took a JS example.
I found the answer a few days back, and as #apokryfos(Thank you again!) mentioned in the comments above, the program execution process is now making more sense. I want to summarize my learnings here.
Firstly, the code I considered, was not like with like comparison (in #apokryfos words) with the React doc statements, and this is true. In case of static HTML + vanilla JS where the HTML button has an event-listener JS function, this function is declared only once and when the event occurs the same JS function is executed everytime.
The code I have given in the question is similar to this, and so when executed in console or in event listener will only be declared once.
In case of React(or any state based UI libraries/frameworks), the HTML is not static and it needs to change on state-change. On the execution side (considering React), component will be created when we call it (in JSX), and the component's function/class will be executed completely from top to bottom. This means
from all the event-handlers that doesn't deal with state, constants to useState's destructed elements and useEffect's callback functions, everything are re-initialized.
If the parent's state changes initiate a render on its children(in normal scenarios), then the children will need to re-render themselves completely with the new props and/or new state to show the updated UI
Considering the example in React docs (link), useEffect with no dependencies will get executed after every render, and here it's updating the DOM by showing the current state value. Unless the callback function has the latest value of that state, it'll only print the stale value. So re-initialising the functions here is the main reason behind not having stale values in the callback functions
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = 'You clicked ${count} times';
});
}
This is a boon and a curse sometimes. Assume if we are sending useState's setState function to the child, and in the child, we have a useEffect(that makes a network call to fetch data) that takes this function as its dependency. Now, for every state change in parent, even if there is a use-case or not, the useEffect will get triggered as this function in dependency is changed (as every update re-initializes the functions). To avoid this, we can utilize useCallback on the functions which we want to memorize and change only when certain params are changed, but it is not advisable to use this on useEffect's callback function since we might end-up in stale values.
Further Reading:
GeeksForGeeks useCallback
SourceCode interpretation of useEffect
Another SourceCode interpretation of useEffect
I'm trying to understand the general advice I've seen regarding React and stale closures.
Specifically, as I understand it, the term "stale closure" is used to describe a scenario where a component and useEffect function are constructed like this
function WatchCount() {
const [count, setCount] = useState(0);
useEffect(function() {
setInterval(function log() {
console.log(`Count is: ${count}`);
}, 2000);
}, []);
return (
<div>
{count}
<button onClick={() => setCount(count + 1) }>
Increase
</button>
</div>
);
}
React calls WatchCount to render a component, and sets the value of the count variable. When javascript calls the log function two seconds later, the count variable will be bound to the count variable from when WatchCount was first called. The value of count won't reflect updates that may have happened on renders of WatchCount that happened in-between the first render and the interval code eventually firing.
The general advice that's given to "solve" this is to list your variable in the dependencies array -- the second argument to useEffect
useEffect(function iWillBeStale() {
setInterval(function log() {
console.log(`Count is: ${count}`);
}, 2000);
}, [count]);
As a javascript programmer, I don't understand how this "solves" the problem. All we've done here is create an array that includes the variable in it, and passed that array to useEffect My naive view is that the count variable in log is still scoped to the first call of WatchCount, and should still be stale.
Am I missing some nuance of javascript's scope here?
Or does this "fix" things because of something that useEffect is doing with those variables?
Or some third thing?
Am I missing some nuance of javascript's scope here?
No, you're right, creating the array and passing it to useEffect doesn't affect the closure, the closed-over constant keeps its value.
Or does this "fix" things because of something that useEffect is doing with those variables?
Yes. React runs the entire render function each time the state changes, which creates a new closure and passes it to useEffect again. When the dependencies change, useEffect re-runs the effect function which creates a new interval with the new closure.
Also, the effect function is returning a cleanup function in the author's solution, which runs when the component unmounts or before running the effect the next time (when the dependencies change). This cleanup function calls clearInterval, which means the stale closure won't be executed again, and the number of concurrently active intervals doesn't increase.
Admittedly, this proposed solution has a huge bug: clearing the interval and starting a new interval every time the count changes does not lead to a nice periodic 2s interval, the gaps between two logs might be much larger - the logging is essentially debounced and will only run if no increment happened in the last 2s. If this is not desired, a ref might be a much simpler solution:
const [count, setCount] = useState(0);
const countRef = useRef(0);
countRef.current = count;
useEffect(function() {
setInterval(function log() {
console.log(`Count is: ${countRef.current}`);
}, 2000);
}, []);
setState updates state asynchronously. It's my understanding that, when using a class component, you can do something like this to ensure certain code is executed after one setState updates state:
setState({color: red}, callbackThatExecutesAfterStateIsChanged);
I'm using a functional component & hooks. I'm aware, here, useEffect()'s callback will execute everytime after color state changes and on initial execution.
useEffect(callback, [color]);
How can I replicate similar behaviour as the class component example - that is, to execute a chunk of code once after one setState() successfully changes state and not on initial execution?
If you ask me, there is no safe way to do this with hooks.
The problem is that you both have to read and set an initialized state in order to ignore the first update:
const takeFirstUpdate = (callback, deps) => {
const [initialized, setInitialized] = useState(false);
const [wasTriggered, setWasTriggered] = useState(false);
useEffect(() => {
if (!initialized) {
setInitialized(true);
return;
}
if (wasTriggered) {
return;
}
callback();
setWasTriggered(true);
}, [initialized, wasTriggered]);
};
While the hook looks like it works, it will trigger itself again by calling setInitialized(true) in the beginning, thus also triggering the callback.
You could remove the initialized value from the deps array and the hook would work for now - however this would cause an exhaustive-deps linting error. The hook might break in the future as it is not an "official" usage of the hooks api, e.g. with updates on the concurrent rendering feature that the React team is working on.
The solution below feels hacky. If there's no better alternative, I'm tempted to refactor my component into a class component to make use of the easy way class components allow you to execute code once state has been updated.
Anyway, my current solution is:
The useRef(arg) hook returns an object who's .current property is set to the value of arg. This object persists throughout the React lifecycle. (Docs). With this, we can record how many times the useEffect's callback has executed and use this info to stop code inside the callback from executing on initial execution and for a second time. For example:
initialExecution = useRef(true);
[color, setColor] = useState("red");
useEffect(() => {
setColor("blue");
});
useEffect(() => {
if (initialExecution.current) {
initialExecution.current = false;
return;
}
//code that executes when color is updated.
}, [color]);
I'am puzzled by the setState() accepted object.The code link is here https://codepen.io/DRL9/pen/jadbWq and the code is as follow:
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
intervalCount: 1,
buttonCount: 1
};
this.increment = 1;
this.intervalId = null;
}
tick() {
this.setState({
intervalCount: this.state.intervalCount + this.increment
});
this.setState({
intervalCount: this.state.intervalCount + this.increment
});
}
onClick() {
this.setState({
buttonCount: this.state.buttonCount + this.increment
});
this.setState({
buttonCount: this.state.buttonCount + this.increment
});
}
componentDidMount() {
this.intervalId = setInterval(() => this.tick(), 1000);
}
componentWillUnmount() {
clearInterval(this.intervalId);
}
render() {
return <div>
<div>
interval counter: {this.state.intervalCount}
</div>
<button onClick={this.onClick.bind(this)}>increment</button>
<div>
button counter: {this.state.buttonCount}
</div>
</div>;
}
}
I expect that intervalCount will increment 1 like the behavior when I click increment button. However, it increment 2 each tick.
The only different is that one updated is in setInterval function and the other updated is in onClick function.
Why are their behavior different?
We can't talk in absolutes regarding the timing of setState as it is, by definition, unpredictable. The state changes may be delayed to some time in the future, and this behavior may be different depending on the version of React that you are using.
In the example provided, React is delaying state updates until the onClick handler has finished running. React knows when this handler is finished running because we are passing the handler through JSX's onClick (which is then processed internally by React):
// React processes the onClick handler below
<button id="btn" onClick={this.onClick.bind(this)}>increment</button>
If we were to instrument the onClick logic ourselves, by manually grabbing the button element from the DOM and adding a click event listener that calls our onClick handler, the button updates identically to the setInterval (React doesn't know that we are updating state within a click handler, so it chooses not to make the optimization of batching the calls to setState).
See this codepen, where the button counter has a click handler manually added to it in the componentDidMount function as opposed to using JSX's onClick. Notice that the button counter now increments in intervals 2 instead of 1.
I want to stress that this behavior is not deterministic and you should never use this.state within your setState function. Instead, you want to use the variation of setState that accepts an updater function that contains the previous state. Then, build your new state from the state passed to the updater:
this.setState(state => ({
buttonCount: state.buttonCount + this.increment
}));
See this codepen, which uses an updater to update the button counter, producing the expected effect of updating the button counter in intervals of 2.
For more info on setState see the official documentation.
From the documentation for setState:
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
This is saying that when you reference state data (this.state.buttonCount or this.state.intervalCount) immediately after you've changed it using setState (as you do in both functions on the second setState command) the behavior will be unpredictable. Maybe setState immediately updates the state data, as it seems to be doing with intervalCount, and maybe setState waits to update the state data so it can batch it later, as it seems to be doing with buttonCount. As a developer we should avoid exposing ourselves to such unpredictable behavior by using other variables when we want to modify the state multiple times during the same event.
As to why intervalCount is fairly consistently being updated immediately (and thus incrementing the second time) and buttonCount is consistently being batched (and only incrementing one time for the two calls to setState) my guess is this: onClick is triggered by a user interaction so the React engine probably guesses that during user interactions a lot of state may be changing, so it batches the calls to setState, maybe until the event fully propagates. tick, on the other hand, is triggered by an internal callback without any user interaction being processed, so the React engine probably guesses it's safe to update state right away without batching.