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
In your current setup,
getData
is a dependency of theuseEffect
hook that callssetData
. 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.
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.
The problem with the piece of code is the dependency of the useEffect being the function getData. Explaining further, consider the following:
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:
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