act

Flushes React renders, effects and updates in tests so assertions see the finished DOM.

Signature
await act(async actFn)
  • Added in React 18.3.1

Parameters

ParameterTypeDescription
$actFn() => Promise<void> | voidAn async function wrapping renders or interactions for the components being tested. Updates triggered inside actFn are added to an internal act queue, which is then flushed together to process and apply changes to the DOM. Because it is async, React also runs code that crosses an async boundary and flushes any updates it schedules.

Return value

voidact does not return anything. The call is still awaited so that React finishes flushing everything queued inside actFn before the next line of the test runs.

Errors

React errors with "The current testing environment is not configured to support act(...)" when `global.IS_REACT_ACT_ENVIRONMENT` is not set to `true`.

Changelog

  • PHP 19.0.0`react-dom/test-utils` was removed; import `act` from `react` instead of from the test-utils entry point.
  • PHP 18.3.1`act` is exported from the `react` package itself.
  • PHP 18.0.0The synchronous form is discouraged: React recommends `await act(async () => {...})` and plans to deprecate and remove the sync version.

act draws a boundary around a piece of test code and guarantees that, when it returns, React has done everything that code triggered: the render is committed, effects have run, state updates have been flushed and the DOM matches. Without it your assertions run against a half-finished tree, and React logs the familiar "An update to X inside a test was not wrapped in act(...)" warning.

When to reach for it

Most of the time you do not call it directly — React Testing Library wraps render, fireEvent and userEvent in act for you, and sets IS_REACT_ACT_ENVIRONMENT. Reach for it explicitly when you drive a root yourself with createRoot, when you advance fake timers that trigger updates, or when you resolve a promise that a component is waiting on.

What trips people up

Prefer await act(async () => {...}) over the synchronous form. The sync version works in many cases but not all, and React intends to remove it. An act warning that appears despite wrapping usually means an update escaped the callback — a timer that fires later, a promise that resolves after the test moved on, or state set from an unawaited async call. Wrap the flush point, not the setup.

Example

Example · jsx
// Counter.test.jsx — Jest or Vitest with a jsdom environment
import { act, useState } from "react";
import { createRoot } from "react-dom/client";

// Normally set once in a global test setup file.
globalThis.IS_REACT_ACT_ENVIRONMENT = true;

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(c => c + 1)}>Clicked {count} times</button>
  );
}

test("increments when clicked", async () => {
  const container = document.createElement("div");
  document.body.appendChild(container);
  const root = createRoot(container);

  await act(async () => {
    root.render(<Counter />);
  });
  expect(container.textContent).toBe("Clicked 0 times");

  const button = container.querySelector("button");
  await act(async () => {
    button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
  });
  expect(container.textContent).toBe("Clicked 1 times");

  await act(async () => {
    root.unmount();
  });
  container.remove();
});

When to use it

  • Render a component into a container with createRoot and assert on the committed DOM.
  • Dispatch a click and assert on the state that the resulting update produced.
  • Advance fake timers or resolve a pending promise and flush the effects that follow.

Frequently asked questions

How do I fix "An update to X inside a test was not wrapped in act(...)"?

Wrap the code that triggers the update in await act(async () => {...}) — the warning means React committed an update after your test had moved on. Common causes are resolved promises, timers advanced outside act, and calling a component method directly instead of through fireEvent or userEvent.

Do I need act with React Testing Library?

Rarely. Testing Library already wraps render, fireEvent and userEvent in act and sets IS_REACT_ACT_ENVIRONMENT for you. You still need it when you step fake timers, resolve a promise by hand, or drive a createRoot yourself.

Where do I import act from now?

From react: import { act } from 'react'. It has been exported from the react package since 18.3.1, and react-dom/test-utils was removed in React 19, so the older ReactDOM.unstable_batchedUpdates and test-utils imports no longer work.

More examples

Flushing a resolved promise and its effects

The assertion sees "Ada", not "Loading…". Because the act callback is async, React lets the microtask queue drain inside the act scope and flushes the setUser update that the .then handler schedules. With a synchronous act the effect would have run but the promise would still be pending, and the test would see the loading text plus an act warning when the update landed later.

Example · jsx
import { act, useEffect, useState } from "react";
import { createRoot } from "react-dom/client";

globalThis.IS_REACT_ACT_ENVIRONMENT = true;

function User({ load }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    let cancelled = false;
    load().then(result => {
      if (!cancelled) setUser(result);
    });
    return () => {
      cancelled = true;
    };
  }, [load]);

  return <p>{user ? user.name : "Loading…"}</p>;
}

test("shows the loaded user", async () => {
  const container = document.createElement("div");
  document.body.appendChild(container);
  const root = createRoot(container);
  const load = () => Promise.resolve({ name: "Ada" });

  await act(async () => {
    root.render(<User load={load} />);
  });

  // The await inside act lets the promise settle and the resulting
  // setUser update flush before the assertion runs.
  expect(container.textContent).toBe("Ada");

  await act(async () => root.unmount());
});

Fake timers, and why Testing Library usually removes the need for act

The test asserts "Saved", jumps the clock, then asserts "Dismissed". Testing Library's render already runs inside act, which is why most tests never mention it; advancing fake timers is not something the library can intercept, so the timer callback's state update is the one place an explicit act is still required.

Example · jsx
import { act, useEffect, useState } from "react";
import { render, screen } from "@testing-library/react";

function Toast({ ms }) {
  const [visible, setVisible] = useState(true);

  useEffect(() => {
    const id = setTimeout(() => setVisible(false), ms);
    return () => clearTimeout(id);
  }, [ms]);

  return visible ? <p>Saved</p> : <p>Dismissed</p>;
}

test("hides itself after the delay", async () => {
  jest.useFakeTimers();

  // render() is already wrapped in act by Testing Library,
  // and IS_REACT_ACT_ENVIRONMENT is set for you.
  render(<Toast ms={3000} />);
  expect(screen.getByText("Saved")).toBeInTheDocument();

  // Advancing timers happens outside React, so wrap it yourself.
  await act(async () => {
    jest.advanceTimersByTime(3000);
  });

  expect(screen.getByText("Dismissed")).toBeInTheDocument();
  jest.useRealTimers();
});

Discussion

  • Be the first to comment on this lesson.