search
React star Featured

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.

person By Gautam Sharma
calendar_today January 1, 2026
schedule 14 min read
React useEffect React 18 Strict Mode Frontend Development

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:

  1. First run: Component mounts
  2. 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>
  );
}

You can disable Strict Mode to prevent the double execution, but this is not recommended as it removes the safety benefits.

// 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
);
// 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.

Gautam Sharma

About Gautam Sharma

Full-stack developer and tech blogger sharing coding tutorials and best practices

Related Articles

React

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.

January 1, 2026
React

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.

January 1, 2026
React

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.

January 1, 2026