Search code examples
reactjstypescriptreact-hooksuse-effectuse-state

Reactjs Custom hook causing an infinite loop


can someone help explain why this code is causing an infinite loop and what is the best approach to fixing it please and thank you.
I am assuming its because of the useEffect within the App component which is causing a re-render which then the useState also causes a render therefore causing an infinite loop. I think I am not understanding how useEffect and useState works properly.

import "./styles.css";
import React, { useEffect, useState } from "react";

interface IObject {
  isLoading: boolean;
  isError: boolean;
  data: any[] | any;
}

function useHook1(): IObject {
  console.log("hook 1 too many re-renders?");
  return { isLoading: false, isError: false, data: [] };
}

function useHook2(): IObject {
  const result = { isLoading: false, isError: false, data: "testing" };
  console.log("hook 2 too many re-renders?");
  return result;
}

export default function App() {
  const { isLoading, isError, data } = useHook1();
  const testResult = useHook2();
  const [state, setState] = useState();

  useEffect(() => {
    console.log("inside useEffect within App")
    setState(testResult.data)
  }, [testResult])

  console.log("too many re-renders?");
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      <p>{testing}</p>
    </div>
  );
}


Solution

  • useHook2 returns a new object every time it runs. This means testResult is new/changed on every render, and your useEffect runs whenever testResult changes. So:

    1. Your effect updates state, which causes a re-render.

    2. On the rerender, useHook2 gets invoked and testResult is updated.

    3. testResult changed, so your effect runs again and you return to step 1, entering an infinite loop.


    The solution, if I understand what you're trying to do, is to do the state management in the custom hook:

    function useHook1() {
      const [isLoading, setIsLoading] = useState(false);
      const [isError, setIsError] = useState(false);
      const [data, setData] = useState([])
    
      return {
        isLoading,
        isError,
        data,
      };
    }
    

    You can simplify this a bit by collapsing the repeated useState usage into a single useReducer:

    function useHook1() {
      const [state, dispatch] = useReducer(
        (state, action) => ({...state, ...action}),
        { isLoading: false, isError: false, data: [] }
      );
    
      return { ...state };
    }
    

    If you need to trigger updates from outside the hook you can make that available too:

    function useHook1() {
      const [state, dispatch] = useReducer(
        (state, action) => ({...state, ...action}),
        { isLoading: false, isError: false, data: [] }
      );
    
      return { ...state, dispatch };
    }
    
    export default function App () {
      const [{ isLoading, isError, data }, setState] = useHook1();
      
      return isLoading ? <div>Loading</div> : (
        <div onClick={() => dispatch({ data: ["new data"]})}>...</div>
      )
    }