Issue #971

When testing React components, dealing with tasks that happen at different times is super important to make sure your tests give reliable results. React Testing Library gives you two important tools for dealing with these situations: act and waitFor. Even though they both help with handling actions that don’t happen right away, they’re used in different situations.

act: Ensuring Synchronous Updates

The act function wraps parts of your code that cause updates linked to changes in state, effects, or other delayed actions. Its main goal is to make sure these updates finish before moving on to other checks. This is especially important when testing components that do things asynchronously, as it keeps the testing environment steady and reliable.

Consider a straightforward example featuring a counter component:

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

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

test('increments the counter when the button is clicked', async () => {
  const { getByText } = render(<Counter />);
  const incrementButton = getByText('Increment');
  const counterDisplay = getByText('0');
  await act(async () => {
    fireEvent.click(incrementButton);
  });
  expect(counterDisplay.textContent).toBe('1');
});

In this instance, act encapsulates the fireEvent.click call, ensuring that the state update triggered by the button click is executed prior to verifying the updated value of the counter.

Understanding When act Is Necessary

It’s really important to know when to use “act” and when not to. Sometimes, developers might use “act” when they don’t really need to, especially around functions like render and fireEvent, which are already wrapped in act.

// ❌ Unnecessary use of act
act(() => {
  render(<Example />)
})

const input = screen.getByRole('textbox', {name: /choose a fruit/i})
act(() => {
  fireEvent.keyDown(input, {key: 'ArrowDown'})
})

// ✅ Correct usage without act
render(<Example />)
const input = screen.getByRole('textbox', {name: /choose a fruit/i})
fireEvent.keyDown(input, {key: 'ArrowDown'})

If you see an act warning during testing, it usually means something unexpected is happening in your test or there are still updates after tests complete.

Simplifying with findByRole

In scenarios where you’re looking to await the presence of an element identified by role, React Testing Library offers a concise alternative: findByRole. By leveraging findByRole, you can seamlessly await the appearance of the desired element without resorting to additional utilities

const input = await screen.findByRole('textbox', {name: /choose a fruit/i});

This succinct syntax simplifies your testing code while ensuring readability and maintainability, further streamlining your asynchronous testing workflow.

waitFor: Handling Asynchronous Changes

On the other hand, the waitFor function is incredibly useful for handling changes that happen over time in your component’s state or the page’s elements. It’s like patiently waiting for something to happen, whether it’s an element appearing or changing, or expecting an action to finish after a certain amount of time.

import React, { useEffect, useState } from 'react';
import { render, waitFor } from '@testing-library/react';

function AsyncMessage() {
  const [message, setMessage] = useState('');
  useEffect(() => {
    const timer = setTimeout(() => {
      setMessage('Hello, World!');
    }, 500);
    return () => clearTimeout(timer);
  }, []);
  return <p>{message}</p>;
}

test('displays the message after a delay', async () => {
  const { getByText } = render(<AsyncMessage />);
  await waitFor(() => {
    expect(getByText('Hello, World!')).toBeInTheDocument();
  });
});

In our example, the waitFor function patiently waits for the message to show up in the document before moving on to the next step of checking. It does this by managing the asynchronous delay introduced by the setTimeout function behind the scenes.

The default waiting time between checks is 50 milliseconds. It immediately runs your function before starting this checking process. If you don’t provide a specific timeout, it waits for 1000 milliseconds before giving up.

Conclusion

In conclusion, both act and waitFor represent indispensable tools within the React Testing Library arsenal for managing asynchronous behavior in tests. Understanding their nuances and judiciously applying them according to the specific testing scenario empowers developers to craft more robust and reliable tests for their React components.

Read more