DEV Community

reactuse.com
reactuse.com

Posted on

The Art of Preventing useEffect Double Execution in Strict Mode

🚀 Discover 100+ powerful React Hooks possibilities! Visit www.reactuse.com for comprehensive documentation with MCP (Model Context Protocol) support, or install via npm install @reactuses/core to supercharge your React development efficiency with our extensive hook collection!

Preface: When Your Side Effects Get a Twin

Have you ever encountered this scenario: you carefully craft a single API request, only to find two identical requests lurking in your network panel during development? Or perhaps your meticulously designed counter mysteriously starts counting from 2 instead of 1?

Congratulations, you've met React 18's "double insurance" mechanism in Strict Mode.

Strict Mode: React's "OCD" Design

React Strict Mode is like an overly cautious friend who always needs to double-check everything: "Are you sure you want to execute this side effect? Let me try it again to see if anything breaks."

// What you think will happen
useEffect(() => {
  console.log('I will only run once') // Naive!
}, [])

// What actually happens in Strict Mode
useEffect(() => {
  console.log('I will only run once') // First time
  console.log('I will only run once') // Second time: Surprise!
}, [])
Enter fullscreen mode Exit fullscreen mode

The intention behind this design is good—it helps you discover side effects that aren't "pure" enough in advance. But in real development, sometimes we genuinely need certain operations to run only once, such as:

  • Sending analytics data
  • Initializing third-party libraries
  • Setting up global event listeners
  • Logging user behavior

Traditional Solutions: Each Has Its Own Awkwardness

Solution 1: The useRef Approach

const hasRun = useRef(false)

useEffect(() => {
  if (hasRun.current) return
  hasRun.current = true

  // Your side effect logic
  console.log('Finally runs only once')
}, [])
Enter fullscreen mode Exit fullscreen mode

Problem: You need to write this boilerplate code everywhere you need "run only once," like adding the same seasoning to every dish.

Solution 2: Global Flag

let hasInitialized = false

useEffect(() => {
  if (hasInitialized) return
  hasInitialized = true

  // Initialization logic
}, [])
Enter fullscreen mode Exit fullscreen mode

Problem: Global variable pollution, multiple component instances interfere with each other, like roommates sharing a fridge—someone's always taking the wrong stuff.

Why Not Wrap Based on useRef?

You might wonder: "Since the useRef approach works, why not just wrap it into a custom hook?" This is indeed a natural thought:

// Seemingly reasonable useRef wrapper
function useOnceEffect(effect, deps) {
  const hasRun = useRef(false)

  useEffect(() => {
    if (hasRun.current) return
    hasRun.current = true
    return effect()
  }, deps)
}
Enter fullscreen mode Exit fullscreen mode

But this approach has several fatal flaws:

Problem 1: Semantic Confusion with Dependency Changes

function MyComponent({ userId }) {
  useOnceEffect(() => {
    console.log(`Sending analytics for user ${userId}`)
    analytics.track('user_action', { userId })
  }, [userId]) // What happens when userId changes?
}
Enter fullscreen mode Exit fullscreen mode

When userId changes, do we want to:

  • Re-execute the effect (because it's a new user)?
  • Or continue blocking execution (because it's "once")?

The useRef approach has ambiguous semantics here, while the WeakSet approach is crystal clear: the same effect function reference runs only once.

Problem 2: Memory Management Complexity

Using useRef wrapper requires considering when to reset hasRun.current:

function useOnceEffect(effect, deps) {
  const hasRun = useRef(false)
  const lastDeps = useRef(deps)

  // Complex dependency comparison logic needed
  const depsChanged = !lastDeps.current || 
    deps.some((dep, i) => dep !== lastDeps.current[i])

  if (depsChanged) {
    hasRun.current = false // Reset state
    lastDeps.current = deps
  }

  useEffect(() => {
    if (hasRun.current) return
    hasRun.current = true
    return effect()
  }, deps)
}
Enter fullscreen mode Exit fullscreen mode

This makes the code complex and error-prone—you might as well use the native useEffect directly.

Elegant Solution: createOnceEffect

Let's look at a more elegant solution:

import { useEffect, useLayoutEffect } from 'react'

type EffectHookType = typeof useEffect | typeof useLayoutEffect

const record = new WeakSet()

const createOnceEffect: (hook: EffectHookType) => EffectHookType
  = hook => (effect, deps) => {
    const onceWrapper = () => {
      const shouldStart = !record.has(effect)
      if (shouldStart) {
        record.add(effect)
        return effect()
      }
    }
    hook(() => {
      return onceWrapper()
    }, deps)
  }

export const useOnceEffect = createOnceEffect(useEffect)
Enter fullscreen mode Exit fullscreen mode

Design Philosophy: The Clever Use of WeakSet

The core idea of this solution is using WeakSet to record effect functions that have already been executed. Why choose WeakSet over regular Set or arrays?

Advantages of WeakSet

Compared to the useRef approach, the WeakSet solution has fundamental advantages:

  1. True Global Uniqueness: Based on effect function references, regardless of which component instance, the same function will only execute once
  2. Automatic Garbage Collection: When components unmount, effect function references disappear, and WeakSet automatically cleans up without memory leaks
  3. Clear Semantics: The WeakSet approach has unambiguous semantics—the same effect function reference runs only once
  4. Zero Configuration: No need to consider dependency array changes, state resets, and other complex situations
  5. Superior Performance: Lookup and addition operations are O(1) time complexity, with no complex dependency comparison logic

How It Works

const onceWrapper = () => {
  const shouldStart = !record.has(effect) // Check if already executed
  if (shouldStart) {
    record.add(effect) // Mark as executed
    return effect() // Execute original effect
  }
}
Enter fullscreen mode Exit fullscreen mode

This is like giving each effect function an "already executed" ID card. When it tries to run a second time, the gatekeeper says: "Oh, you've already been in, no need to enter again this time."

Practical Usage: Say Goodbye to Duplicate Execution

// Replace the original useEffect
useOnceEffect(() => {
  // Send page visit analytics
  analytics.track('page_view', { page: 'home' })

  // Initialize third-party library
  initThirdPartySDK()

  // Set up global listeners
  window.addEventListener('beforeunload', handleBeforeUnload)

  return () => {
    window.removeEventListener('beforeunload', handleBeforeUnload)
  }
}, [])
Enter fullscreen mode Exit fullscreen mode

Extension: Supporting useLayoutEffect

This solution also thoughtfully supports useLayoutEffect:

export const useOnceLayoutEffect = createOnceEffect(useLayoutEffect)
Enter fullscreen mode Exit fullscreen mode

When you need to synchronously execute certain operations before DOM updates but only want to run them once, this comes in handy.

Caveats: Not a Silver Bullet

While useOnceEffect is very useful, remember:

  1. Dependency arrays still matter: Even for "run only once," you should correctly set dependency arrays
  2. Cleanup functions work as usual: Returned cleanup functions will still execute when components unmount
  3. Limited use cases: Only suitable for scenarios that truly need "global run only once"

Ready-Made Solution

If you don't want to implement this yourself, you can directly use the ready-made solution. The ReactUse library has already implemented useOnceEffect for you, ready to use out of the box:

npm install @reactuse/core
Enter fullscreen mode Exit fullscreen mode
import { useOnceEffect } from '@reactuse/core'

function MyComponent() {
  useOnceEffect(() => {
    console.log('Will only run once, even in strict mode')
  }, [])

  return <div>My Component</div>
}
Enter fullscreen mode Exit fullscreen mode

Conclusion: Making Code More Elegant

React Strict Mode's double execution mechanism may cause some confusion, but its existence helps us write better code. Tools like createOnceEffect help us elegantly handle scenarios that truly need "run only once" while keeping our code clean.

Remember, good code doesn't just solve problems—it should be pleasant to read. Like this clever use of WeakSet, it both solves practical problems and showcases the elegant features of the JavaScript language.

Next time you encounter Strict Mode's "twin" problem, give this approach a try. You'll discover that solving problems can be so elegant.

Top comments (1)

Collapse
 
companyurlfinder profile image
Company URL Finder

Very interesting information, thank you!