The act() warning in React development builds is happening because your test environment is trying to reconcile asynchronous updates outside of React’s controlled "act" environment, which is crucial for ensuring consistent test results.
This warning pops up when React detects asynchronous operations (like setTimeout, Promises, or network requests) that complete after your test has finished executing its synchronous code, but before React has had a chance to update the DOM and re-render. This can lead to tests passing sometimes and failing others, or tests that don’t accurately reflect the final state of your UI.
Here are the common causes and how to fix them:
1. Asynchronous Operations Not Wrapped in act()
Diagnosis: You have code in your test that initiates an asynchronous operation (e.g., fetch, setTimeout) and then immediately asserts on the state or DOM before that operation has completed.
Cause: Your test is finishing its synchronous execution, but the asynchronous task is still running in the background. React’s test renderer or DOM testing library doesn’t know to wait for these updates.
Fix: Wrap any code that performs asynchronous updates within act(). This tells React’s testing utilities to wait for all asynchronous updates to settle before proceeding.
import { render, screen } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import MyComponent from './MyComponent';
test('updates after async operation', async () => {
// Render the component
render(<MyComponent />);
// Simulate an async operation (e.g., a button click that triggers a fetch)
// If this involves state updates, it needs to be inside act()
await act(async () => {
// Simulate user interaction or other async setup
// For example, if a button click inside MyComponent triggers a fetch
// and then updates state, that entire flow should be wrapped.
// If you're directly calling a function that causes async updates:
await new Promise(resolve => setTimeout(resolve, 100)); // Simulate delay
});
// Now assert on the updated DOM
expect(screen.getByText('Updated Content')).toBeInTheDocument();
});
Why it works: act() ensures that all pending microtasks and macrotasks (like Promises and timers) are flushed, allowing React to complete its rendering and state updates within the controlled testing environment.
2. Missing await for Asynchronous Test Steps
Diagnosis: Your test has asynchronous steps but doesn’t await their completion before making assertions.
Cause: The test runner proceeds to the assertion before the asynchronous operation has finished, leading to assertions on stale data or UI.
Fix: Ensure you await all promises returned by asynchronous test utilities or your own asynchronous functions.
import { render, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event'; // Example for user interaction
test('handles async data fetch on button click', async () => {
render(<MyComponent />);
const user = userEvent.setup();
// Simulate clicking a button that triggers an async fetch
await user.click(screen.getByRole('button', { name: /load data/i }));
// The click itself might trigger async updates, but if the fetch
// returns a promise that your component handles, you need to wait for it.
// If MyComponent has a useEffect that fetches data and updates state,
// and you want to assert on the result *after* the fetch, you might need:
await screen.findByText('Data Loaded Successfully', { timeout: 2000 }); // Wait for the async update
});
Why it works: await pauses the execution of the test function until the awaited promise resolves, ensuring that subsequent code runs only after the asynchronous task is complete. screen.findBy* is a convenient way to wait for elements that appear asynchronously.
3. Global Timers Not Mocked or Controlled
Diagnosis: Your test uses setTimeout, setInterval, or other timer-based functions, and these are causing asynchronous updates to occur outside of act().
Cause: Native timers continue to run in the background, even when your test code has ostensibly finished. If these timers trigger React state updates, they bypass act().
Fix: Use Jest’s fake timers to control time within your tests.
import { render, screen } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
// Enable fake timers for this test file
jest.useFakeTimers();
test('updates after setTimeout', () => {
render(<MyComponent />);
// Advance timers by 100ms. If this causes a state update,
// it's still best practice to wrap it if the update is complex or
// involves multiple re-renders that need to be batched by act.
act(() => {
jest.advanceTimersByTime(100);
});
expect(screen.getByText('Delayed Content')).toBeInTheDocument();
// Clean up fake timers after the test
jest.useRealTimers();
});
Why it works: Fake timers allow you to manually advance time, ensuring that all timer callbacks are executed synchronously within the act() block, giving React full control over the update process.
4. Uncaught Promises in Test Setup or Component Logic
Diagnosis: An asynchronous operation within your component or test setup results in an unhandled promise rejection.
Cause: Uncaught promise rejections can sometimes interfere with the test runner’s ability to track asynchronous operations, leading to the act() warning. While not directly an act() issue, it often co-occurs with asynchronous problems.
Fix: Ensure all promises are handled, either with .catch() or try...catch blocks in async/await functions.
// In your component's async logic:
async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
// update state with data
} catch (error) {
console.error("Failed to fetch data:", error);
// update state to show error
}
}
// In your test, if you mock fetch and it rejects:
test('handles fetch error', async () => {
global.fetch = jest.fn().mockRejectedValueOnce(new Error('Network Error'));
render(<MyComponent />);
// If the component is supposed to show an error message:
await screen.findByText('Error loading data', { timeout: 2000 });
});
Why it works: Properly handling promise rejections prevents unexpected errors that can disrupt the test environment and ensures that asynchronous flows complete predictably.
5. Incorrectly Using act() (Over-wrapping or Under-wrapping)
Diagnosis: You might be wrapping too much in act() (including synchronous code that doesn’t need it) or too little (missing the outermost act() call that encloses the asynchronous update).
Cause: act() is intended to group together operations that lead to an asynchronous update and then wait for that update to complete. Wrapping unrelated synchronous code can sometimes mask issues or add unnecessary overhead. Not wrapping the actual asynchronous update that causes a re-render is the most common mistake.
Fix: Identify the specific asynchronous operation that causes a state update and re-render. Wrap the initiation of that operation and any subsequent synchronous code that depends on its completion within a single act() block.
// Good: Wrap the async operation and subsequent assertion
test('updates state after async call', async () => {
render(<MyComponent />);
// Assume MyComponent has a method like `loadData` that returns a promise
// and updates state upon resolution.
await act(async () => {
await MyComponentInstance.loadData(); // This is the async operation
});
expect(screen.getByText('Data loaded')).toBeInTheDocument();
});
// Less ideal (if loadData is truly async and updates state internally):
// Might work, but less explicit about waiting for the async part.
// await MyComponentInstance.loadData();
// expect(screen.getByText('Data loaded')).toBeInTheDocument();
Why it works: act() is designed to batch updates. By wrapping the entire lifecycle of an asynchronous operation that results in a UI update, you ensure React processes all state changes and re-renders consistently before your test proceeds.
6. React Concurrent Mode / Suspense Issues in Tests
Diagnosis: If you’re using Concurrent Features like Suspense, updates might be batched or deferred in ways that act() needs to be aware of.
Cause: Concurrent rendering introduces new patterns for handling asynchronous data fetching and rendering. Traditional synchronous testing approaches might not fully capture the behavior of these features.
Fix: Ensure your act() calls encompass the entire lifecycle of a Suspense-driven data fetch and render. screen.findBy* is often your friend here as it waits for elements to appear, which is a good proxy for a Suspense-resolved component.
import { render, screen } from '@testing-library/react';
import { Suspense } from 'react';
import MyComponentThatSuspends from './MyComponentThatSuspends';
test('renders data after Suspense resolves', async () => {
render(
<Suspense fallback={<div>Loading...</div>}>
<MyComponentThatSuspends />
</Suspense>
);
// Wait for the content that appears *after* the Suspense boundary resolves
// This implicitly handles the asynchronous data fetching and rendering.
await screen.findByText('Resolved Data', { timeout: 3000 });
});
Why it works: screen.findBy* and other waitFor utilities are designed to work with React’s asynchronous nature, including Concurrent Mode. They poll the DOM until a condition is met, effectively waiting for asynchronous updates to complete and the UI to reflect them.
The next error you’ll likely encounter after fixing act() warnings is related to flaky tests due to race conditions if you haven’t fully accounted for all asynchronous paths, or assertion failures because the state or UI isn’t what you expected, even if act() is correctly used.