skip to Main Content

I was surprised that the following code leads to infinite rerenders using NextJS 13.4. As far as I can tell, I am just using a simple React Hook and the standard state + useEffect to get the data and update the state on the initial component rendering. The rerendering definitely has to do with the hook because if I replace the line const { getData } = useBackend(); with const { getData } = useMemo(useBackend, []); the infinite rerendering loop is fixed.

Am I missing a detail that is causing the rerendering? Is this behavior specific to NextJS? Advice is appreciated!

import { useEffect, useState } from "react";

type DataT = {
    id: string;
};

const useBackend = () => {
    const getData = (): DataT[] => {
        const data = [{ id: "foo" }];
        return data;
    };
    return { getData };
};

const App = () => {
    const [data, setData] = useState<undefined | DataT[]>(undefined);
    const { getData } = useBackend();

    useEffect(() => {
        const data = getData();
        setData(data);
        console.log(data);
    }, [getData]);

    return (
        <div>
            {data?.map((data) => (
                <div key={data.id}>{data.id}</div>
            ))}
        </div>
    );
};

export default App;

EDIT

Trying to make the example even simpler, I noticed that it has to do with the fact that the data is an array. The following code does not lead to infinite rerenders.

import { useEffect, useState } from "react";

const useBackend = () => {
    const getData = (): string => {
        return "foo";
    };
    return { getData };
};

const App = () => {
    const [data, setData] = useState<string>("");
    const { getData } = useBackend();

    useEffect(() => {
        const data = getData();
        setData(data);
        console.log(data);
    }, [getData]);

    return <div>{data}</div>;
};

export default App;

But if I change the string to string[] we’re back to infinite rerender.

import { useEffect, useState } from "react";

const useBackend = () => {
    const getData = (): string[] => {
        return ["foo", "bar"];
    };
    return { getData };
};

const App = () => {
    const [data, setData] = useState<string[]>([]);
    const { getData } = useBackend();

    useEffect(() => {
        const data = getData();
        setData(data);
        console.log(data);
    }, [getData]);

    return <div>{data}</div>;
};

export default App;

2

Answers


  1. In your current setup, getData is a dependency of the useEffect hook that calls setData. So you’re code fires like:

    useEffect -> fires setData -> fires getData -> fires setData -> getData …

    You can remove the dependency and stop the problem. for now.

    import { FC, useEffect, useState } from "react";
    
    type DataT = { id: string; };
    
    const useBackend = () => {
      const getData = () => [{ id: "foo" }];
      return { getData };
    };
    
    export const App: FC = () => {
      const [data, setData] = useState<DataT[]>([]);
      const { getData } = useBackend();
    
      useEffect(() => {
          const data = await getData();
          setData(data);
          console.log(data);
        }
      }, []);
    
      return (
        <div>
          {data.map((data) => (
            <div key={data.id}>{data.id}</div>
          ))}
        </div>
      );
    };
    
    

    React 18 is testing conditional mounting and reusable state.

    If strict mode is enabled, which is the default setting in Next.js 13+, React automatically performs a double rendering of components in dev mode. This double rendering is a deliberate approach to help developers adapt to and prepare for the integration of this new features.

    In short we need to build our components and pages to be re-rendered multiple times and can no longer depend on things like an empty dependency array.

    For this type of feature it’s best to check if it’s mounted. Since data fetching is typically async, I made it async too.

    import { FC, useEffect, useRef, useState } from "react";
    
    type DataT = { id: string; };
    
    const useBackend = () => {
      const [data, setData] = useState<DataT[]>();
      const isMounted = useRef(false);
    
      const fetchData = async () => {
        try {
         const data = await getData();
         setData(data);
        catch (e) {
         console.log(e);
        }
      };
    
      useEffect(() => {
        if (!isMounted.current) {
          fetchData();
          isMounted.current = true;
        }
      }, [fetchData]);
    
      return { data };
    };
    
    export const App: FC = () => {
      const [data, setData] = useState<DataT[]>([]);
      const { data } = useBackend();
      
      if(loading){
       return <>loading</>
      }
      return (
        <div>
          {data?.map((data) => (
            <div key={data.id}>{data.id}</div>
          ))}
        </div>
      );
    };
    
    
    Login or Signup to reply.
  2. The problem with the piece of code is the dependency of the useEffect being the function getData. Explaining further, consider the following:

    1. The component renders for the first time, during which it calls the useBackend hook and gets an instance of getData (lets say A), and then the effect runs.
    2. The effect calls setData and causes a re-render of the component, which in turn calls the useBackend hook again, which returns an instance of getData function (lets say B), and then its time to check if the effect should run again depending on if the dependencies changed.

    What happens is useBackend returns a new instance of getData every time it is called i.e. instances A and B are different which is why the dependency to the effect change on every re-render, thereby calling setState, which causes a re-render that runs the effect and so on.

    The solution to this problem is simply using a useCallback for the getData function that makes sure we get the same instance for getData every time the hook is called, so something like this:

    const useBackend = () => {
      const getData = useCallback((): DataT[] => {
        const data = [{ id: 'foo' }];
        return data;
      }, []);
      return { getData };
    };
    

    The above piece of code should do the charm. Check it out in this sandbox of you would like to: https://stackblitz.com/edit/react-ts-uumnu4?file=index.tsx

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search