The act(...) warning: what it is and why it matters
React's act(...) warning can be one of the more confusing messages you'll encounter in testing. It typically appears when a component updates outside of what React considers an expected interaction. Understanding what triggers it—and how to respond—will save you debugging time and help you write better tests.
The warning exists to flag untested behavior. Consider a class component that calls an async function (updateUsername) which sets state after a promise resolves. If you forget to call that function in the component or forget to pass the new value, tests that only verify the initial render will still pass—but the UI can get stuck in an intermediate state (for example, a "Saving..." label that never disappears).
React can't know about updates triggered from asynchronous code running outside its own callstack, such as promise resolutions you're managing manually or Jest fake timers. When that happens, React warns you that something changed outside an act(...) block, signaling that your test likely didn't cover the relevant state update.
The most common fix: assert on the UI after async work
The simplest solution isn't to call act directly—it's to strengthen your test so it covers the full component behavior. If your component shows a loading indicator while saving, add an assertion that waits for that indicator to disappear:
test('calls updateUsername with the new username', async () => {
const handleUpdateUsername = jest.fn()
const fakeUsername = 'sonicthehedgehog'
render(<UsernameForm updateUsername={handleUpdateUsername} />)
const usernameInput = screen.getByLabelText(/username/i)
user.type(usernameInput, fakeUsername)
user.click(screen.getByText(/submit/i))
expect(handleUpdateUsername).toHaveBeenCalledWith(fakeUsername)
await waitForElementToBeRemoved(() => screen.queryByText(/saving/i))
})
That addition ensures the state update happens and that your component renders correctly in both states. The test will now pass for both class and function components, since React Testing Library's async utilities wrap everything in act automatically.
Interestingly, this bug is caught only without the extra assertion in function components, not class components. The React team chose to apply the warning only to hooks-based updates to avoid flooding existing class-component tests with noise—a practical decision that still guides developers toward better test coverage for new code.
Alternative: manually await the mocked promise
If your async operation provides no visual feedback to assert on, you can manually wrap the promise resolution in an act call:
test('calls updateUsername with the new username', async () => {
const promise = Promise.resolve() // You can also resolve with a mocked return value if necessary
const handleUpdateUsername = jest.fn(() => promise)
const fakeUsername = 'sonicthehedgehog'
render(<UsernameForm updateUsername={handleUpdateUsername} />)
const usernameInput = screen.getByLabelText(/username/i)
user.type(usernameInput, fakeUsername)
user.click(screen.getByText(/submit/i))
expect(handleUpdateUsername).toHaveBeenCalledWith(fakeUsername)
// we await the promise instead of returning directly, because act expects a "void" result
await act(async () => {
await promise
})
})
You can import act directly from @testing-library/react, which re-exports it from react-dom/test-utils or react-test-renderer. This eliminates the warning, but it won't catch the earlier bug where the signature update call was commented out. Whenever possible, prefer asserting on the resulting UI state over merely disposing of the warning.
When you need to call act manually
If your test uses all the async utilities correctly and still triggers warnings, the update is likely happening outside React's knowledge. There are a few common scenarios:
1. Advancing Jest fake timers
Components that poll an API on an interval (e.g., with setInterval) present a tricky case. Even if you handle cleanup properly, advancing a timer inside a test triggers a component update outside React's callstack, producing the warning:
// ...
let current = true
function tick() {
setState({status: 'pending'})
checkStatus(orderId).then(
d => {
// ...
React Testing Library has no built-in utility for fake timers, so you need to wrap the timer advancement yourself:
import * as React from 'react'
import { render, screen, act } from '@testing-library/react'
import { checkStatus } from '../api'
jest.mock('../api')
test('polling backend on an interval', async () => {
const orderId = 'abc123'
const orderStatus = 'Order Received'
checkStatus.mockResolvedValue({ orderStatus })
render(<OrderStatus orderId={orderId} />)
expect(screen.getByText(/\.\.\./i)).toBeInTheDocument()
expect(checkStatus).toHaveBeenCalledTimes(0)
// advance the timers by a second to kick off the first request
act(() => jest.advanceTimersByTime(1000))
expect(await screen.findByText(orderStatus)).toBeInTheDocument()
expect(checkStatus).toHaveBeenCalledWith(orderId)
expect(checkStatus).toHaveBeenCalledTimes(1)
})
The warning disappears once the timer tick happens inside act.
2. Testing custom hooks
When you call state-updating functions returned from a custom hook (such as increment and decrement from a counter hook) in a test, those updates aren't in React's callstack. Wrap those calls in act:
import * as React from 'react'
import { renderHook, act } from '@testing-library/react'
import useCount from '../use-count'
test('increment and decrement updates the count', () => {
const { result } = renderHook(() => useCount())
expect(result.current.count).toBe(0)
act(() => result.current.increment())
expect(result.current.count).toBe(1)
act(() => result.current.decrement())
expect(result.current.count).toBe(0)
})
Since act is re-exported from React Testing Library, no extra import is needed.
3. Calling methods exposed via useImperativeHandle
If you test a component that exposes a ref method which internally calls setState, the same principle applies. Call those imperative methods inside act to silence the warning:
import * as React from 'react'
import { render, screen, act } from '@testing-library/react'
import ImperativeCounter from '../imperative-counter'
test('can call imperative methods on counter component', () => {
const counterRef = React.createRef()
render(<ImperativeCounter ref={counterRef} />)
expect(screen.getByText('The count is: 0')).toBeInTheDocument()
act(() => counterRef.current.increment())
expect(screen.getByText('The count is: 1')).toBeInTheDocument()
act(() => counterRef.current.decrement())
expect(screen.getByText('The count is: 0')).toBeInTheDocument()
})
Bottom line
The act(...) warning is often a signal that your test isn't completely exercising the component's behavior. Most of the time, the right fix is to wait for the asynchronous state update to finish using React Testing Library's async utilities. The warning serves as a useful guardrail—not something to suppress blindly, but a prompt to verify that everything your component does during a test is covered.



