If your useEffect runs twice, you are almost certainly running in development with React Strict Mode. Right after the first mount, React runs your effect's cleanup and then runs the effect again — once — to check that the cleanup really undoes the setup. Production builds run it once.
The fix is not to stop the second run. It is to write a cleanup so that setup → cleanup → setup behaves exactly like a single setup. Below are the patterns for data fetching, event listeners, timers, and code that must run once per app load.
Tested with: React 18.2.0, development build, wrapped in <StrictMode>.
What Actually Happens in Strict Mode
useEffect(() => {
console.log("setup");
return () => console.log("cleanup");
}, []);In development with <StrictMode> the console prints setup, then cleanup, then setup again. In a production build you see a single setup.
- An empty dependency array does not prevent this. [] means "don't re-run after re-renders" — the Strict Mode check still happens once after mount.
- It only happens in development. Your users never see the double run.
- useLayoutEffect behaves the same way.
- Most starter setups enable Strict Mode: the Vite and Create React App templates wrap the app in <StrictMode>, and Next.js turns it on by default for the App Router.
React does this on purpose: an effect without a proper cleanup (a listener that is never removed, a request whose result arrives late) is a real bug that shows up later in production, when a component unmounts and mounts again. Running setup twice makes that bug visible immediately. See React's documentation on this behavior.
Fix 1: Data Fetching — Ignore Stale Results
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let ignore = false;
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => {
if (!ignore) setUser(data);
});
return () => {
ignore = true;
};
}, [userId]);
if (!user) return <p>Loading…</p>;
return <h1>{user.name}</h1>;
}In development the request is sent twice, but only the second response is used — the first one arrives after its cleanup set ignore to true. The same flag fixes a real production race: if userId changes quickly, a slow response for the previous user can no longer overwrite the current one.
If you want the stale request cancelled instead of ignored, use AbortController:
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then((res) => res.json())
.then(setUser)
.catch((err) => {
if (err.name !== "AbortError") console.error(err);
});
return () => controller.abort();
}, [userId]);Fix 2: Event Listeners and Subscriptions
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);Without the cleanup, the double run would leave two listeners attached. That leak is exactly what Strict Mode is trying to show you. The same rule applies to WebSockets, IntersectionObserver, and store subscriptions: whatever setup opens, cleanup closes.
Fix 3: Timers and Intervals
useEffect(() => {
const id = setInterval(() => {
setSeconds((s) => s + 1);
}, 1000);
return () => clearInterval(id);
}, []);Note the updater function setSeconds((s) => s + 1): it reads the latest value, so the effect does not need seconds in its dependencies.
Fix 4: Code That Must Run Once per App Load
Some logic — initializing an analytics SDK, reading a token from storage — should run once per page load, not once per component mount. It doesn't belong to a component's lifecycle at all. Use a module-level flag:
let didInit = false;
function App() {
useEffect(() => {
if (!didInit) {
didInit = true;
loadSettingsFromStorage();
checkAuthToken();
}
}, []);
// ...
}Or run it before React renders, at the top level of your entry file (for example main.jsx, before createRoot).
Why Not Just Use a useRef Guard?
A popular snippet stores a hasRun flag in a ref and skips the second call. An earlier version of this article recommended it. It is the wrong fix:
// ❌ Don't do this
function useRunOnceEffect(callback) {
const hasRun = useRef(false);
useEffect(() => {
if (!hasRun.current) {
hasRun.current = true;
callback();
}
}, []);
}- It hides the check instead of passing it. Missing cleanups (listeners, sockets, timers) go unnoticed until they leak in production.
- It doesn't mean "once per app". A ref belongs to one component instance. When the component really unmounts and mounts again — navigating away and back, a changed key — the ref starts over and the effect runs again, still without any cleanup.
- React's documentation advises against it. Refs shouldn't be used to prevent effects from firing; write the cleanup instead.
In our test the ref guard did run the callback only once in Strict Mode — it "works" in the narrow sense, which is why it is tempting. Use the ignore flag, a real cleanup, or a module-level flag instead.
Should You Disable Strict Mode?
Removing <StrictMode> makes the double run disappear in development, but production was never affected — so you gain nothing for users and lose the check that catches missing cleanups. Keep it on and fix the effect.
When an Effect Runs Too Often in Production
If an effect runs more than you expect in a production build, Strict Mode is not the cause. Check these:
Unstable dependencies
An object, array, or function created during render is a new value every render, so an effect that depends on it re-runs every render:
// ❌ options is a new object on every render
const options = { userId };
useEffect(() => {
const conn = connect(options);
return () => conn.close();
}, [options]);
// ✅ create it inside the effect and depend on the primitive
useEffect(() => {
const conn = connect({ userId });
return () => conn.close();
}, [userId]);Updating state the effect depends on
// ❌ every update changes count, which re-runs the effect: infinite loop
useEffect(() => {
setCount(count + 1);
}, [count]);Ask whether you need an effect at all. A value that can be computed from props or state should be computed during render, not copied into state by an effect.
The component remounts
A changing key, or a parent that conditionally renders the component, creates a new instance, and every new instance runs its effects again.
Let a Data-Fetching Library Handle It
TanStack Query (React Query) and SWR cache and deduplicate requests and handle stale responses for you. With TanStack Query v5:
import { useQuery } from "@tanstack/react-query";
function UserProfile({ userId }) {
const { data: user, isPending, error } = useQuery({
queryKey: ["user", userId],
queryFn: () => fetch(`/api/users/${userId}`).then((res) => res.json()),
});
if (isPending) return <p>Loading…</p>;
if (error) return <p>Something went wrong.</p>;
return <h1>{user.name}</h1>;
}In a Next.js App Router project you can often fetch in a Server Component and skip the client-side effect entirely.
Quick Checklist
- Two runs only in development? That's Strict Mode — add a cleanup, don't suppress it.
- Fetching data? Use an ignore flag or AbortController.
- Listener, subscription, or timer? Remove or clear it in the cleanup.
- Must run once per app load? A module-level flag, not useRef.
- Re-running in production? Look for new objects or functions in the dependency array.
FAQs – Why useEffect Runs Twice
Why is useEffect running twice in React 18?
In development with Strict Mode, React mounts the component, runs the effect's cleanup, and runs the effect again once, to verify that the cleanup undoes the setup.
Does this happen in production?
No. A production build runs the effect once per mount.
Does an empty dependency array stop the second run?
No. [] stops re-runs after re-renders; the Strict Mode check after the first mount still happens in development.
Why is my API called twice?
In development, because of Strict Mode — use an ignore flag, AbortController, or a data-fetching library. If it also happens in production, check the dependency array and whether the component remounts.
Should I use useRef to make the effect run only once?
No. It hides missing cleanups and still re-runs when the component truly remounts. Write a cleanup, or use a module-level flag for once-per-app logic.
Is useLayoutEffect affected too?
Yes. It goes through the same setup → cleanup → setup cycle in development.
Related Articles
- What is React JS? Features, Benefits, and Examples Explained
- Mastering React Hooks: The Comprehensive Guide on Hooks in React JS
- React Environment Setup: A Comprehensive Guide
- React vs Angular: Deciding the Best Framework for Your Project in 2025
- React 19 is Now Stable: Learn What's New in React 19
- React 19 UseOptimistic Hook: What is UseOptimistic Hook and Everything You Need to Know
- React 19 Use Hook: What is Use Hook and Everything You Need to Know
- Secure React Applications: Common Vulnerabilities
- Mastering Advanced React Patterns: Higher-Order Components, Render Props, and Custom Hooks
- Mastering State Management in React: A Comprehensive Guide with Redux, Context API, and Zustand
- Learn How to Use TypeScript with React: Benefits and Best Practices
- Learn How to Build an Interactive Map with React and Leaflet
- How to Use Apollo Client with React for GraphQL Integration
- Debugging Memory Leaks in React: Tools, Fixes & Tips
Related Articles
View allAbout Muhaymin Bin Mehmood
Front-end Developer skilled in the MERN stack, experienced in web and mobile development. Proficient in React.js, Node.js, and Express.js, with a focus on client interactions, sales support, and high-performance applications.




