I’ve got an app with 2 values that user can change stored as state. When they’re changed I do some processing in useEffect and also store output of this processing in state and render it.
At this point everything is working perfectly fine, but this processing takes some time and I want to show some loading indicator. I want it to show after button click.
This is simplified version of my app and dumb implementation of this loading indicator – I know why this doesn’t work, I just don’t know how to write it correctly so I made this just to show what I’m trying to do. Here is the code:
function App() {
const [value1, setValue1] = useState(0);
const [value2, setValue2] = useState(0);
const [output, setOutput] = useState(0);
const [isLoading, setIsLoading] = useState(false); // setting true here won't work,
// because I want to show loading indicator after user change value1 or value2
// and not on inital load
useEffect(() => {
if (!value1 && !value2) {
return;
}
setIsLoading(true);
for (let i = 0; i < 1999999999; i++) {} // some long operations
setOutput(value1 + value2);
setIsLoading(false);
}, [value1, value2]);
return (
<div>
<button onClick={() => setValue1(value1 + 1)}>increment value1</button>
<button onClick={() => setValue2(value2 + 1)}>increment value2</button>
<div>
{value1} + {value2} = {isLoading ? 'please wait...' : output}
</div>
</div>
);
}
4
Answers
You can start the value of isLoading equals true, so, the page will start on loading.
Try this piece of code:
Complete code is here: https://codesandbox.io/s/relaxed-hill-jy1bfm?file=/src/App.js:689-1018
The problem is that it is asynchronous. There may be many different approaches to the solution, but for your simple example I have simple solution:
The problem is that between setting
isLoading
totrue
and setting it back tofalse
(after the calculation-heavy operation) no rendering happened.Several approaches now come to my mind; and I’m not sure which one actually works as expected and (from those that do) which one I would pick; so I just share what’s on my mind:
approach 1 (adding it to the task queue of the main thread): only setting the loading flag synchronously (and thus returning from user code and handing the control flow back to the current render-run of react), and deliberately triggering the calculation in an asynchronous way
approach 2 (adding it to the microtask queue of the main thread): turn it into micro tasks (a.k.a. Promises): when your useEffect just creates and starts a promise object and then "forgets" about it, the control flow is handed back to react. when a promise resolves and changes your component state react will do a re-render. But I suspect this might not bring any change, because afaik the microtask queue runs on the main thread and will run until empty, before react can schedule the re-rendering task. Disclaimer: promises are not my strong suit and I might have fudged that up here.
approach 3 (using flushSync): ensuring the UI is rendered before your next statement. you are not allowed to call flushSync inside useEffect, therefore you need to place the call inside a new task or microtask
or