6

I have the following hook :


const useBar = () => {
  const [myFoo, setFoo] = useState(0);
  const [myBar, setBar] = useState(0);
  useEffect(() => {
    setFoo(myFoo + 1);
    console.log("setting foo (1)", myFoo, myBar);
  }, [setFoo, myFoo, myBar]);
  useEffect(() => {
    setBar(myBar + 1);
    console.log("setting bar (2)", myFoo, myBar);
  }, [setBar, myBar, myFoo]);
};

When working with a component I have the expected behaviour of infinite loop :

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

const Bar = () => {
  useBar();
  return <div>Bar</div>;
};

function App() {
  return (
    <Bar />
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

From console.log :

setting foo (1) 1 1
setting bar (2) 1 1
setting foo (1) 2 2
setting bar (2) 2 2
setting foo (1) 3 3
setting bar (2) 3 3
...

However testing this with @testing-library/react-hooks gives only the first loop output :

describe("Tests", () => {
  test("useBar", async () => {
    const { rerender } = renderHook(() => {
      return useBar();
    });

    // await waitForNextUpdate();
    // tried the above doesn't work

    // rerender(); 
    // with rerender it loops through the next "cycle"
  });

});

Was wondering how to test properly hooks, without calling rerender and mimic the same behaviour as React - to re-render the component on state changes.

drinchev
  • 19,201
  • 4
  • 67
  • 93
  • 1
    Can you provide a working example of what you're trying to achieve? With your current example you will end up with an endless "loop" of updates which will slow down the UI. I achieved a variation of what you're doing by using the `act` helper: https://codesandbox.io/s/ecstatic-ardinghelli-hsctu (keep in mind it's a contrived example). – Simon Ingeson Jul 23 '19 at 20:31
  • I'm not familiar with `@testing-library/react-hooks` but, as Simon said, have you tried using the build it [act](https://reactjs.org/docs/hooks-faq.html#how-to-test-components-that-use-hooks) from `react-dom/test-utils`? – bamse Jul 26 '19 at 06:52
  • 1
    I don't understand why you use two useEffect and next, why you want to put in array dependency the setBar function? – xargr Jul 26 '19 at 07:15
  • 1
    @SimonIngeson there are many practical hooks that include more than one `useEffect`. I've put the infinite loop there, since sometimes your hook might go to infinite loop because of this effect and since you probably want to test that this doesn't happen. On the other hand hooks with 2+ `useEffect` are not executed properly when tested with `react-hooks-testing-library`, so that's the reason of my question. If you find the example illogical, please try to think : _"How to test that a hook is not running an infinite loop, due to interdependent `useEffect`"_ – drinchev Jul 26 '19 at 15:00
  • @xargr There are many hooks that use two `useEffect` ( check https://usehooks.com/useEventListener/ as an example ). There are lots of places where you need to have async action and the only way to achieve that would be inside a `useEffect`. On the other hand `setBar` is a function used in the hook and the official `eslint` react hooks plugin complains if not added. – drinchev Jul 26 '19 at 15:06
  • 1
    @drinchev yeah I know but in dependency array you can put only variables, object, array to make shallow checking if needs to re-run the render or not. You can't make shallow checking with function – xargr Jul 26 '19 at 15:20
  • Thanks for clarifying. Then the only way I can think of testing that is to return `myFoo` or `myBar` from the `useBar` hook, call `rerender` and see if they have incremented as expected. But that doesn't really help you as what you really want to know is if you ended up with an infinite loop. Another way I had some success identifying something was wrong was using [`waitForElement`](https://testing-library.com/docs/dom-testing-library/api-async#waitforelement), but it doesn't necessarily fail the test and requires you to test the integration of the hook instead of testing the hook directly. – Simon Ingeson Jul 26 '19 at 16:32

1 Answers1

6

I guess you want to test a hook that uses useEffect and report an error when an infinite loop occurs. But in fact we can't test an infinite loop, we can only set a limit on the number of times. When the loop exceeds this limit, we think that it will continue to loop indefinitely. As for whether it is actually infinite loop, it is not easy prove.

However, it is not easy to test a repetition of useEffect. Although using waitForNextUpdate can get a timeout error, this error can also be caused by a long time asynchronous operation, and as a unit test, you need fast feedback. You can't wait for the timeout every time. It's a waste of time.

Thanks for this issues, I found the direction.Provide a way to trigger useEffect from tests

When I replace useEffect with useLayoutEffect, I can get a Maximum update depth exceeded error quickly and accurately. This only needs to replace useEffect with useLayoutEffect in the required test. The test will fail when the loop is infinite.

Edit Test useEffect infinite loop

import React from 'react'
import { renderHook } from '@testing-library/react-hooks'

import useBar from './useBar'

describe('Replace `useEffect` with `useLayoutEffect` to check for an infinite loop', () => {
  let useEffect = React.useEffect
  beforeAll(() => {
    React.useEffect = React.useLayoutEffect
  })
  afterAll(() => {
    React.useEffect = useEffect
  })
  it('useBar', async () => {
    const { result } = renderHook(() => useBar())
  })
})
XYShaoKang
  • 981
  • 4
  • 5