React Testing Library: Understanding act() and when to use it.

In the world of React Testing Library, the act() function often surfaces as a somewhat mysterious entity. You might have seen warnings like “An update to ComponentName inside a test was not wrapped in act(…),” leaving you puzzled. In this article, we will demystify the act() function and explain why it’s crucial for certain test scenarios.

The Purpose of act()

The primary goal of act() is to ensure that all updates related to React components (such as state changes, effects, etc.) are processed and applied before moving on to the next operation in your test. This helps your tests run in a way that simulates how React works in a real browser environment.

When Do You Need act()?

You often need act() when your test involves some sort of user interaction that triggers a state update or effect in your component. For example, triggering a button click that, in turn, modifies some state in your component.

Example Without act()

Consider the following simple component:

import React, { useState } from 'react';

const Counter = () => {
const [count, setCount] = useState(0);

return (
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
<span>{count}</span>
</div>

);
};
export default Counter;

Now, let’s write a test case that does not use act():

import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import Counter from './Counter';

it("increments the count", () => {
const { getByText } = render(<Counter />);

fireEvent.click(getByText('Increment'));
expect(getByText('1')).toBeInTheDocument();
});

In this example, you might get away without seeing any warnings. However, if your component had side effects like asynchronous calls, you could end up with the infamous “not wrapped in act(…)” warning.

Enter findBy

findBy queries are like the superhero version of getBy queries but for asynchronous actions. They return a Promise that resolves when the queried element is found or rejects if the element is not found within a specified timeout.

The syntax is similar to getBy:

const button = await findByRole('button', { name: 'Click Me' });

Why findBy is Often Better

Implicit act()

findBy wraps the query with act() implicitly. This makes your test not only cleaner but also ensures that you don’t forget to wrap your asynchronous calls in act().

Asynchronous Made Easy

Let’s consider an example:

import React, { useEffect, useState } from 'react';

const AsyncComponent = () => {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data')
.then((res) => res.json())
.then((data) => setData(data));
}, []);
return <div>{data ? data.message : 'Loading...'}</div>;
};

Using act()

import { render, act } from '@testing-library/react';

it('should fetch and display data', async () => {
const { getByText } = render(<AsyncComponent />);

await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
});
expect(getByText('Hello')).toBeInTheDocument();
});

Using findBy

import { render } from '@testing-library/react';

it('should fetch and display data', async () => {
const { findByText } = render(<AsyncComponent />);

const message = await findByText('Hello');
expect(message).toBeInTheDocument();
});

In the findBy example, you can see that the code is much cleaner. It is doing the same thing as the act() example, but it’s less verbose and more intuitive.

Error Handling

If findBy does not find the element within the specified timeout, it will automatically throw an error, making it easier for you to debug what went wrong. This is in line with the philosophy of writing tests that closely resemble how your code runs in the real world.

Other Implicit act() Calls

findBy is not the only function whiwraps the query with act() implicitly. Here are the other occasions where this happens.

Conclusion

While act() is powerful and offers a greater degree of control, findBy queries provide a more elegant and less verbose way of testing asynchronous operations in React components. The implicit act() wrapping ensures that your tests are both clean and reliable, often making findBy a preferable choice for dealing with asynchronous behavior in your tests.

,