Empresas
Empregos
  • Sobre nós
  • Soluções
    • Publicação de vagas
      Publique sua vaga e receba candidatos qualificados em 48h.
    • Avaliações de candidatos
      Mais de 500 testes técnicos e psicológicos, mais anti-fraude.
    • Headhunting
      Busca executiva personalizada do início ao fim.
    • Folha de Pagamento + EOR
      Dispersão de folha e EOR em mais de 15 países da LATAM.
  • Preços
  • Empregos

0

202
Visualizações
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 Respostas
Responde à pergunta

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 Relatório
Responde à pergunta
Encontrar trabalhos remotos

Descubra a nova forma de encontrar um emprego!

melhores empregos
Principais categorias de trabalho
Empresas
Postar vaga Preços Comercial
Jurídico
Termos e Condições Política de privacidade
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Recomende algumas ofertas para mim
Preciso de ajuda