🚀 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!
}, [])
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')
}, [])
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
}, [])
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)
}
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?
}
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)
}
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)
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:
- True Global Uniqueness: Based on effect function references, regardless of which component instance, the same function will only execute once
-
Automatic Garbage Collection: When components unmount, effect function references disappear, and
WeakSet
automatically cleans up without memory leaks - Clear Semantics: The WeakSet approach has unambiguous semantics—the same effect function reference runs only once
- Zero Configuration: No need to consider dependency array changes, state resets, and other complex situations
- 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
}
}
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)
}
}, [])
Extension: Supporting useLayoutEffect
This solution also thoughtfully supports useLayoutEffect
:
export const useOnceLayoutEffect = createOnceEffect(useLayoutEffect)
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:
- Dependency arrays still matter: Even for "run only once," you should correctly set dependency arrays
- Cleanup functions work as usual: Returned cleanup functions will still execute when components unmount
- 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
import { useOnceEffect } from '@reactuse/core'
function MyComponent() {
useOnceEffect(() => {
console.log('Will only run once, even in strict mode')
}, [])
return <div>My Component</div>
}
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)
Very interesting information, thank you!