react: Provide a way to trigger useEffect from tests

Hello,

I tried testing components that use the cool new hooks API, but useEffect doesn’t seem to work with the test renderer.

Here’s a small failling Jest test:

import React, { useEffect } from "react";
import { create as render } from "react-test-renderer";

it("calls effect", () => {
  return new Promise(resolve => {
    render(<EffectfulComponent effect={resolve} />);
  });
});

function EffectfulComponent({ effect }) {
  useEffect(effect);

  return null;
}

And here’s a minimal reproducing repo: https://github.com/skidding/react-test-useeffect

Note that other use APIs seemed to work (eg. useContext).

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 28
  • Comments: 23 (8 by maintainers)

Commits related to this issue

Most upvoted comments

We should have something to trigger them.

I think you don’t need such a hassle for this. Jest would automock if you create a file <root>/__mocks__/react.js and in there you can just…

const React = require.actual('react')
module.exports = { ...React, useEffect: React.useLayoutEffect }

This is a great workaround as you don’t need to touch any code when this is somehow fixed, you will just remove the mock.

I wrote a longish doc on how to use .act() to flush effects and batch user and api interactions with React. https://github.com/threepointone/react-act-examples

You can manually run tree.update() and effect hooks will be ran. Example:

const Comp = () => {
  useEffect(() => console.log('effect'));
  return null;
}

const tree = renderer.create(<Comp />); // nothing logged
tree.update(); // console.log => 'effect'

TIP: until it will be fixed in library I fixed my tests by mocking useEffect to return useLayoutEffect just in tests.

I have own useEffect module where is just

// file app/hooks/useEffect.js
export {useEffect} from 'react';

then in my component I use

// file app/component/Component.js
import {useEffect} from '../hooks/useEffect';
...

and then in the component test I mock it like following

// file app/component/Component.test.js 
jest.mock('../hooks/useEffect', () => {
    return { useEffect: require('react').useLayoutEffect };
});
...

does it wait until at least one update has been scheduled?

No. it has to be at least within the next ‘tick’, ie a macrotask on the js task queue, as soon as act() exits (Or within the act() callback scope itself). I hope my explainer doc makes this clearer next week.

(Bonus: it will also recursively flush effects/updates until the queue is empty, so you don’t miss any hanging effects/updates)

It’s triggered for initial render after the browser is idle.

The only reason next updates trigger it is because we flush passive effects before committing the next render. This is important to avoid inconsistencies.

So as a result for every Nth render, you’ll see N-1th passive effects flushed.

We’ll likely offer a way to flush them on demand too.

@malbernaz I would NOT recommend wrapping every setState like you did. The Act warning is useful for surfacing bugs, and your hack is equivalent to silencing it.

@threepointone I am trying to understand how async act fixes it. Here is an example of component I want to test. It should listen to size changes of the screen:

const withScreenSize = Comp => ({ ...props}) => {
  const [ size, setSize ] = React.useState(Dimensions.get('window'))
  React.useEffect(() => {
    const sizeListener = () => {
      const { width, height } = Dimensions.get('window')
      setSize({ width, height })
    }
    Dimensions.addEventListener('change', sizeListener)
    return () => Dimensions.removeEventListener('change', sizeListener)
  }, [setSize])
return <Comp screenSize={size} {...props} />

To test it, I mock the Dimensions object (in React Native):

    // Mock Dimensions object to emulate a resize
    const listeners = []
    const oldDimensions = {...Dimensions}
    Dimensions.addEventListener = (type, listener) => type==='change' && listeners.push(listener)
    Dimensions.removeEventListener = (type, listener) => type==='change' && listeners.splice(listeners.indexOf(listener), 1)
    Dimensions.get = () => ({ width, height })

Now I am trying to test the following:

  • The resizeListener is correctly added on mount
  • The resizeListener is correctly removed on unmount
  • The new screen size is taken into account and the component’s state is updated

This is how I will emulate the resize:

function resizeScreen() {
      Dimensions.get = () => ({ width: 200, height: 200 })
      listeners.forEach(listener => listener())
}

While trying, I encountered so many weird errors that I don’t understand…

      const wrapper = TestRenderer.create(<Comp />);
      resizeScreen()
      wrapper.update()
      const updatedView = wrapper.root.findByType('View');
      // When using update with empty parameter, I get `Can't access .root on unmounted test renderer`. Though it is what appears in the code of @blainekasten above: tree.update()
      await act(async () => {
          const wrapper = TestRenderer.create(<Comp />);
          const view = wrapper.root.findByType('View');
          // Here I get: "Can't access .root on unmounted test renderer" directly on the line above.

In the end, this is how I got it to work:

      const wrapper = TestRenderer.create(<Comp />);
      const view = wrapper.root.findByType('View');
      await act(async () => {})
      resizeScreen()

Is this how I am supposed to do it?