Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test hooks with two useEffect statements?

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.

like image 812
drinchev Avatar asked Jul 18 '19 11:07

drinchev


1 Answers

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())
  })
})
like image 191
XYShaoKang Avatar answered Oct 07 '22 21:10

XYShaoKang