Search code examples
javascriptreactjsuse-effectreact-testing-libraryact

How to resolve act() in useEffect?


I have a small problem with act() error in react-testing-library. In useEffect I try to call an function that is a promise. Promise returns some data and displays it, but even if the tests pass, the act error is still displayed. Component:

export function getUser() {
  return Promise.resolve({ name: "Json" });
}

const Foo = () => {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const loadUser = async () => {
      const userData = await getUser();
      setUser(userData);
    };
    loadUser();
  }, []);

  return (
    <div>
      <p>foo</p>
      {user ? <p>User is: {user.name}</p> : <p>nothing</p>}
    </div>
  );
};

Also I have my Foo component inside of App Component, like that:

import Foo from "./components/Foo";

function App() {
  return (
    <div>
      some value
      <Foo />
    </div>
  );
}

export default App;

TEST:

  test("should display userName", async () => {
    render(<Foo />);
    expect(screen.queryByText(/User is:/i)).toBeNull();
    expect(await screen.findByText(/User is: JSON/i)).toBeInTheDocument();
  });

Do You have any ideas to resolve it?

EDIT: here's an error message

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

Solution

  • Instead of act use waitFor to let the rendering resolve.

    import { render, waitFor } from '@testing-library/react-native';
    import { useEffect, useState } from 'react';
    import { Text } from 'react-native';
    
    describe('useState', () => {
      it('useState', () => {
        function MyComponent() {
          const [foo] = useState('foo');
          return <Text testID="asdf">{foo}</Text>;
        }
        const { getByTestId } = render(<MyComponent></MyComponent>)
        expect(getByTestId("asdf").props.children).toBe("foo");
      });
      it('useState called async', async () => {
        function MyComponent() {
          const [foo, setFoo] = useState('foo');
          useEffect(() => {
            (async () => {
              setFoo(await Promise.resolve('bar'))
            })()
          }, []);
          return <Text testID="asdf">{foo}</Text>;
        }
        const {getByTestId} = await waitFor(()=>render(<MyComponent></MyComponent>))
        expect(getByTestId("asdf").props.children).toBe("bar");
      });
    });
    

    In addition to the above answer for situations where the update does not occur immediately, (i.e. with a timeout) you can use await act(()=>Promise.resolve()); (note this generates a warning due improper typing but the await is needed for it to work.

    Here's an example with renderHook

      it("should return undefined", async () => {
        const { result } = renderHook(() => {
          const [loaded, loadedFonts] = useExpoFonts([]);
          return useReplaceWithNativeFontCallback(loaded, loadedFonts);
        }, {});
    
    
        const replaceWithNativeFont0 = result.current;
        expect(replaceWithNativeFont0({})).toBeUndefined();
        await act(() => Promise.resolve());
        const replaceWithNativeFont1 = result.current;
        expect(replaceWithNativeFont1({})).toBeUndefined();
      });
    

    And with the typical render

      it("should render fonts", async () => {
        function MyComponent() {
          const [loaded, loadedFonts] = useExpoFonts([]);
          const replaceWithNativeFont = useReplaceWithNativeFontCallback(
            loaded,
            loadedFonts
          );
          const style = replaceWithNativeFont({
            fontFamily: "something",
            fontWeight: "300",
          });
          return <View testID="blah" style={style} />;
        }
        const { unmount } = render(
          <ThemeProvider>
            <MyComponent />
          </ThemeProvider>
        );
    
    
        await act(() => Promise.resolve());
        expect(screen.getByTestId("blah").props.style).toStrictEqual({
          fontFamily: "something",
          fontWeight: "300",
        });
        unmount();
      });