React 18 introduced intentional changes that caused useEffect to run twice in development when StrictMode is enabled. This behavior is designed to surface potential issues in your code. While frustrating for some, understanding the cause and applying reliable fixes can ensure your applications operate smoothly without compromising future-proofing.
Symptom: useEffect Runs Twice in Development
Developers upgrading to React 18 often notice that useEffect executes twice in development mode when using an empty dependency array ([]). This behavior is specific to StrictMode and does not affect production builds. For instance:
import React, { useEffect } from "react";
export default function App() {
useEffect(() => {
console.log("Effect ran");
}, []);
return <p>Hello, world!</p>;
}In React 18 with StrictMode enabled, the console outputs:
Effect ran
Effect ranThis can disrupt logic dependent on effects running once, such as data fetching.
Cause: StrictMode's Development-only Check
React 18 introduced a development-only feature in StrictMode where components are intentionally mounted, unmounted, and remounted. This check aims to uncover issues like unhandled side-effects or missing cleanup logic in components.
For example:
- On the first mount,
useEffectruns. - React immediately unmounts the component to trigger cleanup logic.
- The component is remounted, causing
useEffectto run a second time.
React's rationale includes:
- Preparing for future features like state preservation during unmounts.
- Providing developers with tools to improve code reliability.
Fix: Conditional Execution Based on useRef
To avoid unintended side-effects during this double execution, you can use a useRef to conditionally run useEffect logic. Here's how:
steps
Import the necessary hooks:
jsximport { useEffect, useRef } from "react";Initialize a
useRefto track if the effect has run:jsxconst hasEffectRun = useRef(false);Conditionally execute the effect code:
jsxuseEffect(() => { if (!hasEffectRun.current) { console.log("Effect runs once"); hasEffectRun.current = true; } }, []);
This ensures the effect logic runs only once during the StrictMode's double-render cycle.
Implementation Guide: Handling Dual Execution Variations
The solution can be adapted based on when you prefer the effect logic to run during the StrictMode checks.
comparison
Variation 1: Execute on the first render:
useEffect(() => {
if (!hasEffectRun.current) {
console.log("Run during the first effect execution");
hasEffectRun.current = true;
}
}, []);Variation 2: Execute on the second (stable) render:
useEffect(() => {
if (hasEffectRun.current) {
console.log("Run only on the stable (second) execution");
} else {
hasEffectRun.current = true;
}
}, []);Choose a variation based on your application's needs.
Takeaway: Adopt Best Practices without Disabling StrictMode
React 18's StrictMode changes highlight potential shortcomings in effect cleanup and side-effect handling. While it might seem tempting to disable StrictMode, doing so conceals real issues and diminishes forward compatibility.
Instead:
- Use recommended patterns like
useRefto mitigate double execution. - Embrace StrictMode as a tool for robust, future-ready applications.
- Avoid quick fixes that lead to long-term maintenance headaches.
FAQ
Why does useEffect run twice in React StrictMode?
useEffect runs twice in React 18 StrictMode during development due to an intentional remounting cycle. This tests cleanup functions and surfaces potential bugs.
Is it safe to disable StrictMode to fix the issue?
No, disabling StrictMode conceals real bugs and removes safety checks, reducing your application's reliability. Prefer using solutions like useRef.
How does useRef help mitigate useEffect double renders?
useRef provides a persistent value across renders. By tracking when useEffect has run, you can conditionally execute logic to prevent duplicate operations.
Official reference: React documentation.