Epic Web Conf late-bird tickets are available now, hurry!

Get your tickets here

Join the community and network with other great web devs.

Time's up. The sale is over

Fix the "not wrapped in act(...)" warning

February 3rd, 2020 — 13 min read

by Lubo Minar
by Lubo Minar
No translations available.Add translation

Watch "Fix the "not wrapped in act(...)" warning" on egghead.io

Watch "Fix the "not wrapped in act(...)" warning with Jest fake timers" on egghead.io

Watch "Fix the "not wrapped in act(...)" warning when testing Custom Hooks" on egghead.io

Watch "Fix the "not wrapped in act(...)" warning when testing the useImperativeHandle Hook" on egghead.io

Imagine you have a component like this:

Here's the code for that (if you were to do it with class components, don't worry a function version is coming later in the post):

class UsernameFormClass extends React.Component {
  state = {status: 'idle', error: null}
  handleSubmit = async event => {
    event.preventDefault()
    const newUsername = event.target.elements.username.value
    this.setState({status: 'pending'})
    try {
      await this.props.updateUsername(newUsername)
      this.setState({status: 'fulfilled'})
    } catch (e) {
      this.setState({status: 'rejected', error: e})
    }
  }
  render() {
    const {error, status} = this.state

    return (
      <form onSubmit={this.handleSubmit}>
        <label htmlFor="username">Username</label>
        <input id="username" />
        <button type="submit">Submit</button>
        <span>{status === 'pending' ? 'Saving...' : null}</span>
        <span>{status === 'rejected' ? error.message : null}</span>
      </form>
    )
  }
}

So, let's imagine we want to write a test for this. Here's what you might write to verify that the component is working properly:

import * as React from 'react'
import user from '@testing-library/user-event'
import {render, screen} from '@testing-library/react'

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)
})

Great! So now, if we make some sort of typo and not call the updateUsername function or we forget to call it with the new username then our test will fail and it provides us value.

But, what if we rewrite this to a function component with hooks? Let's try that:

function UsernameForm({updateUsername}) {
  const [{status, error}, setState] = React.useState({
    status: 'idle',
    error: null,
  })

  async function handleSubmit(event) {
    event.preventDefault()
    const newUsername = event.target.elements.username.value
    setState({status: 'pending'})
    try {
      await updateUsername(newUsername)
      setState({status: 'fulfilled'})
    } catch (e) {
      setState({status: 'rejected', error: e})
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="username">Username</label>
      <input id="username" />
      <button type="submit">Submit</button>
      <span>{status === 'pending' ? 'Saving...' : null}</span>
      <span>{status === 'rejected' ? error.message : null}</span>
    </form>
  )
}

And the cool thing about React Testing Library is because it's free of implementation details we can run those exact same tests with this refactored version of our component.

The dreaded act(...) warning

Unfortunately, if we try that, we'll get this really annoying warning in the console that looks like this:

console.error node_modules/react-dom/cjs/react-dom.development.js:530
  Warning: An update to UsernameForm inside a test was not wrapped in act(...).

  When testing, code that causes React state updates should be wrapped into act(...):

  act(() => {
    /* fire events that update state */
  });
  /* assert on the output */

  This ensures that you're testing the behavior the user would see in the browser. Learn more at https://fb.me/react-wrap-tests-with-act
      in UsernameForm

What the!? So what's going on here? Well, you might not have noticed, but our test didn't test our component's happy-path fully and left one important aspect vulnerable to regressions. Let's go back to the class version and comment out an important line of code:

class UsernameFormClass extends React.Component {
  state = {status: 'idle', error: null}
  handleSubmit = async event => {
    event.preventDefault()
    const newUsername = event.target.elements.username.value
    this.setState({status: 'pending'})
    try {
      await this.props.updateUsername(newUsername)
      // this.setState({status: 'fulfilled'})
    } catch (e) {
      this.setState({status: 'rejected', error: e})
    }
  }
  render() {
    const {error, status} = this.state

    return (
      <form onSubmit={this.handleSubmit}>
        <label htmlFor="username">Username</label>
        <input id="username" />
        <button type="submit">Submit</button>
        <span>{status === 'pending' ? 'Saving...' : null}</span>
        <span>{status === 'rejected' ? error.message : null}</span>
      </form>
    )
  }
}

Guess what! Our tests still pass! But, the Saving... loading indicator next to our submit button never goes away:

Oh no! It sure would've been nice if we'd had some warning that we weren't testing all that our component was doing so we could've written a good test from the start. Wait... Is that what act was yelling at us about? YES IT WAS!

So the act warning from React is there to tell us that something happened to our component when we weren't expecting anything to happen. So you're supposed to wrap every interaction you make with your component in act to let React know that we expect our component to perform some updates and when you don't do that and there are updates, React will warn us that unexpected updates happened. This helps us avoid bugs like the one described above.

Luckily for you and me, React automatically handles this for any of your code that's running within the React callstack (like click events where React calls into your event handler code which updates the component), but it cannot handle this for any code running outside of it's own callstack (like asynchronous code that runs as a result of a resolved promise you are managing or if you're using jest fake timers). With those kinds of situations you typically need to wrap that in act(...) or async act(...) yourself. BUT, React Testing Library has async utilities that are wrapped in act automatically!

How to fix the act(...) warning

So, let's first thank the React team for letting us know that we didn't test everything that's happening in our component (thanks React team! 🙏) and then let's add an assertion to our test to cover the use case we forgot the first time around:

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))
})

Great, so now we're waiting for the saving text to be removed and that ensures that the saving text appears in the first place and is removed when saving is complete.

You'll notice that this test passes whether we're using class components or function components (+1 point for avoiding Testing Implementation Details), but without that extra line we only get the warning with function components not class components. The reason for this is because the React team couldn't reasonably add this warning for class components without creating a TON of new warnings for people and their existing tests. So while the warning would probably help people find and fix bugs like this, the decision was made to only apply it to hooks so people would get the warning as they develop and test new components (+1 point for using function components over class components).

You can read more about React's act utility here, but again, you shouldn't have to use it very often. It's built-into React Testing Library. There are very few times you should have to use it directly if you're using React Testing Library's async utilities.

If you're still experiencing the act warning, then the most likely reason is something is happening after your test completes for which you should be waiting (like in our earlier examples).

An Alternative: waiting for the mocked promise

There's one alternative I want to show you that isn't quite as good for this use case, but could be useful, especially if there's no visual indication of the async task completing.

Because our code waits for the updateUsername promise to resolve before continuing, we could return a promise from our fake version and use an async act to await that promise resolution:

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
  })
})

Note that we're manually calling act here and you can get that from react-dom/test-utils or React Testing Library re-exports it so you can grab it from the import you already have. You're welcome.

This isn't preferable because it's still not going to catch the bug we demonstrated earlier by commenting out that setState call, but it does make the warning go away properly. I'd just recommend making additional assertions about the way things look after the async action has completed.

Other use cases for manually calling act(...)

If you're using all the React Testing Library async utilities and are waiting for your component to settle before finishing your test and you're still getting act warnings, then you may need to use act manually. Here are a few examples:

1. When using jest.useFakeTimers()

Let's say you have a component that's checking against an API on an interval:

function OrderStatus({orderId}) {
  const [{status, data, error}, setState] = React.useReducer(
    (s, a) => ({...s, ...a}),
    {status: 'idle', data: null, error: null},
  )

  React.useEffect(() => {
    let current = true
    function tick() {
      setState({status: 'pending'})
      checkStatus(orderId).then(
        d => {
          if (current) setState({status: 'fulfilled', data: d})
        },
        e => {
          if (current) setState({status: 'rejected', error: e})
        },
      )
    }
    const id = setInterval(tick, 1000)
    return () => {
      current = false
      clearInterval(id)
    }
  }, [orderId])

  return (
    <div>
      Order Status:{' '}
      <span>
        {status === 'idle' || status === 'pending'
          ? '...'
          : status === 'error'
          ? error.message
          : status === 'fulfilled'
          ? data.orderStatus
          : null}
      </span>
    </div>
  )
}

So we've handled all the edge cases and cleaned up after ourselves nicely, but when we write our test for it, we're going to get the act warning again:

import * as React from 'react'
import {render, screen} from '@testing-library/react'
import {checkStatus} from '../api'

jest.mock('../api')

beforeAll(() => {
  // we're using fake timers because we don't want to
  // wait a full second for this test to run.
  jest.useFakeTimers()
})

afterAll(() => {
  jest.useRealTimers()
})

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
  jest.advanceTimersByTime(1000)

  expect(await screen.findByText(orderStatus)).toBeInTheDocument()

  expect(checkStatus).toHaveBeenCalledWith(orderId)
  expect(checkStatus).toHaveBeenCalledTimes(1)
})

The act warning here is happening because of this line:

// ...
let current = true
function tick() {
  setState({status: 'pending'})
  checkStatus(orderId).then(
    d => {
// ...

The tick function is happening outside of React's callstack, so it's unsure whether this interaction with the component is properly tested. React Testing Library does not have a utility for jest fake timers and so we need to wrap the timer advancement in act ourselves, like this:

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)
})

And now the React act(...) warning goes away!

Notice, that we can pull React DOM's act testing utility directly from @testing-library/react because it's simply re-exported by @testing-library/react (cool right!?).

2. When testing custom hooks

You'll get an act(...) warning with custom hooks when you call functions that are returned from your custom hook which result in state updates. Here's a simple example of that:

import * as React from 'react'

function useCount() {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)
  const decrement = () => setCount(c => c - 1)
  return {count, increment, decrement}
}

export default useCount

Here, we'll use @testing-library/react to validate our hook works:

import * as React from 'react'
import {renderHook} 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)

  result.current.increment()
  expect(result.current.count).toBe(1)

  result.current.decrement()
  expect(result.current.count).toBe(0)
})

When we call the increment and decrement functions from our hook, that triggers a state update and because we're not in the React callstack we're going to get an act(...) warning. So, let's wrap that 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)
})

You'll notice that we're pulling act from @testing-library/react and that's because it simply re-exports the act function from react-test-renderer so you don't need to add an additional export. Nice right!?

3. When using useImperativeHandle

You may not have even heard of this hook, and if you have you may not have run into this problem. That's because you only run into this if you're calling methods directly on a component which do internal state updates and you're outside of React's callstack. Let's read through an example of act warnings popping up when testing components that use this hook.

So here's a pretty contrived example, but you should be double-thinking your decisions any time you use this hook anyway:

function ImperativeCounter(props, ref) {
  const [count, setCount] = React.useState(0)
  React.useImperativeHandle(ref, () => ({
    increment: () => setCount(c => c + 1),
    decrement: () => setCount(c => c - 1),
  }))
  return <div>The count is: {count}</div>
}
ImperativeCounter = React.forwardRef(ImperativeCounter)

Here's how you might test this:

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()

  counterRef.current.increment()
  expect(screen.getByText('The count is: 1')).toBeInTheDocument()

  counterRef.current.decrement()
  expect(screen.getByText('The count is: 0')).toBeInTheDocument()
})

You'll get an act warning on those increment and decrement calls because they're happening outside the React callstack. So, let's wrap them in act:

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()
})

Conclusion

Hopefully you have a better understanding (and appreciation) for React's (sometimes annoying but always right) act(...) testing utility and the next time you get a warning you're able to identify the source of the problem and fix it more quickly.

Again, most of the time you shouldn't actually have to use act directly (thanks to React Testing Library's async utilities), but when you do, you'll know why it's needed and fixes the problem.

Good luck!

Epic React

Get Really Good at React

Illustration of a Rocket

Testing JavaScript

Ship Apps with Confidence

Illustration of a trophy
Kent C. Dodds
Written by Kent C. Dodds

Kent C. Dodds is a JavaScript software engineer and teacher. Kent's taught hundreds of thousands of people how to make the world a better place with quality software development tools and practices. He lives with his wife and four kids in Utah.

Learn more about Kent

If you found this article helpful.

You will love these ones as well.