
Remember that time Cloudflare accidentally took down their own dashboard because of a React useEffect bug? Yeah, that happened. In September 2025, a simple mistake in a dependency array caused their dashboard to make repeated, unnecessary calls to their Tenant Service API, overwhelming it and causing a widespread outage. The best part? This is exactly the kind of problem React's new useEffectEvent hook was designed to solve.
The Cloudflare Incident: A Cautionary Tale
Here's what went wrong at Cloudflare: they had a useEffect hook that was supposed to call their Tenant Service API once, but they accidentally included a problematic object in the dependency array. This object was being recreated on every render, so React thought it was "always new" and kept re-running the effect. The result? Their dashboard was essentially DDoS-ing their own API.
// The problematic pattern (what Cloudflare had)
useEffect(() => {
fetchTenantData()
}, [someObjectThatGetsRecreatedEveryRender]) // Oops!
This is a classic React gotcha that most developers have encountered at least once. The object in the dependency array gets recreated on every render, causing the effect to run infinitely, which in Cloudflare's case meant their API was getting hammered with requests.
Enter useEffectEvent: The Solution
React's useEffectEvent is a new hook (currently in React Canary) that's specifically designed to solve this problem. It allows you to include values in your effect's dependencies without causing the effect to re-run when those values change.
Here's how you'd fix the Cloudflare scenario with useEffectEvent:
import { useEffect, useEffectEvent } from 'react'
function Dashboard() {
const fetchTenantData = useEffectEvent(() => {
// This function can access the latest values without
// causing the effect to re-run
fetch('/api/tenant-data')
.then((response) => response.json())
.then((data) => setTenantData(data))
})
useEffect(() => {
fetchTenantData()
}, []) // Empty dependency array - effect runs once
}
Why useEffectEvent is a Game Changer
The traditional way to handle this would be to either:
- Use
useCallbackto memoize the function (but you still need to be careful about dependencies) - Move the function inside the effect (but then you can't reuse it)
- Use
useRefto store the latest values (but it's verbose and error-prone)
useEffectEvent solves this elegantly by letting you access the latest values without triggering re-renders. It's like having your cake and eating it too, but without the calories.
Real-World Examples
Let's look at some common patterns where useEffectEvent shines. These examples show the specific problem it solves: accessing reactive values without causing effects to re-run.
1. The Classic "Stale Closure" Problem
Here's a common scenario where you want to access the latest state/props in an effect without causing it to re-run:
function SearchResults({ query, filters }) {
const [results, setResults] = useState([])
const [isLoading, setIsLoading] = useState(false)
// Without useEffectEvent, this would cause the effect to re-run
// every time query or filters change
const performSearch = useEffectEvent(() => {
// This function can access the latest query and filters
// without causing the effect to re-run
setIsLoading(true)
searchAPI(query, filters)
.then((data) => setResults(data))
.finally(() => setIsLoading(false))
})
useEffect(() => {
// This effect only runs once on mount
// but performSearch always has access to latest values
performSearch()
}, []) // Empty dependency array - no re-runs!
return (
<div>
{isLoading
? 'Loading...'
: results.map((item) => <div key={item.id}>{item.title}</div>)}
</div>
)
}
2. Event Handlers That Need Latest State
This is where useEffectEvent really shines - when you need to access the latest state in event handlers without causing effects to re-run:
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([])
const [user, setUser] = useState(null)
// This function can access the latest messages and user
// without causing the WebSocket to reconnect
const handleNewMessage = useEffectEvent((message) => {
// Always has access to the latest messages and user
setMessages((prev) => [...prev, { ...message, user: user?.name }])
})
useEffect(() => {
const socket = new WebSocket(`ws://localhost:8080/rooms/${roomId}`)
socket.onmessage = (event) => {
const message = JSON.parse(event.data)
handleNewMessage(message) // This always has latest state
}
return () => socket.close()
}, [roomId]) // Only re-runs when roomId changes, not when messages or user change
}
3. Analytics with Dynamic Values
Here's a perfect use case - tracking events that need the latest values but shouldn't cause effects to re-run:
function ProductPage({ productId, userId, category }) {
const [viewTime, setViewTime] = useState(0)
const [hasInteracted, setHasInteracted] = useState(false)
// This function can access the latest values without causing
// the effect to re-run every time they change
const trackEvent = useEffectEvent((eventName, properties = {}) => {
analytics.track(eventName, {
productId,
userId,
category,
viewTime,
hasInteracted,
...properties,
timestamp: Date.now(),
})
})
useEffect(() => {
// Track page view once
trackEvent('page_view')
// Track scroll depth every 5 seconds
const interval = setInterval(() => {
trackEvent('scroll_depth', { depth: window.scrollY })
}, 5000)
return () => clearInterval(interval)
}, []) // Runs once, but trackEvent always has latest values
}
4. The Cloudflare Scenario (Fixed)
Here's how Cloudflare's issue would look with useEffectEvent:
function Dashboard() {
const [tenantData, setTenantData] = useState(null)
const [user, setUser] = useState(null)
const [preferences, setPreferences] = useState({})
// This function can access the latest user and preferences
// without causing the effect to re-run
const fetchTenantData = useEffectEvent(() => {
// Always has access to the latest user and preferences
const requestBody = {
userId: user?.id,
preferences: preferences,
timestamp: Date.now(),
}
fetch('/api/tenant-data', {
method: 'POST',
body: JSON.stringify(requestBody),
})
.then((response) => response.json())
.then((data) => setTenantData(data))
})
useEffect(() => {
// This only runs once on mount
// but fetchTenantData always has the latest values
fetchTenantData()
}, []) // Empty dependency array - no infinite loops!
return <div>{/* Dashboard UI */}</div>
}
The Mental Model
Think of useEffectEvent as a way to create "stable" functions that can access the latest values without causing effects to re-run. It's like having a function that's always up-to-date but doesn't trigger the dependency system.
The key insight is that useEffectEvent functions are:
- Stable: They don't change between renders
- Fresh: They always have access to the latest values
- Safe: They won't cause infinite loops
Migration Strategy
If you're working with existing code that has these patterns, here's how to migrate:
- Identify problematic effects: Look for effects that have functions in their dependency arrays
- Extract the function: Move the function outside the effect and wrap it with
useEffectEvent - Remove from dependencies: Remove the function from the dependency array
- Test thoroughly: Make sure the effect still behaves as expected
Browser Support and Polyfills
useEffectEvent is currently in React Canary, so you'll need to use the experimental version:
npm install react@canary react-dom@canary
Or if you're using a specific version:
npm install react@18.3.0-canary-abc123 react-dom@18.3.0-canary-abc123
The Bottom Line
useEffectEvent is one of those React features that makes you wonder how we ever lived without it. It solves a real problem that every React developer has encountered, and it does so in an elegant way that's easy to understand and use.
The Cloudflare incident is a perfect example of why this hook matters. A simple mistake in a dependency array caused a major outage. With useEffectEvent, that mistake becomes much harder to make, and the consequences are much less severe.
So the next time you're writing a useEffect and you find yourself reaching for useCallback or useRef to avoid dependency issues, consider whether useEffectEvent might be the cleaner solution. Your backend (and your users) will thank you.
And remember, just because you can call an API doesn't mean you should call it infinitely. Sometimes the best code is the code that doesn't run when it shouldn't.