skip to content

Using Async Utilities Whenever a Components State Updates

Local state updates are asynchronous. Re-renders can delay elements appearing in the DOM.


· 2 min read

Last Updated:


TL;DR

State updates are asynchronous operations and so their outcomes must be asserted against using asynchronous utilities provided by React Testing Library.

When to Use Asynchronous Queries

I thought that the outcome of state updates could be asserted synchronously.

Component.jsx
const Component = () => {
const [isClicked, setIsClicked] = useState(false);
const handleClick = () => {
setIsClicked(true);
};
return (
<button onClick={handleClick}>{isClicked ? <div>clicked</div> : <div>not clicked</div>}</button>
);
};

Testing that the text content of the button changes on click.

Component.test.jsx
test("shows new text when button is clicked", async () => {
render(<Component />);
const buttonToClick = screen.getByRole("button", { name: /not clicked/i });
expect(buttonToClick).toBeVisible();
user.click(buttonToClick);
expect(screen.getByRole("button", { name: /clicked/i })).toBeVisible();
});

I would expect this test to be successful. But it fails.

TestingLibraryElementError: Unable to find an element with the text: clicked.

It fails because we are not waiting for the Component to process its state change and update. Updating the test to use an asynchronous query solves this.

Component.test.jsx
expect(await screen.findByRole("button", { name: /clicked/i })).toBeVisible();

For a long time I thought that async queries were only for obvious asynchronous operations—network calls or timeouts, things like that. I thought that state updates were handled differently, that you could assert on their outcome as though they were synchronous. I thought this because of two reasons.

First, time and again, example tests don’t use the async queries when checking a state update.

UserEventTestWithoutAsyncQueries.test.jsx
test("test theme button toggle", () => {
render(<App />);
const buttonEl = screen.getByText(/Current theme/i);
userEvent.click(buttonEl);
expect(buttonEl).toHaveTextContent(/dark/i);
});

Secondly, every use case I’ve seen for these async queries involves a network call. The focus is always on testing asynchronous network calls, and this focus shows on the supported example page here where explicit attention is drawn to the fact that the server request does not resolve immediately, necessitating the use of async utilities to verify the state of the component. The same emphasis is notably absent from the local state update example.

I didn’t think async queries were necessary when checking the DOM after a local state update. But it makes sense—and it’s clearly stated in the official documentation.

Several utilities are provided for dealing with asynchronous code. These can be useful to wait for an element to appear or disappear in response to an event, user action, timeout, or Promise.

Example

I suppose we need to remember that setState is asynchronous and that a render can delay elements from showing in the DOM. A re-render takes time and affects the appearance/disappearance of elements just the same as any other asynchronous operation.