How to test hooks with two useEffect statements?
Asked Answered
N

1

7

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.

Nervine answered 18/7, 2019 at 11:57 Comment(7)
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: codesandbox.io/s/ecstatic-ardinghelli-hsctu (keep in mind it's a contrived example).Brutal
I'm not familiar with @testing-library/react-hooks but, as Simon said, have you tried using the build it act from react-dom/test-utils?Penetrant
I don't understand why you use two useEffect and next, why you want to put in array dependency the setBar function?Zsa
@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"Nervine
@Zsa There are many hooks that use two useEffect ( check 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.Nervine
@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 functionZsa
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, but it doesn't necessarily fail the test and requires you to test the integration of the hook instead of testing the hook directly.Brutal
L
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())
  })
})
Liguria answered 28/7, 2019 at 6:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.