notes.dt.in.th

React 18’s StrictEffectMode

These notes are copied from my Reddit comments about my earlier Notes about React 18 RC.

About StrictEffectMode

Comment

One more tidbit that I found particular interesting [in React 18] is the “doubling” of effects in <React.StrictMode> (no.10). I got bitten by it. I used to write code like this:

function App() {
  const canvasRef = useRef(null)
  useEffect(() => {
    window.addEventListener('resize', () => {
      resizeCanvasToWindow(canvasRef.current)
    })
  }, [])
  // ...
}

Note that I never called removeEventListener(). Normally this is a red flag, but since it’s the root component that never gets unmounted, this is actually fine. At least, in earlier versions of React this is not problem (although they render twice, they invoke effects just once.)

However, in React 18, under <React.StrictMode>, under development build, React will actually invoke the effect twice (i.e. effect()?.(); effect()) just to force you to always clean up after your effects.

With the example above, the event listener is now attached twice, so each 'resize' event invokes resizeCanvasToWindow() 2 times. (To fix, I need to add removeEventListener() in useEffect’s return. Took me a while to figure that one out!)

The history of StrictEffectMode

Comment

In case you are curious about the story I git blame’d the test files and here’s what I found:

  • This feature is called StrictEffectsMode (test file name).
  • Sep 2020: The feature is implemented in PR #19523 but is hidden behind a feature flag.
  • Feb 2021: 3 levels of strict modes introduced in PR #20844 and PR #20849.
    • Level 0 (sloppy mode) is ReactDOM.render.
    • Level 1 (legacy mode) is ReactDOM.render + StrictMode (which double-renders).
    • Level 2 (strict effects mode) is ReactDOM.createRoot + StrictMode (which double-renders + double-effects).