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.
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();
});