How does this closure make componentDidUpdate behave like useEffect? - javascript

I was reading Dan Abramov's article: A Complete Guide to useEffect, and in the section: Each Render Has Its Own… Everything, there are two examples, first
uses useEffect like this:
const UseEffectCounter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
console.log(`You clicked ${count} times`);
}, 3000);
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
)
}
The above example logs sequentially after the 3 seconds timeout.
The second example uses componentDidUpdate like this:
class ComponentDidUpdateCounter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
componentDidUpdate() {
setTimeout(() => {
console.log(`You clicked ${this.state.count} times`);
}, 3000);
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({count: this.state.count + 1})}>+1</button>
</div>
)
}
}
If click n times and wait for the timeout finishes, console logs You clicked n times for n times, rather than the sequential number of clicks. This is fine because React mutates this.state.count to always point to the latest value of the state.
However, simply modifying the componentDidUpdate like this would make the component behave like the first example:
componentDidUpdate() {
const count = this.state.count
setTimeout(() => {
console.log(`You clicked ${count} times`);
}, 3000);
}
How does this closure make the component behave like useEffect?

The reason for the difference is that this.state.count is evaluated at different points in your examples.
this.state.count is evaluated in the third example after each update at the when you assign it to count, whereas in the second example this.state.count is evaluated at the end of the timeout. At the end of the timeout, all of the updates have most likely already occurred, so you see the same values logged. But assigning it to a local count variable only defers the logging, not the evaluation.
In the useEffect version, each render is creating a new count const, and a const can only be assigned once. This is why it logs each value sequentially, because a const can never be updated, even if you wait on a state update to complete.

Related

useEffect, useState and promises.. what to do [duplicate]

According to the documentation for the useState React Hook:
If the new state is computed using the previous state, you can pass a
function to setState. The function will receive the previous value,
and return an updated value.
So given
const [count, setCount] = useState(initialCount);
you can write
setCount(prevCount => prevCount + 1);
I understand the reason for using the updater function form with setState, as multiple calls may be batched. However
During subsequent re-renders, the first value returned by useState
will always be the most recent state after applying updates.
So I'm not clear why the above example couldn't be written as
setCount(count + 1);
(which is how it's presented in Using the State Hook).
Is there a case where you must use functional updates with the useState hook to get the correct result?
(Edit: Possibly related to https://github.com/facebook/react/issues/14259 )
The main scenarios when the functional update syntax is still necessary are when you are in asynchronous code. Imagine that in useEffect you do some sort of API call and when it finishes you update some state that can also be changed in some other way. useEffect will have closed over the state value at the time the effect started which means that by the time the API call finishes, the state could be out-of-date.
The example below simulates this scenario by having a button click trigger two different async processes that finish at different times. One button does an immediate update of the count; one button triggers two async increments at different times without using the functional update syntax (Naive button); the last button triggers two async increments at different times using the functional update syntax (Robust button).
You can play with this in the CodeSandbox to see the effect.
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
function App() {
const [count, setCount] = useState(1);
const [triggerAsyncIndex, setTriggerAsyncIndex] = useState(1);
const [triggerRobustAsyncIndex, setTriggerRobustAsyncIndex] = useState(1);
useEffect(
() => {
if (triggerAsyncIndex > 1) {
setTimeout(() => setCount(count + 1), 500);
}
},
[triggerAsyncIndex]
);
useEffect(
() => {
if (triggerAsyncIndex > 1) {
setTimeout(() => setCount(count + 1), 1000);
}
},
[triggerAsyncIndex]
);
useEffect(
() => {
if (triggerRobustAsyncIndex > 1) {
setTimeout(() => setCount(prev => prev + 1), 500);
}
},
[triggerRobustAsyncIndex]
);
useEffect(
() => {
if (triggerRobustAsyncIndex > 1) {
setTimeout(() => setCount(prev => prev + 1), 1000);
}
},
[triggerRobustAsyncIndex]
);
return (
<div className="App">
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
<br />
<button onClick={() => setTriggerAsyncIndex(triggerAsyncIndex + 1)}>
Increment Count Twice Async Naive
</button>
<br />
<button
onClick={() => setTriggerRobustAsyncIndex(triggerRobustAsyncIndex + 1)}
>
Increment Count Twice Async Robust
</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Another possible scenario where functional updates could be necessary would be if multiple effects are updating the state (even if synchronously). Once one effect updates the state, the other effect would be looking at out-of-date state. This scenario seems less likely to me (and would seem like a poor design choice in most cases) than async scenarios.

Why doesn't componentDidUpdate log the real result like useEffect?

The following examples which log user clicks with time out, first uses useEffect and second uses componentDidUpdate:
const UseEffectCounter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
console.log(`You clicked ${count} times`);
}, 3000);
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
)
}
class ComponentDidUpdateCounter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
componentDidUpdate() {
setTimeout(() => {
console.log(`You clicked ${this.state.count} times`);
}, 3000);
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({count: this.state.count + 1})}>+1</button>
</div>
)
}
}
The first example logs sequentially after the 3 seconds timeout, which I understand that each time setCount is invoked, the state updates and the component re-renders, thus triggers useEffect on each re-render, and logs the sequential count value.
I don't fully understand the 2nd example: if componentDidUpdate runs after each update, thus each setState on button click, why doesn't it log the count value sequentially but logs the latest calculation of count for number of click times?
First code example that uses useEffect hook, logs the value of count sequentially because of closures. Callback function of setTimeout closes over the value of count and each callback logs the value of count that it closed over.
Second code example logs the updated value of count because, unlike the functional component example, there is no closure and in class component, React mutates this.state.count to always point to the latest value of the state.
P.S: For more details, read this blog post on useEffect hook by Dan Abramov where he goes over a similar example and explains the difference between the two code examples.
Because by the time the timeout goes off, this.state.count has changed. The value is not locked in when you start the timeout, you're looking it up when it goes off.
With the useEffect code, you're logging a local const which can never change. Even if the component rerenders, that will result in a different local const being created, and the one your timeout is referencing is unchanged.

setTimeout for this.state vs useState

When I use class component, I have code:
setTimeout(() => console.log(this.state.count), 5000);
When I use hook:
const [count, setCount] = useState(0);
setTimeout(() => console.log(count), 5000);
If I trigger setTimeout then change the count to 1 before the timeout (5000ms), class component will console.log(1) (the newest value), and for useState it is console.log(0) (value when register timeout).
Why does this happen?
Updated Version:
Question: Difference in behavior of a React State variable inside setTimeout / setInterval for function and class components?
Case 1: State variable in function component (stale closure):
const [value, setValue] = useState(0)
useEffect(() => {
const id = setInterval(() => {
// It will always print 0 even after we have changed the state (value)
// Reason: setInterval will create a closure with initial value i.e. 0
console.log(value)
}, 1000)
return () => {
clearInterval(id)
}
}, [])
Case 2: State variable in class component (no stale closure):
constructor(props) {
super(props)
this.state = {
value: 0,
}
}
componentDidMount() {
this.id = setInterval(() => {
// It will always print current value from state
// Reason: setInterval will not create closure around "this"
// as "this" is a special object (refernce to instance)
console.log(this.state.value)
}, 1000)
}
Case 3: Let's try to create a stale closure around this
// Attempt 1
componentDidMount() {
const that = this // create a local variable so that setInterval can create closure
this.id = setInterval(() => {
console.log(that.state.value)
// This, too, always print current value from state
// Reason: setInterval could not create closure around "that"
// Conclusion: Oh! that is just a reference to this (attempt failed)
}, 1000)
}
Case 4: Let's again try to create a stale closure in class component
// Attempt 2
componentDidMount() {
const that = { ...this } // create a local variable so that setInterval can create closure
this.id = setInterval(() => {
console.log(that.state.value)
// Great! This always prints 0 i.e. the initial value from state
// Reason: setInterval could create closure around "that"
// Conclusion: It did it because that no longer is a reference to this,
// it is just a new local variable which setInterval can close around
// (attempt successful)
}, 1000)
}
Case 5: Let's again try to create a stale closure in class component
// Attempt 3
componentDidMount() {
const { value } = this.state // create a local variable so that setInterval can create closure
this.id = setInterval(() => {
console.log(value)
// Great! This always prints 0 i.e. the initial value from state
// Reason: setInterval created closure around value
// Conclusion: It is easy! value is just a local variable so it will be closed
// (attempt successful)
}, 1000)
}
Case 6: Class has won (no extra effort to avoid the stale closure). But, how to avoid it in function component?
// Let's find solution
const value = useRef(0)
useEffect(() => {
const id = setInterval(() => {
// It will always print the latest ref value
// Reason: We used ref which gives us something like an instance field.
// Conclusion: So, using ref is a solution
console.log(value.current)
}, 1000)
return () => {
clearInterval(id)
}
}, [])
source-1, source-2
Case 6: Let's find another solution for function components
useEffect(() => {
const id = setInterval(() => {
// It will always print the latest state value
// Reason: We used updater form of setState (which provides us latest state value)
// Conclusion: So, using updater form of setState is a solution
setValue((prevValue) => {
console.log(prevValue)
return prevValue
})
}, 1000)
return () => {
clearInterval(id)
}
}, [])
Original Version:
The issue is caused by closures and can be fixed by using ref. But here is a workaround to fix it i.e. access the latest state value using "updater" form of setState:
function App() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
setTimeout(() => console.log('count after 5 secs: ', count, 'Wrong'), 5000)
}, [])
React.useEffect(() => {
setTimeout(() => {
let count
setCount(p => {
console.log('p: ', p)
count = p
return p
})
console.log('count after 5 secs: ', count, 'Correct')
}, 5000);
}, [])
return (<div>
<button onClick={() => setCount(p => p+1)}>Click me before 5 secs</button>
<div>Latest count: {count}</div>
</div>)
}
ReactDOM.render(<App />, document.getElementById('mydiv'))
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<body>
<div id="mydiv"></div>
</body>
For the useState, it create a timeout using count in the first time. It accesses the count value through a closure. When we set a new value by setCount, the component rerender but does not change the value passed to timeout.We can use const count = useRef(0) and pass to timeout count.current. This will always use the newest value of count.
Check this link for more information.
Timeouts don't play along nicely with reacts declarative programming model. In functional components, each render is a single frame in time. They never change.
When state updates, all state variables are created locally anew and don't overwrite the old closed variables.
You can also think of effects in the same way, where an effect will run in its local realm with all its local state variables on each render and new renders don't affect their output.
The only way to break out of this model is refs. Or class components where state is effectively similar to refs where the instance (this) is the ref container. Refs allow cross-render communication and closure busting. Use sparingly and with caution.
Dan Abramov has a fantastic article explaining all this and a hook that solves this. As you correctly answered, the issue is caused by stale closures. The solution indeed involves using refs.
Explanation
With function components, every render is a function call, creating a new function closure for that specific call. The function component is closing over the setTimeout callback function, so that everything in the setTimeout callback is accessing only the specific render where it was called.
Reusable solution:
Using a Ref and accessing it only within the setTimeout callback will give you a value that is persistent across renders.
However, it isn't that convenient to use a React Ref with a value that is always updating, like a counter. You are in charge of both updating the value, and causing a rerender yourself. Updating a Ref doesn't entail a component render.
My solution, for easy use, is to combine both useState and useRef hooks into a single "useStateAndRef" hook. This way, you get a setter that gets both the value, and a ref for use in async situations such as setTimeout and setInterval:
import { useState, useRef } from "react";
function useStateAndRef(initial) {
const [value, setValue] = useState(initial);
const valueRef = useRef(value);
valueRef.current = value;
return [value, setValue, valueRef];
}
export default function App() {
const [count, setCount, countRef] = useStateAndRef(0);
function logCountAsync() {
setTimeout(() => {
const currentCount = countRef.current;
console.log(`count: ${count}, currentCount: ${currentCount}`);
}, 2000);
}
return (
<div className="App">
<h1>useState with updated value</h1>
<h2>count: {count}</h2>
<button onClick={() => setCount(prev => prev + 1)}>+</button>
<button onClick={logCountAsync}>log count async</button>
</div>
);
}
Working CodeSandbox link: https://codesandbox.io/s/set-timeout-with-hooks-fdngm?file=/src/App.tsx

converting class based component with state to functional component with hooks

I have this componet with state, here when the user is online it increases the count. I want to change it to functional component with hooks, which I have done below
class App extends React.Component {
state = {
isOnline: true,
count: 1
}
handleOnline = () => {
if (!this.state.isOnline) {
this.setState({
count: this.state.count + 1
}, () => {
this.setState({ isOnline: !this.state.isOnline })
})
} else {
this.setState({ isOnline: !this.state.isOnline })
}
}
render() {
return (
<div className="App">
<h1>online ==> {this.state.isOnline ? 'Online' : 'Offline'}</h1>
<h1>count ==> {this.state.count}</h1>
<button onClick={this.handleOnline}>Toggle</button>
</div>
);
}
}
Here is my conversion to functional component with hooks,
const App = () => {
const [isOnline, setIsOnline] = useState(true)
const [count, setCount] = useState(1)
const handleClick = () => {
if (!isOnline) {
setIsOnline(!isOnline)
setCount(count + 1)
} else {
setIsOnline(!isOnline)
}
}
return (
<div className="App">
<h1>online ==> {isOnline ? 'Online' : 'Offline'}</h1>
< h1 > count ==> {count}</h1>
<button onClick={handleClick}>Toggle</button>
</div>
)
}
In the class based component, I have read not to use setState one after another so I used the callback function in this.setState like this
this.setState({
count: this.state.count + 1
}, () => {
this.setState({ isOnline: !this.state.isOnline })
})
Now, in the functional component I have used setCount and setIsOnline one after another is it good ???
const handleClick = () => {
if (!isOnline) {
setIsOnline(!isOnline)
setCount(count + 1)
} else {
setIsOnline(!isOnline)
}
I have read to use useEffect for callbacks, but all I get is infinite loop. Even though both of my components work and give me the desired result . I wanted to know if i must use useEffect for the callback or if my implementation with hooks in functional component is correct???
This implementation is correct, yes we should not set one state after the other because setState works asynchronously but since you are only setting two states so its fine.
Although you can also keep one state object instead of both separate states i.e
const [state, setState] = useState({ count: 1, isOnline: true });
And then you can set both object keys in a single setState, like:
setState(() => ({
count: 1,
isOnline: false,
}))
Also in the class based approach you have used a callback but you actually don't need that, you can use single setState for setting both states i.e.
this.setState(() => ({
count: this.state.count + 1,
isOnline: !this.state.isOnline ,
}))
Another important note:
Try to use functional set state as I use in examples above, as it reduces the risk of being caught into React state asynchronous issues.
Calling set state one after the other is totally fine and is the correct thing to do here:
const handleClick = () => {
if (!isOnline) {
setIsOnline(!isOnline)
setCount(count + 1)
} else {
setIsOnline(!isOnline)
}
}
State is updated asychronously, which means that your state variables isOnline and count don't actually change until your component re-renders. Calling setCount and setIsOnline doesn't update these variables, but tells React to update on the next render.
This is why you can't do something like this:
const handleClick = () => {
setCount(count + 1)
setCount(count + 1)
}
The count would be incremented by 1, NOT by 2.
Why?
Because the value count has not been updated yet, since we have to wait until re-render before it gets updated. That means count has the same value the entire way through the function - it never changes. So you can call setCount(count + 1) a million times and the value will only ever increment by 1.
This is what people mean when they say you should use the set state callback function.
This is the set state callback function:
const handleClick = () => {
setCount(prev => prev + 1)
setCount(prev => prev + 1)
}
This works as expected, and count will now be incremented by 2.
If we pass a function like this prev => prev + 1 to set state, React will pass the most recent value to the function.
The rule is:
Every time you use the old state value to set the new state value - pass a function to set state.
So although your current implementation does work, you should really be passing a function to set state on count since you are depending on previous state:
const handleClick = () => {
if (!isOnline) {
setIsOnline(!isOnline)
setCount(prev => prev + 1)
} else {
setIsOnline(!isOnline)
}
}
Usually you should do this for your boolean value too, for example:
setIsOnline(prev => !prev)
but since you're using isOnline in your if statement, you shouldn't do that here as the prev value may be different than the value your if uses.

When are functional updates required for computations involving previous state?

According to the documentation for the useState React Hook:
If the new state is computed using the previous state, you can pass a
function to setState. The function will receive the previous value,
and return an updated value.
So given
const [count, setCount] = useState(initialCount);
you can write
setCount(prevCount => prevCount + 1);
I understand the reason for using the updater function form with setState, as multiple calls may be batched. However
During subsequent re-renders, the first value returned by useState
will always be the most recent state after applying updates.
So I'm not clear why the above example couldn't be written as
setCount(count + 1);
(which is how it's presented in Using the State Hook).
Is there a case where you must use functional updates with the useState hook to get the correct result?
(Edit: Possibly related to https://github.com/facebook/react/issues/14259 )
The main scenarios when the functional update syntax is still necessary are when you are in asynchronous code. Imagine that in useEffect you do some sort of API call and when it finishes you update some state that can also be changed in some other way. useEffect will have closed over the state value at the time the effect started which means that by the time the API call finishes, the state could be out-of-date.
The example below simulates this scenario by having a button click trigger two different async processes that finish at different times. One button does an immediate update of the count; one button triggers two async increments at different times without using the functional update syntax (Naive button); the last button triggers two async increments at different times using the functional update syntax (Robust button).
You can play with this in the CodeSandbox to see the effect.
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
function App() {
const [count, setCount] = useState(1);
const [triggerAsyncIndex, setTriggerAsyncIndex] = useState(1);
const [triggerRobustAsyncIndex, setTriggerRobustAsyncIndex] = useState(1);
useEffect(
() => {
if (triggerAsyncIndex > 1) {
setTimeout(() => setCount(count + 1), 500);
}
},
[triggerAsyncIndex]
);
useEffect(
() => {
if (triggerAsyncIndex > 1) {
setTimeout(() => setCount(count + 1), 1000);
}
},
[triggerAsyncIndex]
);
useEffect(
() => {
if (triggerRobustAsyncIndex > 1) {
setTimeout(() => setCount(prev => prev + 1), 500);
}
},
[triggerRobustAsyncIndex]
);
useEffect(
() => {
if (triggerRobustAsyncIndex > 1) {
setTimeout(() => setCount(prev => prev + 1), 1000);
}
},
[triggerRobustAsyncIndex]
);
return (
<div className="App">
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
<br />
<button onClick={() => setTriggerAsyncIndex(triggerAsyncIndex + 1)}>
Increment Count Twice Async Naive
</button>
<br />
<button
onClick={() => setTriggerRobustAsyncIndex(triggerRobustAsyncIndex + 1)}
>
Increment Count Twice Async Robust
</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Another possible scenario where functional updates could be necessary would be if multiple effects are updating the state (even if synchronously). Once one effect updates the state, the other effect would be looking at out-of-date state. This scenario seems less likely to me (and would seem like a poor design choice in most cases) than async scenarios.

Categories

Resources