Search code examples
reactjsreact-hooksfetchuse-effectuse-reducer

useEffect infinite loop occurs only while testing, not otherwise - despite using useReducer


I'm trying to test a useFetch custom hook. This is the hook:

import React from 'react';

function fetchReducer(state, action) {
  if (action.type === `fetch`) {
    return {
      ...state,
      loading: true,
    };
  } else if (action.type === `success`) {
    return {
      data: action.data,
      error: null,
      loading: false,
    };
  } else if (action.type === `error`) {
    return {
      ...state,
      error: action.error,
      loading: false,
    };
  } else {
    throw new Error(
      `Hello! This function doesn't support the action you're trying to do.`
    );
  }
}

export default function useFetch(url, options) {
  const [state, dispatch] = React.useReducer(fetchReducer, {
    data: null,
    error: null,
    loading: true,
  });

  React.useEffect(() => {
    dispatch({ type: 'fetch' });

    fetch(url, options)
      .then((response) => response.json())
      .then((data) => dispatch({ type: 'success', data }))
      .catch((error) => {
        dispatch({ type: 'error', error });
      });
  }, [url, options]);

  return {
    loading: state.loading,
    data: state.data,
    error: state.error,
  };
}

This is the test

import useFetch from "./useFetch";
import { renderHook } from "@testing-library/react-hooks";
import { server, rest } from "../mocks/server";

function getAPIbegin() {
  return renderHook(() =>
    useFetch(
      "http://fe-interview-api-dev.ap-southeast-2.elasticbeanstalk.com/api/begin",
      { method: "GET" },
      1
    )
  );
}

test("fetch should return the right data", async () => {
  const { result, waitForNextUpdate } = getAPIbegin();

  expect(result.current.loading).toBe(true);
  await waitForNextUpdate();
  expect(result.current.loading).toBe(false);
  const response = result.current.data.question;
  expect(response.answers[2]).toBe("i think so");
});

// Overwrite mock with failure case

test("shows server error if the request fails", async () => {
  server.use(
    rest.get(
      "http://fe-interview-api-dev.ap-southeast-2.elasticbeanstalk.com/api/begin",
      async (req, res, ctx) => {
        return res(ctx.status(500));
      }
    )
  );

  const { result, waitForNextUpdate } = getAPIbegin();

  expect(result.current.loading).toBe(true);
  expect(result.current.error).toBe(null);
  expect(result.current.data).toBe(null);
  await waitForNextUpdate();
  console.log(result.current);
  expect(result.current.loading).toBe(false);
  expect(result.current.error).not.toBe(null);
  expect(result.current.data).toBe(null);
});
  • I keep getting an error only when running the test: "Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render."

  • The error is coming from TestHook: node_modules/@testing-library/react-hooks/lib/index.js:21:23) at Suspense

I can't figure out how to fix this. URL and options have to be in the dependency array, and running the useEffect doesn't change them, so I don't get why it's causing this loop. When I took them out of the array, the test worked, but I need the effect to run again when those things change.

Any ideas?


Solution

  • Try this.

    function getAPIbegin(url, options) {
      return renderHook(() =>
        useFetch(url, options)
      );
    }
    
    test("fetch should return the right data", async () => {
      const url = "http://fe-interview-api-dev.ap-southeast-2.elasticbeanstalk.com/api/begin";
      const options = { method: "GET" };
      const { result, waitForNextUpdate } = getAPIbegin(url, options);
    
      expect(result.current.loading).toBe(true);
      await waitForNextUpdate();
      expect(result.current.loading).toBe(false);
      const response = result.current.data.question;
      expect(response.answers[2]).toBe("i think so");
    });
    

    I haven't used react-hooks-testing-library, but my guess is that whenever React is rendered, the callback send to RenderHook will be called repeatedly, causing different options to be passed in each time.