React’s Strict Mode double-invokes effects to help you find bugs you didn’t know you had.

Let’s see it in action. Imagine a simple component that fetches data on mount:

import React, { useState, useEffect } from 'react';

function DataFetcher() {
  const [data, setData] = useState(null);

  useEffect(() => {
    console.log('Fetching data...');
    fetch('/api/data')
      .then(res => res.json())
      .then(data => setData(data));
  }, []); // Empty dependency array means this runs once on mount

  return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}

export default DataFetcher;

In a regular React app, this useEffect runs once. But if you wrap DataFetcher in React.StrictMode:

import React from 'react';
import ReactDOM from 'react-dom/client';
import DataFetcher from './DataFetcher';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <DataFetcher />
  </React.StrictMode>
);

You’ll see "Fetching data…" logged to your console twice during the initial render.

This double-invocation is Strict Mode’s secret weapon. It simulates unmounting and re-mounting components by running the effect and then running the cleanup function immediately, followed by running the effect again.

The core problem Strict Mode helps you catch is the lack of referential integrity in your effects. When an effect’s cleanup function runs, it’s supposed to undo whatever the effect did. If your effect does something that has side effects outside of its immediate scope, and you don’t properly clean it up, you can end up with stale data, memory leaks, or unexpected behavior when the component re-renders or is unmounted.

Here’s how it works under the hood:

  1. Initial Render: Your component mounts.
  2. Effect Runs: The useEffect callback executes.
  3. Strict Mode Simulation: React immediately calls the cleanup function associated with that effect.
  4. Re-mount Simulation: React then calls the useEffect callback again.
  5. Second Cleanup: On a true unmount (or subsequent re-renders depending on the lifecycle), the cleanup function would run again.

The key is that the cleanup function is designed to run when the component unmounts. By running it immediately after the effect on mount, Strict Mode forces you to ensure your cleanup logic is robust enough to handle being called at any time, not just at the end of the component’s life.

What does this mean for your code?

  • Idempotent Effects: Your effect logic should ideally be idempotent. This means running it multiple times should have the same effect as running it once. If your effect modifies global state, subscribes to an event, or starts a timer, you must have a corresponding cleanup that reverses these actions.
  • Correct Cleanup Functions: The cleanup function returned from useEffect is crucial. If you don’t return anything, React assumes there’s no cleanup needed. If you do return a function, React will call it before re-running the effect or when the component unmounts.

Let’s look at a common offender: setting up event listeners without cleaning them up.

import React, { useState, useEffect } from 'react';

function EventListenerComponent() {
  const [message, setMessage] = useState('Waiting for event...');

  useEffect(() => {
    const handleScroll = () => {
      console.log('Scrolled!');
      setMessage('Scroll detected!');
    };

    window.addEventListener('scroll', handleScroll);

    // This is the critical part Strict Mode helps you find
    // If you forget this, you'll have duplicate listeners on re-renders
    return () => {
      console.log('Cleaning up scroll listener...');
      window.removeEventListener('scroll', handleScroll);
    };
  }, []); // Empty dependency array

  return <div>{message}</div>;
}

export default EventListenerComponent;

In a non-strict mode app, if EventListenerComponent re-renders for any reason (even without a dependency change in the useEffect), the useEffect callback would run again, adding another scroll listener, leading to multiple "Scrolled!" logs. Strict Mode, by simulating that immediate unmount/re-mount, would force the removeEventListener to run right after the addEventListener during the initial render. If your cleanup function is correct, this doesn’t cause a problem. If it’s missing, you’ll see the console.log('Cleaning up scroll listener...') and then the effect will run again, adding the listener a second time.

The most common cause of issues with Strict Mode’s double-invocation is forgetting to return a cleanup function from useEffect when the effect performs a side effect that needs to be undone. This includes:

  1. Adding Event Listeners: As shown above, always removeEventListener in the cleanup.
  2. Setting Timers (setTimeout, setInterval): Always clearTimeout or clearInterval in the cleanup.
  3. Subscribing to External Data Sources/WebSockets: Always unsubscribe in the cleanup.
  4. Mutating Global Objects/DOM: Undo the mutation in the cleanup.
  5. Starting Asynchronous Operations Without Cancellation: If an async operation might complete after the component unmounts, you need a way to ignore its result. A common pattern is to use a boolean flag set in the cleanup.

Consider this pattern for handling asynchronous operations:

import React, { useState, useEffect } from 'react';

function AsyncOperationComponent() {
  const [result, setResult] = useState(null);

  useEffect(() => {
    let isMounted = true; // Flag to track if component is still mounted

    console.log('Starting async operation...');
    fetch('/api/slow-data')
      .then(res => res.json())
      .then(data => {
        if (isMounted) { // Only update state if component is still mounted
          console.log('Async operation completed, updating state.');
          setResult(data);
        } else {
          console.log('Async operation completed, but component unmounted.');
        }
      })
      .catch(error => {
        if (isMounted) {
          console.error('Async operation failed:', error);
          // Handle error appropriately
        }
      });

    // Cleanup function
    return () => {
      console.log('Cleaning up async operation (setting isMounted to false).');
      isMounted = false; // Mark component as unmounted
    };
  }, []); // Empty dependency array

  return <div>{result ? JSON.stringify(result) : 'Processing...'}</div>;
}

export default AsyncOperationComponent;

In this AsyncOperationComponent, Strict Mode will cause the useEffect to run, start the fetch, and then immediately run the cleanup (isMounted = false). When the fetch completes, it checks isMounted. If it’s false, the setResult call is skipped, preventing a state update on an unmounted component. This is precisely the kind of bug Strict Mode’s double-invocation helps you expose.

The most subtle aspect of Strict Mode’s double-invocation is that it doesn’t just check if your cleanup function exists, but if it correctly reverses the effect’s side effects. For example, if your effect console.logs something, and your cleanup also console.logs something, Strict Mode will run your effect’s console.log, then your cleanup’s console.log, then your effect’s console.log again. While this doesn’t break anything, it highlights that your cleanup is running. The real danger is when the effect does something persistent (like adding a listener or setting a timer) and the cleanup fails to remove it. Strict Mode makes these failures immediately apparent during development.

After fixing all issues related to double-invoked effects, the next thing you might encounter in Strict Mode is warnings about legacy string refs.

Want structured learning?

Take the full React course →