No articles found
Try different keywords or browse our categories
Fix: useEffect runs twice in React 18 Strict Mode - Complete Solution Guide
Learn how to fix the useEffect running twice issue in React 18 Strict Mode. This guide covers causes, solutions, and best practices for handling React 18's development behavior.
The ‘useEffect runs twice in React 18 Strict Mode’ behavior is a common source of confusion for React developers upgrading to React 18. This isn’t actually an error, but rather a new development feature designed to help you write more robust components by simulating component mount/unmount cycles.
This comprehensive guide explains why this happens, what it means, and provides multiple solutions to handle useEffect properly in React 18 with clean code examples and directory structure.
What Changed in React 18 Strict Mode?
In React 18, Strict Mode was enhanced to automatically detect and warn about potential problems in your components. One of these enhancements is that useEffect now runs twice in development mode - once for mounting and once for unmounting and remounting. This helps identify issues with missing cleanup functions and improper effect dependencies.
Key Changes:
- Development behavior: useEffect runs twice to simulate mount/unmount cycles
- Production behavior: useEffect runs once (normal behavior)
- Purpose: Helps identify missing cleanup and dependency issues
- Scope: Only affects development mode with Strict Mode enabled
Understanding the Problem
In React 17 and earlier, useEffect would run once when the component mounted. In React 18 with Strict Mode, the effect runs twice during development to help you identify potential issues:
- First run: Component mounts
- Second run: Component unmounts and remounts immediately
This behavior only occurs in development mode and with Strict Mode enabled.
Typical React Project Structure:
my-react-app/
├── package.json
├── src/
│ ├── App.jsx
│ ├── index.js
│ ├── components/
│ │ ├── DataFetcher.jsx
│ │ └── WebSocketComponent.jsx
│ └── hooks/
│ └── useApi.js
Solution 1: Implement Proper Cleanup Functions
The most important solution is to implement proper cleanup functions in your effects.
❌ Incorrect Usage:
// components/DataFetcher.jsx
import { useState, useEffect } from 'react';
function DataFetcher() {
const [data, setData] = useState(null);
useEffect(() => {
// ❌ No cleanup function - can cause memory leaks
fetch('/api/data')
.then(response => response.json())
.then(setData);
}, []);
return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}
✅ Correct Usage:
// components/DataFetcher.jsx
import { useState, useEffect } from 'react';
function DataFetcher() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let isMounted = true; // ✅ Flag to prevent state updates after unmount
const fetchData = async () => {
try {
const response = await fetch('/api/data');
const result = await response.json();
// ✅ Only update state if component is still mounted
if (isMounted) {
setData(result);
setLoading(false);
}
} catch (error) {
if (isMounted) {
console.error('Error fetching data:', error);
setLoading(false);
}
}
};
fetchData();
// ✅ Cleanup function
return () => {
isMounted = false;
};
}, []);
return <div>{loading ? 'Loading...' : JSON.stringify(data)}</div>;
}
Solution 2: Handle Side Effects Properly
For effects that perform side effects like subscriptions or timers, ensure proper cleanup.
❌ Incorrect Usage:
// components/Timer.jsx
import { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
// ❌ No cleanup - timer continues after component unmounts
const interval = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// Missing return statement for cleanup
}, []);
return <div>Timer: {seconds}s</div>;
}
✅ Correct Usage:
// components/Timer.jsx
import { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// ✅ Proper cleanup
return () => {
clearInterval(interval);
};
}, []);
return <div>Timer: {seconds}s</div>;
}
Solution 3: Handle WebSocket Connections
WebSocket connections require special cleanup to prevent memory leaks.
❌ Incorrect Usage:
// components/WebSocketComponent.jsx
import { useState, useEffect } from 'react';
function WebSocketComponent() {
const [messages, setMessages] = useState([]);
useEffect(() => {
// ❌ No cleanup - WebSocket remains open after unmount
const ws = new WebSocket('ws://localhost:8080');
ws.onmessage = (event) => {
setMessages(prev => [...prev, event.data]);
};
}, []);
return (
<div>
{messages.map((msg, index) => (
<div key={index}>{msg}</div>
))}
</div>
);
}
✅ Correct Usage:
// components/WebSocketComponent.jsx
import { useState, useEffect } from 'react';
function WebSocketComponent() {
const [messages, setMessages] = useState([]);
useEffect(() => {
const ws = new WebSocket('ws://localhost:8080');
let isMounted = true;
ws.onmessage = (event) => {
if (isMounted) {
setMessages(prev => [...prev, event.data]);
}
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
// ✅ Proper cleanup
return () => {
isMounted = false;
ws.close();
};
}, []);
return (
<div>
{messages.map((msg, index) => (
<div key={index}>{msg}</div>
))}
</div>
);
}
Solution 4: Handle Event Listeners
Event listeners must be properly removed to prevent memory leaks.
❌ Incorrect Usage:
// components/WindowResize.jsx
import { useState, useEffect } from 'react';
function WindowResize() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
// ❌ No cleanup - event listener remains after unmount
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
}, []);
return (
<div>
Window: {windowSize.width} x {windowSize.height}
</div>
);
}
✅ Correct Usage:
// components/WindowResize.jsx
import { useState, useEffect } from 'react';
function WindowResize() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
// ✅ Add event listener
window.addEventListener('resize', handleResize);
// ✅ Remove event listener on cleanup
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return (
<div>
Window: {windowSize.width} x {windowSize.height}
</div>
);
}
Solution 5: Disable Strict Mode (Not Recommended)
You can disable Strict Mode to prevent the double execution, but this is not recommended as it removes the safety benefits.
❌ Not Recommended:
// src/index.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('root');
const root = createRoot(container);
// ❌ Removing StrictMode disables the double execution
root.render(
<App /> // Without React.StrictMode
);
✅ Recommended:
// src/index.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('root');
const root = createRoot(container);
// ✅ Keep StrictMode for development benefits
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Solution 6: Use Custom Hook for Complex Effects
Create custom hooks to encapsulate complex effect logic with proper cleanup.
// hooks/useEventListener.js
import { useEffect, useRef } from 'react';
function useEventListener(eventName, handler, element = window) {
const savedHandler = useRef();
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
const eventListener = (event) => savedHandler.current(event);
if (element && element.addEventListener) {
element.addEventListener(eventName, eventListener);
}
return () => {
if (element && element.removeEventListener) {
element.removeEventListener(eventName, eventListener);
}
};
}, [eventName, element]);
}
// components/ClickCounter.jsx
import { useState } from 'react';
import { useEventListener } from '../hooks/useEventListener';
function ClickCounter() {
const [count, setCount] = useState(0);
useEventListener('click', () => {
setCount(prev => prev + 1);
});
return <div>Clicks: {count}</div>;
}
Solution 7: Handle API Calls with AbortController
For API calls, use AbortController to cancel requests when components unmount.
// components/ApiComponent.jsx
import { useState, useEffect } from 'react';
function ApiComponent() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// ✅ Create AbortController for request cancellation
const abortController = new AbortController();
const fetchData = async () => {
try {
const response = await fetch('/api/data', {
signal: abortController.signal // ✅ Pass signal to fetch
});
if (!abortController.signal.aborted) {
const result = await response.json();
setData(result);
}
} catch (err) {
if (err.name !== 'AbortError' && !abortController.signal.aborted) {
setError(err);
}
} finally {
if (!abortController.signal.aborted) {
setLoading(false);
}
}
};
fetchData();
// ✅ Cleanup: abort request when component unmounts
return () => {
abortController.abort();
};
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{JSON.stringify(data)}</div>;
}
Solution 8: Conditional Effects
Sometimes you want to run effects only under certain conditions.
// components/ConditionalEffect.jsx
import { useState, useEffect } from 'react';
function ConditionalEffect({ shouldRun }) {
const [data, setData] = useState(null);
useEffect(() => {
if (!shouldRun) return; // ✅ Early return if condition not met
let isMounted = true;
const fetchData = async () => {
try {
const response = await fetch('/api/data');
const result = await response.json();
if (isMounted) {
setData(result);
}
} catch (error) {
if (isMounted) {
console.error('Error:', error);
}
}
};
fetchData();
return () => {
isMounted = false;
};
}, [shouldRun]); // ✅ Include condition in dependency array
return <div>{data ? JSON.stringify(data) : 'No data'}</div>;
}
Complete Project Structure After Fix
my-react-app/
├── package.json
├── package-lock.json
├── node_modules/
├── public/
│ └── index.html
├── src/
│ ├── App.jsx
│ ├── index.js
│ ├── components/
│ │ ├── DataFetcher.jsx
│ │ ├── Timer.jsx
│ │ ├── WebSocketComponent.jsx
│ │ ├── WindowResize.jsx
│ │ ├── ApiComponent.jsx
│ │ └── ConditionalEffect.jsx
│ ├── hooks/
│ │ ├── useEventListener.js
│ │ └── useApi.js
│ └── utils/
│ └── helpers.js
Working Code Examples
Complete Safe Component Example:
// components/SafeEffectComponent.jsx
import { useState, useEffect } from 'react';
function SafeEffectComponent() {
const [count, setCount] = useState(0);
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
// ✅ Effect with proper cleanup
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
// ✅ Effect with cleanup and dependencies
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
return (
<div>
<h2>Count: {count}</h2>
<p>Window: {windowSize.width} x {windowSize.height}</p>
</div>
);
}
export default SafeEffectComponent;
Custom Hook for API Calls:
// hooks/useApi.js
import { useState, useEffect } from 'react';
function useApi(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const abortController = new AbortController();
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url, {
...options,
signal: abortController.signal
});
if (!abortController.signal.aborted) {
const result = await response.json();
setData(result);
}
} catch (err) {
if (err.name !== 'AbortError' && !abortController.signal.aborted) {
setError(err);
}
} finally {
if (!abortController.signal.aborted) {
setLoading(false);
}
}
};
fetchData();
return () => {
abortController.abort();
};
}, [url, JSON.stringify(options)]); // Note: JSON.stringify for object dependencies
return { data, loading, error };
}
export default useApi;
Best Practices for useEffect in React 18
1. Always Include Cleanup Functions
// ✅ Always include cleanup
useEffect(() => {
const subscription = someAPI.subscribe(callback);
return () => {
subscription.unsubscribe();
};
}, []);
2. Use Refs for Mutable Values
// ✅ Use refs to avoid stale closures
useEffect(() => {
const timer = setInterval(() => {
console.log('Current count:', countRef.current);
}, 1000);
return () => clearInterval(timer);
}, []);
3. Validate Dependencies
// ✅ Ensure all dependencies are included
useEffect(() => {
document.title = title; // title is a dependency
}, [title]);
4. Handle Race Conditions
// ✅ Handle race conditions with mounted flags
useEffect(() => {
let isMounted = true;
fetchData().then(result => {
if (isMounted) {
setData(result);
}
});
return () => {
isMounted = false;
};
}, []);
Debugging useEffect Double Execution
1. Add Console Logs
useEffect(() => {
console.log('Effect running');
return () => {
console.log('Effect cleanup');
};
}, []);
2. Use React DevTools
Check the React DevTools Profiler to see component mount/unmount cycles.
3. Check for Missing Dependencies
// ❌ Missing dependency
useEffect(() => {
document.title = name; // name is used but not in dependency array
}, []); // Should be [name]
// ✅ Correct
useEffect(() => {
document.title = name;
}, [name]);
Common Mistakes to Avoid
1. Forgetting Cleanup Functions
// ❌ Don't forget cleanup
useEffect(() => {
const interval = setInterval(() => {}, 1000);
// Missing return statement
}, []);
2. Stale Closures
// ❌ Stale closure issue
useEffect(() => {
const handler = () => {
console.log('Count:', count); // Always logs initial count
};
window.addEventListener('click', handler);
return () => window.removeEventListener('click', handler);
}, []); // Should be [count]
3. Incorrect Dependencies
// ❌ Missing dependencies
useEffect(() => {
if (status === 'active') {
doSomething();
}
}, []); // Should be [status]
Performance Considerations
1. Minimize Effect Dependencies
// ❌ Too many dependencies
useEffect(() => {
// Heavy computation
}, [obj.prop1, obj.prop2, obj.prop3]);
// ✅ Memoize object
const memoizedObj = useMemo(() => ({ prop1, prop2, prop3 }), [prop1, prop2, prop3]);
useEffect(() => {
// Heavy computation
}, [memoizedObj]);
2. Debounce Expensive Operations
// ✅ Debounce expensive operations
useEffect(() => {
const timeoutId = setTimeout(() => {
performExpensiveOperation();
}, 300);
return () => clearTimeout(timeoutId);
}, [inputValue]);
Security Considerations
1. Validate External Data
// ✅ Validate data before using
useEffect(() => {
fetch('/api/data')
.then(response => response.json())
.then(data => {
if (Array.isArray(data)) {
setItems(data);
}
});
}, []);
2. Sanitize DOM Manipulation
// ✅ Sanitize when manipulating DOM directly
useEffect(() => {
const div = document.getElementById('content');
if (div) {
div.innerHTML = sanitizeHtml(content); // Use proper sanitization
}
}, [content]);
Testing useEffect Behavior
1. Unit Tests
// Using React Testing Library
import { render, screen } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import Timer from '../components/Timer';
test('timer increments correctly', () => {
jest.useFakeTimers();
render(<Timer />);
act(() => {
jest.advanceTimersByTime(2000);
});
expect(screen.getByText(/Timer: 2s/)).toBeInTheDocument();
jest.useRealTimers();
});
2. Cleanup Tests
test('cleanup function is called', () => {
const cleanupFn = jest.fn();
function TestComponent() {
useEffect(() => {
return cleanupFn;
}, []);
return <div>Test</div>;
}
const { unmount } = render(<TestComponent />);
unmount();
expect(cleanupFn).toHaveBeenCalled();
});
Alternative Solutions
1. Use useLayoutEffect for DOM Manipulation
// For DOM manipulation that needs to happen before paint
useLayoutEffect(() => {
// DOM manipulation
return () => {
// Cleanup
};
}, []);
2. Custom Hook for Complex Logic
// Create reusable logic
function useSafeEffect(effect, deps) {
useEffect(() => {
let isMounted = true;
const result = effect(() => isMounted);
return () => {
isMounted = false;
if (result && typeof result === 'function') {
result();
}
};
}, deps);
}
Migration Checklist
- Review all useEffect hooks for proper cleanup functions
- Add AbortController to API calls
- Implement mounted flags for async operations
- Verify all dependencies are included in dependency arrays
- Test components with Strict Mode enabled
- Update custom hooks to handle double execution
- Add proper error boundaries where needed
Conclusion
The ‘useEffect runs twice in React 18 Strict Mode’ behavior is a feature, not a bug. It helps you identify potential issues with missing cleanup functions and improper effect dependencies. By implementing proper cleanup functions, handling async operations correctly, and following React’s best practices, you can write more robust and reliable components.
The key is to embrace this development feature as a tool that helps you write better code. With proper cleanup functions, mounted flags for async operations, and correct dependency arrays, your React 18 applications will be more resilient and performant. Remember that this behavior only occurs in development mode, so your production applications will run normally.
By following the solutions and best practices outlined in this guide, you’ll be able to handle useEffect properly in React 18 and create applications that are both functional and maintainable.
Related Articles
Fix: ReactDOM.render is not a function - React 18 Complete Migration Guide
Learn how to fix the 'ReactDOM.render is not a function' error in React 18. This guide covers the new root API, migration steps, and best practices for React 18 applications.
Fix: Invalid React hook call. Hooks can only be called inside of the body of a function component
Learn how to fix the 'Invalid hook call' error in React. This guide covers all causes, solutions, and best practices for proper React hook usage with step-by-step examples.
Fix: Module not found: Can't resolve 'react/jsx-runtime' - Complete Solution Guide
Learn how to fix the 'Module not found: Can't resolve react/jsx-runtime' error in React projects. This guide covers causes, solutions, and prevention strategies with step-by-step instructions.