react-testing-library: useEffect with async function call causes `act(...)` warning

When rendering and testing a component with a useEffect that calls an async function that modifies the component’s state, then an act() console error will be output when running tests.

  • @testing-library/react version: 10.0.4
  • jest version: 26.0.1
  • DOM Environment: jsdom version: 16.2.2

Relevant code or config:

const MyComponent = () => {
    const [loaded, setLoaded] = useState(false)

    useEffect(() => {
        async function load() {
            await loadFromApi()
            setLoaded(true)
        }
        load()
    }, [])

    return loaded ? <div>loaded</div> : <div>loading...</div>
}

// timeout=0 to simulate mocked api responses
const loadFromApi = () => new Promise(resolve => setTimeout(resolve, 0))

What you did:

Just render this component in a jest test.

test('act() console error', () => {
    const sut = render(<MyComponent />)
    expect(sut).toBeDefined()
})

What happened:

This error is output in my project when running tests:

  console.error
    Warning: An update to null inside a test was not wrapped in act(...).
    
    When testing, code that causes React state updates should be wrapped into act(...):

Reproduction:

See console output: https://codesandbox.io/s/react-testing-library-demo-sk2so

Problem description:

Rendering should wait until everything has been resolved.

Suggested solution:

Since async functions inside useEffect are quite common, rendering a component containing such a hook should wait for the component updates to have finished (with a small timeout).

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 36
  • Comments: 19 (7 by maintainers)

Commits related to this issue

Most upvoted comments

Hi @fabb,

There’s no way for React Testing Library to know that you’ve got async stuff happening in the background, and you wouldn’t want that anyway because you probably want to assert the “loading” state anyway.

This is why React Testing Library gives you async utils which you can use to wait for the UI to update asynchronously.

Here’s how you’d fix that codesandbox: https://codesandbox.io/s/react-testing-library-demo-dmklb?file=/src/__tests__/hello.js:244-306

The cool thing about this is the async utils provided by React Testing Library are automatically wrapped in act so you shouldn’t ever have to worry about using act manually. Learn more here: https://kcd.im/act-warning

Good luck 😃

You could bring this question to my https://kcd.im/office-hours

Can’t answer here/now

@queeniema, I don’t typically write jest tests that hit the real API and take a long time to run. I mock things like that so they’re fast and then test those actual calls with an E2E testing tool like Cypress.

If you have any follow-up questions, please take them to https://testing-library.com/discord

Yeah, I suggest opening a new issue with a repro. I have a feeling there’s not much we can do about this though. You’ll probably have to have an explicit unmount and wait for the cleanup to finish.

@kentcdodds In the codesandbox you provided, when I modify the setTimeout to a longer period (10 sec), the test fails (see here).

const loadFromApi = () => new Promise((resolve) => setTimeout(resolve, 10000));

It doesn’t seem to be waiting for the promise to resolve (total test time is ~ 4.5 sec). Any suggestions on how to get this to work for long-running tests?

@queeniema The reason it’s failing is because screen.findByText(/loaded/i) uses waitFor and the default timeout is 1000ms. You’ll need to change it to a higher number for the findBy* query to wait longer. You can see the API here. I tried changing that in the codesandbox but it looks like it doesn’t change the default test timeout.

  1. Turn on fake timers
        beforeEach(() => {
            jest.useFakeTimers();
        });
  1. Mock React.useState with a helper method - defer based on setTimeout()
        function deferUseState(): () => void {
            const { useState } = React;
            React.useState = (<T extends unknown>(defaultVal: T): [T, Dispatch<T>] => {
                const [value, updateValue] = useState<T>(defaultVal);

                const wrappedUpdate = (newValue: T) => {
                    setTimeout(() => { updateValue(newValue); }, 1);
                };

                return [value, wrappedUpdate];
            }) as unknown as typeof React.useState;

            return () => { React.useState = useState; }; // restore function
        }
  1. Update test to use helper method.
const restoreUseState = deferUseState();
// do render as normal
restoreUseState();

// now call act/runTimers as required.
act(() => {
    jest.runAllTimers();
});

Hi guys, I am having the following test wrapped in an async function:

    const renderAPI = await render(<PrivacyPolicy />)

This throws warning:

 console.error
    Warning: An update to PrivacyPolicy 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 PrivacyPolicy

       8 | 
       9 |   useEffect(() => {
    > 10 |     storage.readObject('has_accepted_cookie').then((value) => setShouldShowModal(!value))
         |                                                               ^
      11 |   }, [])

None of the advice here helped, any clue?

Hi @fabb,

There’s no way for React Testing Library to know that you’ve got async stuff happening in the background, and you wouldn’t want that anyway because you probably want to assert the “loading” state anyway.

This is why React Testing Library gives you async utils which you can use to wait for the UI to update asynchronously.

Here’s how you’d fix that codesandbox: https://codesandbox.io/s/react-testing-library-demo-dmklb?file=/src/__tests__/hello.js:244-306

The cool thing about this is the async utils provided by React Testing Library are automatically wrapped in act so you shouldn’t ever have to worry about using act manually. Learn more here: https://kcd.im/act-warning

Good luck 😃

OMG I was stuck with this warning for 24 hours. Thank you for your help, this worked.

BTW thanks for the tip of updating jest to include a backtrace in the error log output, that helps a lot.