Search code examples
javascriptreactjsreact-testing-librarytesting-library

Why is a state change not invoking a react act() error?


Most people are searching for how to get rid of react act() errors in testing. I'm wondering the opposite. I have code that induces a state change. That state change should trigger a re-render in react (and it does). However, I did not act() wrap that state change, and an act() error was not emitted. Why is this?

Consider the following goofy component:

export function SweetTextboxBro() {
  const [text, setText] = React.useState("");
  return (
    <>
      <input
        type="text"
        role="input"
        name="sweetbox"
        onChange={(evt) => {
          setText(evt.currentTarget.value);
        }}
        value={text}
      />
      <p>{text}</p>
    </>
  );
}

Now, consider this minimal test:

import * as React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { SweetTextboxBro } from "../SweetTextboxBro";
import { sleep } from "../timing";

test("it emits act errors", async () => {
  render(<SweetTextboxBro />); // should internally call act()
  const textbox = screen.getByRole("input");
  const input = "weeee";
  // begin state change!
  userEvent.type(textbox, input);
  await sleep(200);
  // state updated! the DOM  has changed! but no act() error... is emitted
  expect(await screen.findByText(input)).toBeInTheDocument();
});

Act errors occur when react re-renders content outside of an act() call. I'm unclear why this case does not apply.


Solution

  • React Testing Library wraps fireEvent and userEvent in a call to act internally. In addition, React automatically handles state changes generated from click events, meaning they wouldn't trigger act warnings in the first place.

    act should only ever be needed when using asynchronous code that runs as a result of a resolved promise, e.g., when making API calls.

    This means that you don't even need to await for the input to change in your test. The following test would also pass.

    test("it emits act errors", () => {
      render(<SweetTextboxBro />); // should internally call act()
      const textbox = screen.getByRole("input");
      const input = "weeee";
      userEvent.type(textbox, input);
      expect(screen.getByText(input)).toBeInTheDocument();
    });