Running React in strict mode with Next.js can lead to useEffect callbacks with zero dependencies to run twice in development. Here’s a way around that.
You may have noticed that with newer Next.js projects, a useEffect
hook runs twice. New Next.js projects now run in strict mode, which is a feature of React 18.
A breaking change that came with React 18 was that components are mounted, unmounted, and remounted in development mode. This causes an effect hook (with specified dependencies that don’t cause additional renders) to run twice.
Here’s an example of a component with an effect hook that runs twice:
import { useEffect, useState, useRef } from "react";
import styles from "../styles/Home.module.css";
export const SimpleEffect = () => {
const [timesRun, setTimesRun] = useState(0);
const counter = useRef < number > 0;
useEffect(() => {
counter.current += 1;
setTimesRun(counter.current);
}, []);
return (
<p className={styles.description}>
<code className={styles.code}>SimpleEffect</code> called:{" "}
<code className={styles.code}>{timesRun}</code>
</p>
);
};
This is a simple counter that gets incremented every time the effect hook runs. This will result in 2
being rendered to the screen when running in development mode.
I’m using a ref
object here because I can be sure that the value increments. Because state changes are set asynchronously, I can’t ensure I’ll get 2
consistently, even thought the hook is run twice.
To ensure this hook only runs once in development mode, we can add another reference object that tracks whether the callback to useEffect
has been called. Something like this:
import { useEffect, useState, useRef } from "react";
import styles from "../styles/Home.module.css";
export const EffectRunOnce = () => {
const [timesRun, setTimesRun] = useState(0);
const counter = useRef < number > 0;
const effectCalled = useRef < boolean > false;
useEffect(() => {
if (effectCalled.current) return;
counter.current += 1;
setTimesRun(counter.current);
effectCalled.current = true;
}, []);
return (
<p className={styles.description}>
<code className={styles.code}>EffectRunOnce</code> called:{" "}
<code className={styles.code}>{timesRun}</code>
</p>
);
};
Now we have an efectCalled
reference that starts as false
. It gets set to true
the first time the effect runs. Every subsequent time, we exit early from the effect callback.
Here's a demo in which you can see both components in action.