Business
Jobs
  • About Us
  • Solutions
    • Job Postings
      Post your job and receive qualified candidates in 48h.
    • Candidate Assessments
      500+ technical and psychological tests, plus anti-fraud.
    • Headhunting
      Tailor-made executive search from start to finish.
    • Payroll + EOR
      Payroll dispersal and EOR across 15+ LATAM countries.
  • Pricing
  • Jobs

0

201
Views
Why does this official React testing recipe using await/act/async actually work?

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!

about 4 years ago · Juan Pablo Isaza
1 answers
Answer question

0

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:

  1. The useEffect callback is flushed (putting fetch on the event loop).
  2. The setImmediate callback "ensures" our mock promise / fetch is resolved.
  3. A third flush happens by a recursion inside the setImmediate callback (called by enqueueTask) making the state changes appear in the DOM.
  4. When there's nothing left to flush it calls the outer most resolve and our act 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 one event loop unless a new task is added by the latest flush. I.e. it's possible to add promises recursively if there's a flush in between, also micro tasks such as promise chains will probably resolve during the first loop (since they are technically "blocking" (source)).

That means that if you add a timer or something else in your mock or React code that might naturally need multiple loops to resolve, it's not gonna be awaited because the "after" event is not captured before resolving since the then listeners are not attached / returned to the outer promise (please correct me if I'm wrong!).

about 4 years ago · Juan Pablo Isaza Report
Answer question
Find remote jobs

Discover the new way to find a job!

Top jobs
Top job categories
Business
Post vacancy Pricing Sales
Legal
Terms and conditions Privacy policy
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Show me some job opportunities
There's an error!