Empresas
Empleos
  • Sobre nosotros
  • Soluciones
    • Publicación de vacantes
      Publica tu vacante y recibe candidatos calificados en 48h.
    • Evaluación de candidatos
      500+ pruebas técnicas y psicológicas, más anti-fraude.
    • Headhunting
      Búsqueda ejecutiva a la medida de principio a fin.
    • Nómina + EOR
      Dispersión de nómina y EOR en más de 15 países de LATAM.
  • Precios
  • Empleos

0

203
Vistas
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 Respuestas
Responde la pregunta

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 Denunciar
Responde la pregunta
Encuentra empleos remotos

¡Descubre la nueva forma de encontrar empleo!

Top de empleos
Top categorías de empleo
Empresas
Publicar vacante Precios Comercial
Legal
Términos y condiciones Política de privacidad
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Recomiéndame algunas ofertas
Necesito ayuda