If you are using React, you may have noticed that the useEffect hook runs multiple times on mount, even when it has an empty dependency array. This can be confusing, especially if you are logging values or making API calls inside useEffect. In this blog, we will explore why this happens across different React versions and how to handle it effectively.
Let's consider a simple React counter component:
import { useState, useEffect } from "react";
const Counter = () => {
const [count, setCount] = useState(5);
useEffect(() => {
console.log("rendered", count);
}, [count]);
return (
<div>
<h1>Counter</h1>
<div>{count}</div>
<button onClick={() => setCount(count + 1)}>click to increase</button>
</div>
);
};
export default Counter;
Expected Behavior
When the component mounts, we expect useEffect to run once and log rendered 5 in the console.
Actual Behavior
However, depending on the React version and environment (development vs. production), useEffect might run multiple times on mount.
1. Disable Strict Mode (Not Recommended)
You can disable Strict Mode by modifying main.jsx or index.js:
import React from "react";
import ReactDOM from "react-dom/client";
import Counter from "./Counter";
ReactDOM.createRoot(document.getElementById("root")).render(
// Remove <React.StrictMode>
<Counter />
);
However, this is not recommended because Strict Mode helps catch potential bugs.
2. Use a Ref to Track First Render
To prevent useEffect from running on the first mount, you can use a useRef to track the first render:
import { useState, useEffect, useRef } from "react";
const Counter = () => {
const [count, setCount] = useState(5);
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
return;
}
console.log("rendered", count);
}, [count]);
return (
<div>
<h1>Counter</h1>
<div>{count}</div>
<button onClick={() => setCount(count + 1)}>click to increase</button>
</div>
);
};
export default Counter;
This ensures useEffect does not run on the first render but runs on subsequent updates.
3. Use a Cleanup Function (For API Calls)
If you are making API requests inside useEffect, you can use an AbortController to cancel requests when the component unmounts:
import { useState, useEffect } from "react";
const FetchData = () => {
const [data, setData] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch("https://api.example.com/data", { signal: controller.signal })
.then((res) => res.json())
.then((data) => setData(data))
.catch((err) => console.error("Fetch error:", err));
return () => controller.abort();
}, []);
return <div>{data ? JSON.stringify(data) : "Loading..."}</div>;
};
export default FetchData;
This prevents unnecessary API calls due to Strict Mode re renders.
In React 16 and 17, useEffect runs only once on mount with an empty dependency array.
In React 18, useEffect runs twice on mount due to Strict Mode.
React 19 is expected to retain Strict Mode behavior, so double execution may still occur.
You can handle this behavior by:
Keeping Strict Mode enabled for debugging.
Using useRef to prevent useEffect from running on the first render.
Using an AbortController for API requests inside useEffect.
Ready to transform your business with our technology solutions? Contact Us today to Leverage Our ReactJS Expertise.