React: act cannot detect secondary updates

Created on 6 Feb 2019  路  14Comments  路  Source: facebook/react

Do you want to request a feature or report a bug?

bug

What is the current behavior?

If a component performs a second (non user triggered) update, act cannot detect it and warns about the update.

For example, a button is clicked and updates its text. After a second, the button resets and its text reverts to its original state.

https://codesandbox.io/s/6xkyl37x7k?previewwindow=tests

(The reproduction is a bit contrived, but demonstrates the issue.)

What is the expected behavior?

The test runs without warning about being wrapped in act.

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?

React & React DOM @ 16.8.0

Question

Most helpful comment

We are aware of these problems and are working on a solution, sorry about that.

All 14 comments

heading for dinner, but tldr - jest.useFakeTimers()/jest.runAllTimers() is great for this. https://jestjs.io/docs/en/timer-mocks I'll come back and try to get your test to pass.

Since I'm not sure if the issue marked duplicate will be re-opened, I'll post this here. We have a library that handles async actions and updates state. A very contrived example of something we need to test for is:

  <input value={count} />
  <button onClick={async () => await someFuncThatChangesCount() } />

How do you test this with act? Currently with react-test-renderer we can do:

  expect(val.props.value).toBe(0);
  await button.props.onClick();
  expect(val.props.value).toBe(1);

This will fail:

  expect(val.props.value).toBe(0);
  act(() => {
    button.props.onClick();
  });
  expect(val.props.value).toBe(1);

And this will continue to warn but because act is receiving a promise

  expect(val.props.value).toBe(0);
  act(async () => {
    await button.props.onClick();
  });
  expect(val.props.value).toBe(1);

I use a similar pattern to have finer grain control when testing async behaviors and am running into the same problem

import React, {useState, useEffect} from 'react';

const ThingsList = ({fetch}) => {
  const [things, setThings] = useState([]);
  const [isLoading, setIsLoading] = useState(true);

  const fetchThings = async () => {
    const things = await fetch('example.com/api/things');
    setThings(things);
    setIsLoading(false);
  };

  useEffect(() => {
    fetchThings()
  }, []);

  if (isLoading) {
    return <div>Loading things</div>;
  }

  if (things.length === 0) {
    return <div>No things</div>;
  }

  return (
    <ul>
      {things.map(({name}, i) => <li key={i}>{name}</li>)}
    </ul>
  );
};

test:

describe('while loading things', () => {
  it('shows loading state', async () => {
    let resolver;

    const promise = new Promise(resolve => {
      resolver = resolve
    });

    const resolveFetch = (val) => {
      resolver(val);
      return promise
    };

    const fetch = () => promise;

    const rendered = render(
      <Component {...{fetch}}/>
    );

    expect(rendered.container.textContent).toContain('Loading things');

    await resolveFetch([
      {name: 'thing 1'},
      {name: 'thing 2'},
    ]);

    expect(rendered.container.textContent).toContain('thing 1');
    expect(rendered.container.textContent).toContain('thing 2');
  });
});

the test only passes when await is at the top level. And because the action is async and act appears not to be, I'm not sure if there's anything I can wrap that allows it to recognize the update

I hoped this would work, but it did not:

const resolveFetch = (val) => {
  act(() => {
    resolver(val);
  });
  return promise
};

@atomanyih I'm experiencing a similar issue with a component similar to yours above. I'm making an asynchronous call within useEffect and setting state within the async callback. My test passes, however, I am seeing the warning in the logs about wrapping my test in act()

We are aware of these problems and are working on a solution, sorry about that.

fyi - there are some suggestions here https://github.com/facebook/react/issues/14769#issuecomment-462528230

This would also be helped by Jest learning how to exhaust the promise queue (facebook/jest#2157), right? That would basically be the approach outlined in the linked comment above, but handled for you by Jest and with the rough edges already sanded down.

we just released an alpha that includes the new async version of act().
@pshrmn - here's a modified version of your codesandbox that passes tests with the new alpha https://codesandbox.io/s/wq83xq6wj5

@nbrady-techempower I don't have a codesandbox from you that I can remix, but I can guess that this would work for you -

await act(async () => {
  await resolveFetch([
      {name: 'thing 1'},
      {name: 'thing 2'},
    ]);
})
// ... assertions 

if this doesn't work, and if you have a git repo/codesandbox link, I'd be happy to look further.

@alexknipfer if you still have issues after trying the new api, feel free to make a new issue with a repro and I can have a look.

Will do @threepointone , thanks for all the updates!

@threepointone Everything looks good to me. Unless there is a reason to keep this issue open (e.g. waiting for a full release), I think that it can be closed.

It would also be nice to see this PR documenting ReactTestRenderer.act https://github.com/reactjs/reactjs.org/pull/1766 merged. I had to do a little digging to find that.

Yes, we'll do that after we incorporate the async parts as well.

@threepointone Reporting back, worked perfectly for my tests, thanks again for the updates! If I run into anything I'll be sure to report it with a repro.

16.9 got released, including async act, and updated documentation https://reactjs.org/blog/2019/08/08/react-v16.9.0.html Closing this, cheers.

Was this page helpful?
0 / 5 - 0 ratings