I've been working with Javascript for a couple of years now, and with my current knowledge of the event loop I'm struggling to understand why this testing recipe from the React docs work. Would someone be able to break down exactly what happens in each step there? To me, it seems magical that this works in the test:
await act(async () => {
render(<User id="123" />, container);
});
// expect something
The component looks like this (copying in case that link gets deprecated):
function User(props) {
const [user, setUser] = useState(null);
async function fetchUserData(id) {
const response = await fetch("/" id);
setUser(await response.json());
}
useEffect(() => {
fetchUserData(props.id);
}, [props.id]);
if (!user) {
return "loading...";
}
return (
<details>
<summary>{user.name}</summary>
<strong>{user.age}</strong> years old
<br />
lives in {user.address}
</details>
);
}
There's no implicit or explicit return happening on the render, so how does act know to await the async stuff happening in the component (fetching etc)?
To me, this would make more sense:
await act(async () => render(<User id="123" />, container));
or (which is the same thing):
await act(async () => {
return render(<User id="123" />, container);
});
or even:
await act(render(<User id="123" />, container));
But that doesn't seem to be how people use it, or how it was intended to be used, so I'm a bit lost. I've seen the same examples with enzymes mount
.
I don't want to create a fragile test, so I really want to understand this.
Does it have something to do with the callback being async i.e. does that append something to the event loop last, making it so that the await waits for everything inside render to happen before resolving?
I'm drawing a blank here and am struggling in the react doc jungle, because everyone seems to use this pattern, but no one really explains why or how it works.
Thanks for the help in advance!
CodePudding user response:
When looking closer at the source code of react-dom
and react-dom/test-utils
it seems like what's making this whole thing work is this setImmediate call happening after the first effect flush in recursivelyFlushAsyncActWork.
It seems like act
chooses to use this recursivelyFlushAsyncActWork
simply because the callback has the signature of being "thenable", i.e. a Promise. You can see this here.
This should mean that what happens is (simplified) this:
- The
useEffect
callback is flushed (puttingfetch
on the event loop). - The
setImmediate
callback "ensures" our mock promise / fetch is resolved. - A third flush happens by a recursion inside the
setImmediate
callback (called byenqueueTask
) making the state changes appear in the DOM. - When there's nothing left to flush it calls the outer most
resolve
and ouract
resolves.
In the code that looks kinda like this (except this is taken from an older version of react-dom
from the node_modules of my React project, nowadays flushWorkAndMicroTasks
seems to be called recursivelyFlushAsyncActWork
):
function flushWorkAndMicroTasks(resolve) {
try {
flushWork(); // <- First effect flush (fetch will be invoked by the useEffect?)
enqueueTask(function () { // <- setImmediate is called in here (finishes the fetch)
if (flushWork()) { // <- Flush one more time and the next loop this will be false
flushWorkAndMicroTasks(resolve);
} else {
resolve(); // <- resolve is called when flushWork has nothing left to flush.
}
});
} catch (err) {
resolve(err);
}
}
Additional info (update)
Unless I'm mistaken this should mean that await act(async () => { render(...); });
"only" awaits the first event loop. I.e. if you add a timer or something else in your mock or React code that might need multiple loops to resolve, it's not gonna be awaited (please correct me if I'm wrong!).