React design principles documentation states that "setState() is asynchronous". Although it does not mention useEffect, this documentation states:
What does useEffect do? By using this Hook, you tell React that your component needs to do something after render.
Which to me indicates it is also asynchronous.
The act() documentation states:
When writing UI tests, tasks like rendering, user events, or data fetching can be considered as “units” of interaction with a user interface. react-dom/test-utils provides a helper called act() that makes sure all updates related to these “units” have been processed and applied to the DOM before you make any assertions
Sunil Pai clarifies this a bit in his "Secrets of the act() API" article:
It guarantees 2 things for any code run inside its scope:
- any state updates will be executed
- any enqueued effects will be executed
Since state updates are asynchronous and act() gaurantees all states update will be executed, how can it do this without us having to call await in our tests?
And further, why doesn't act ensure other settled promises callbacks are executed as well?
Consider this example component:
function App() {
const [foo, setFoo] = useState("");
const [bar, setBar] = useState("");
useEffect(() => {
// Callback added to job queue as a microtask
Promise.resolve().then(() => {
setBar("bar");
});
// Making an asynchronous(?) state update
setFoo(() => {
return "foo";
});
}, []);
return <p>{foo}{bar}</p>;
}
And my test fails with Received: "foo":
test('foobar', async () => {
act(() => {
render(<App />, container);
})
expect(container.textContent).toBe("foobar");
});
From above referenced documentation I've later understood that if you want to flush queued microtasks, you have the use the async version of act. But it doesn't explain how the synchronous act can flush reacts internal asynchronous logic without flushing my apps asynchronous logic. Act just feels like magic and a source to so much confusion for me.