React

Testing an async component without setTimeout

An async component is tested with findBy and await: the test waits for the DOM to change. A hand-written setTimeout produces arbitrary waits.

Key points

  • findBy repeats the query until the timeout: that is the correct wait, not a setTimeout.
  • Stub at the boundary, on fetch, not inside the component.
  • The default timeout is 1,000 ms: raise it once with configure in CI, not with scattered waits.

An async component is tested with findBy and await: the test waits for the DOM to change. A hand-written setTimeout adds arbitrary waits and flaky runs.

The base rule

In Testing Library, findBy is waitFor plus getBy: it repeats the query every 50 ms for up to 1,000 ms. When the data arrives early the test moves on at once; when it never arrives the failure names the query it tried.

The test that works as a model

import { render, screen } from "@testing-library/react";

test("mostra il totale quando la risposta arriva", async () => {
  vi.stubGlobal(
    "fetch",
    vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve({ total: 12 }) })),
  );
  render();
  expect(await screen.findByText("12")).toBeInTheDocument();
});

The stub replaces fetch, which is the component boundary: the inner logic stays real and the network disappears. The assertion reads the text a person sees, not internal state.

Three common mistakes

  • Fixed wait: await new Promise((r) => setTimeout(r, 500)) makes the test slow at best and flaky at worst.
  • Synchronous assertion: getByText straight after render finds nothing, because the promise has not settled yet.
  • Fake timers: with vi.useFakeTimers() userEvent needs advanceTimers, otherwise every click stalls.

Installation

npm i -D vitest jsdom @testing-library/react @testing-library/jest-dom
npx vitest run

All the packages are MIT and run on jsdom locally: no network, no browser, the same command in CI.

Live records in the catalogue

Three async records worth that wait: spinner, ai-chat-input and agent-dock. To browse by category start from 111,985 components.

The limitation

The test waits for the DOM, it does not prove the absence of a race: two requests that settle out of order can still pass it. The default 1,000 ms timeout also fails on a slow CI in ways that never show up locally.

Where to continue

The test model is in Testing Library: the five tests; waiting with Suspense is in Suspense; fetching without a library is in data fetching without libraries; the measured library comparison is in React libraries 2026.

Frequently asked questions

Should the test wait with setTimeout?

No. findBy repeats the query until the timeout, so the test moves on as soon as the DOM changes, with no fixed wait.

Why does an act warning appear?

Because state changes outside an await. Wrap the action in await userEvent or in await waitFor.

Fetch or module: which to mock?

Fetch, the boundary. Mocking the module tests the mock instead of the component.

Can the timeout be raised?

Yes, with configure({ asyncUtilTimeout: 3000 }) once in the setup. A global value beats scattered waits.

Do these tests prove there are no races?

No. They prove the DOM shows the expected data in that order; races show up with repeated runs in a real browser.