1

I want to test a hook that uses states in its implementation, but each time I run my tests I get this error:

Error: Uncaught [Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.]

The hook is complex and I couldn't find which part may cause an issue (plus it works perfectly fine under real conditions, when running the application with npm start).

I tried to write a dummy test to see if I could figure out anything, and I found that updating a state (which I do in my hook) triggers the error.

Basically, this fails:

import { renderHook } from "@testing-library/react";
import React from "react";

it("foo test", () => {
  const { result } = renderHook(() => {
    const [foo, setFoo] = React.useState("foo");
    setFoo("bar"); // This line is the culprit
    return foo;
  });

  expect(result.current).toEqual("bar");
});

But this works:

import { renderHook } from "@testing-library/react";
import React from "react";

it("foo test", () => {
  const { result } = renderHook(() => {
    const [foo, setFoo] = React.useState("foo");
    return foo;
  });

  expect(result.current).toEqual("foo");
});

What is the reason for this error and how can I fix it ?

Also, my package.json file:

{
  "name": "agora-front",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@emotion/react": "^11.10.0",
    "@emotion/styled": "^11.10.0",
    "@mui/icons-material": "^5.8.4",
    "@mui/material": "^5.9.3",
    "@testing-library/jest-dom": "^5.16.4",
    "@testing-library/react": "^13.3.0",
    "@types/react": "^18.0.15",
    "@types/react-dom": "^18.0.6",
    "i18next": "^21.8.16",
    "i18next-browser-languagedetector": "^6.1.4",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-i18next": "^11.18.3",
    "react-router-dom": "^6.3.0",
    "react-scripts": "5.0.1",
    "typescript": "^4.7.4",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "BROWSER=none react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --verbose",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "@babel/preset-typescript": "^7.18.6",
    "@types/jest": "^28.1.6",
    "jest": "^28.1.3",
    "prettier": "2.7.1",
    "react-test-renderer": "^18.2.0"
  }
}
KawaLo
  • 1,783
  • 3
  • 16
  • 34

1 Answers1

0

I got tricked by the test environment and forgot React basic good practices.

Every state update should be wrapped in a condition with useEffect, otherwise render will trigger endlessly.

I followed that practice in all my React components, thus explaining why it was working fine when building the application. But I wrote my tests a bit quickly.

For my quick example, this is the correct way to do it:

import { renderHook } from "@testing-library/react";
import React from "react";

it("foo test", () => {
  const { result } = renderHook(() => {
    const [foo, setFoo] = React.useState("foo");
    
    React.useEffect(() => {
      setFoo("bar"); // Now it works.
    }, [])

    return foo;
  });

  expect(result.current).toEqual("bar");
});
KawaLo
  • 1,783
  • 3
  • 16
  • 34