Frontend Development 10 min read

How to Make React Effects Work Correctly in Strict Mode

This article explains how React's Strict Effects mode double‑invokes effects and layout effects during mount and unmount, and provides practical patterns using refs, cleanup functions, and focus management to ensure components behave correctly when effects run multiple times.

KooFE Frontend Team
KooFE Frontend Team
KooFE Frontend Team
How to Make React Effects Work Correctly in Strict Mode

Source: translated article "How to support strict effects".

Overview

If you haven't read the previous article on StrictMode changes, you can find it elsewhere. Below is a component code example that runs effects on mount and unmount.

<code>function ExampleComponent(props) {
  useEffect(() => {
    // Effect setup code...
    return () => {
      // Effect cleanup code...
    };
  }, []);

  useLayoutEffect(() => {
    // Layout effect setup code...
    return () => {
      // Layout effect cleanup code...
    };
  }, []);

  // Render stuff...
}
</code>

In Strict Effects mode React follows this flow:

React renders the component

React mounts the component

Executes layout‑effect setup code

Executes effect setup code

React simulates the component being hidden or unmounted

Executes layout‑effect cleanup code

Executes effect cleanup code

React simulates the component being shown or remounted

Executes layout‑effect setup code

Executes effect setup code

As long as an effect returns a cleanup function, the double execution usually does not cause problems because most effects have dependencies and can handle being remounted. However, if an effect only runs on mount, you may need to adjust it. Two cases are most affected by multiple mounts: effects that need cleanup on unmount, and effects that run only once (on mount or when dependencies change).

Effect Cleanup Is Symmetric

When an effect adds listeners or uses imperative APIs, the cleanup function should mirror the setup. The common pattern looks like this:

<code>// A Ref (or Memo) is used to init and cache some imperative API.
const ref = useRef(null);
if (ref.current === null) {
  ref.current = new SomeImperativeThing();
}

// Note this could be useLayoutEffect too; same pattern.
useEffect(() => {
  const someImperativeThing = ref.current;
  return () => {
    // And an unmount effect (or layout effect) is used to destroy it.
    someImperativeThing.destroy();
  };
}, []);
</code>

If the component is unmounted and remounted, the imperative API stored in

ref

would be destroyed after the first unmount. To fix this, re‑initialize the ref on each mount:

<code>// Don't use a Ref to initialize SomeImperativeThing!

useEffect(() => {
  // Initialize an imperative API inside the same effect that destroys it.
  // This way it will be recreated if the component gets remounted.
  const someImperativeThing = new SomeImperativeThing();

  return () => {
    someImperativeThing.destroy();
  };
}, []);
</code>

Other functions, such as event handlers, can share the ref value when needed.

<code>// Use a Ref to hold the value, but initialize it in an effect.
const ref = useRef(null);

useEffect(() => {
  // Initialize an imperative API inside the same effect that destroys it.
  // This way it will be recreated if the component gets remounted.
  const someImperativeThing = ref.current = new SomeImperativeThing();

  return () => {
    someImperativeThing.destroy();
  };
}, []);

const handleThing = (event) => {
  const someImperativeThing = ref.current;
  // Now we can call methods on the imperative API...
};
</code>

For sharing an imperative API across components, a lazy‑init getter can be exposed:

<code>// This ref holds the imperative thing.
// It should only be referenced by the current component.
const ref = useRef(null);

// This lazy init function can be shared with other components,
// although it should only be called from an effect or an event handler.
// It should not be called during render.
const getterRef = useRef(() => {
  if (ref.current === null) {
    ref.current = new SomeImperativeThing();
  }
  return ref.current;
});

useEffect(() => {
  // This component doesn't need to (re)create the imperative API.
  // Any code that needs it will do this automatically by calling the getter.
  return () => {
    // It's possible that nothing called the getter function,
    // in which case we don't have to clean up the imperative code.
    if (ref.current !== null) {
      ref.current.destroy();
      ref.current = null;
    }
  };
}, []);
</code>

One‑Time Effects Can Use Ref

If an effect has no cleanup function, it works unchanged in Strict Effects mode. For example, an effect that logs an impression:

<code>useEffect(() => {
  SomeTrackingAPI.logImpression();
}, []);
</code>

When the content is hidden and shown again (e.g., tab switching), you may want to log only once. Using a ref ensures the log runs a single time:

<code>const didLogRef = useRef(false);

useEffect(() => {
  // Whether mounting or remounting, use a ref so we only log once.
  if (didLogRef.current === false) {
    didLogRef.current = true;
    SomeTrackingAPI.logImpression();
  }
}, []);
</code>

"auto focus" Then Restore Focus

A common UI pattern opens a modal, auto‑focuses an element inside it, and restores focus to the button that opened the modal when it closes. The following component records the element to restore focus to:

<code>function Modal({ children }) {
  const restoreFocus = React.useRef(null);

  function handleFocus(event) {
    if (restoreFocus.current === null) {
      restoreFocus.current = event.relatedTarget;
    }
  }

  React.useEffect(() => {
    return () => {
      if (restoreFocus.current !== null) {
        restoreFocus.current.focus();
        restoreFocus.current = null;
      }
    };
  }, []);

  return <div onFocus={handleFocus}>{children}</div>;
}
</code>

Using a button to toggle the modal:

<code>const [open, setOpen] = React.useState(false);

return (
  <React.Fragment>
    <button onClick={() => setOpen(!open)}>{open ? "close" : "open"}</button>
    {open && (
      <Modal>
        <input autoFocus />
      </Modal>
    )}
  </React.Fragment>
);
</code>

In Strict Effects mode, React simulates unmounting by hiding the element, so the focus restoration happens on the button rather than the input, producing a different result. To achieve reliable auto‑focus, you can focus a ref after the modal mounts:

<code>const [open, setOpen] = React.useState(false);
const target = React.useRef(null);

React.useEffect(() => {
  target.current.focus();
}, []);

return (
  <React.Fragment>
    <button onClick={() => setOpen(!open)}>{open ? "close" : "open"}</button>
    {open && (
      <Modal>
        <input ref={target} />
      </Modal>
    )}
  </React.Fragment>
);
</code>

Examples Do Not Cover All Cases

This article only covers the most common scenarios and is not an exhaustive list; future posts will address less‑common cases.

ReactuseEffectRefuseLayoutEffectfocusStrictMode
KooFE Frontend Team
Written by

KooFE Frontend Team

Follow the latest frontend updates

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.