I'm using React (with TypeScript), and I found out very strange behavior. In the method "handleAnswer" I'm changing the variable "isReady" and also printing the value using the useEffect hook. but the change only takes place if I put a timer there, When I took the timer out the variable stopped changing, and I stopped seeing the output in the console. What am I doing wrong?
export const App = () => {
const [isReady, setIsReady] = useState(true);
const [counter, setCounter] = useState(0);
useEffect(() => { console.log(isReady) },[isReady]);
const handleAnswer = async (isLiked: boolean) => {
setIsReady(false);
// WHEN I REMOVE THIS LINE, IT DOESN'T WORK
await new Promise(resolve => setTimeout(resolve, 10));
setCounter(counter + 1)
setIsReady(true);
}
return (
<div className="App" >
{ isReady ? <div className="main-content"></div> :<CircularProgress /> }
<Button variant="contained" onClick={() => handleAnswer(true)} startIcon={<ThumbUpIcon />}>
click
</Button>
</div>
);
}
export default App;
State updates are queued and processed as a batch after the code that queued them has returned. So without the promise, your handleAnswer
code does this:
isReady
to false
.counter
.isReady
to true
.React does those three updates once your code has returned, so they only trigger a single re-render, and during that re-render isReady
has the same value it had the last time the dependencies on your useEffect
were checked, so the useEffect
callback doesn't run.
With the promise, your code does this:
isReady
to false
.counter
.isReady
to true
.Between #2 and #3 on that list, React has a chance to process state changes, and so it does — triggering a re-render when isReady
is false
, which triggers your useEffect
callback because isReady
has a different value than the last time the dependencies were checked. Then later, after the promise settles, your code queues a couple of other state changes, which trigger another render, triggering your useEffect
callback again.
The key points are:
useEffect
callback triggered by a the value being different won't run, because the value isn't different by the time it's checkedIn a comment you've asked:
So I can achieve this behavior without using promise? I need the
isReady
flag to control a scroll loader until the function is done handling some data synchronously.
If the processing is synchronous, it will tie up the main thread that handles UI updates, and your loading indicator may well not be animated (it definitely won't be if the animation is done with JavaScript; if it's an animated GIF or similar, whether it's done depends on the browser and whether all frames are already loaded; if it's a CSS animation, a quick test with Chrome and Firefox suggests that they, at least, will keep animating when the processing is happening, but no guarantees).
If you can avoid synchronous processing on the main thread, I would. Consider sending the work to a worker thread, for instance.
To do it, though, you'll need to change isReady
, then wait to start the synchronous processing until after the result of that change has been rendered by waiting for a useEffect
callback.
Here's an example using a CSS spinner I found here.
const { useState, useEffect } = React;
const Example = () => {
const [isReady, setIsReady] = useState(true);
// On click, set `isReady` to false
const startProcessing = () => {
setIsReady(false);
};
// When `isReady` becomes false, start "processing".
// (You may want to have an instance variable [via a ref]
// to tell you whether to actually do processing, and if
// so what to process.)
useEffect(() => {
if (!isReady) {
// Start "processing"
const done = Date.now() + 3000;
while (Date.now() < done) {
// Busy wait -- NEVER DO THIS IN REAL-WORLD CODE
}
// Done "processing"
setIsReady(true);
}
}, [isReady]);
return <div>
<input type="button" onClick={startProcessing} value="Start Processing (Takes 3 Seconds)" />
<div>isReady = {String(isReady)}</div>
{isReady ? null : <div className="spinner-loader" />}
</div>;
};
ReactDOM.render(<Example />, document.getElementById("root"));
/*
Credit: jlong @github
Source: https://github.com/jlong/css-spinners
*/
@-moz-keyframes spinner-loader {
0% {
-moz-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-webkit-keyframes spinner-loader {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes spinner-loader {
0% {
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(360deg);
-ms-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
/* :not(:required) hides this rule from IE9 and below */
.spinner-loader:not(:required) {
-moz-animation: spinner-loader 1500ms infinite linear;
-webkit-animation: spinner-loader 1500ms infinite linear;
animation: spinner-loader 1500ms infinite linear;
-moz-border-radius: 0.5em;
-webkit-border-radius: 0.5em;
border-radius: 0.5em;
-moz-box-shadow: rgba(0, 0, 51, 0.3) 1.5em 0 0 0, rgba(0, 0, 51, 0.3) 1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) 0 1.5em 0 0, rgba(0, 0, 51, 0.3) -1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) -1.5em 0 0 0, rgba(0, 0, 51, 0.3) -1.1em -1.1em 0 0, rgba(0, 0, 51, 0.3) 0 -1.5em 0 0, rgba(0, 0, 51, 0.3) 1.1em -1.1em 0 0;
-webkit-box-shadow: rgba(0, 0, 51, 0.3) 1.5em 0 0 0, rgba(0, 0, 51, 0.3) 1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) 0 1.5em 0 0, rgba(0, 0, 51, 0.3) -1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) -1.5em 0 0 0, rgba(0, 0, 51, 0.3) -1.1em -1.1em 0 0, rgba(0, 0, 51, 0.3) 0 -1.5em 0 0, rgba(0, 0, 51, 0.3) 1.1em -1.1em 0 0;
box-shadow: rgba(0, 0, 51, 0.3) 1.5em 0 0 0, rgba(0, 0, 51, 0.3) 1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) 0 1.5em 0 0, rgba(0, 0, 51, 0.3) -1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) -1.5em 0 0 0, rgba(0, 0, 51, 0.3) -1.1em -1.1em 0 0, rgba(0, 0, 51, 0.3) 0 -1.5em 0 0, rgba(0, 0, 51, 0.3) 1.1em -1.1em 0 0;
display: inline-block;
font-size: 10px;
width: 1em;
height: 1em;
margin: 1.5em;
overflow: hidden;
text-indent: 100%;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>