-
Notifications
You must be signed in to change notification settings - Fork 72
Description
Is your feature request related to a problem? Please describe.
Lets say you have a flag that is used across 5 components on a page. When that page renders, 5 events would get sent - except this is react and re-renders happen on very fast when loading components, so those 5 events multiply into 70 events sent within one second. Now multiply that flag issue for 3 more flags you have on that page, and you're sending over 300 events in a second. It quickly scales out of control, and saying we should reduce our re-renders is a very difficult task in large and old code bases.
My ask is to make a useCachedFlag() hook that allows to skip the observable event every x seconds. So in the example above, we would only send one event (using a cache) or three events (using debounce)
For context, we were bringing in anywhere between 150 million and 210 million rows of data a day during peak activity. Our homepage on load would send 120 events, and our product page would sent 250.
Describe the solution you'd like
I've made two different solutions in my own codebase that work great.
Debounced Method
import { useLDClient } from 'launchdarkly-react-client-sdk'
import { useEffect, useRef, useState } from 'react'
/**
* Get a specific flag with debouncing to prevent duplicate evaluations
*
* If you need to pass in a default value, then consider making a wrapper around this function that returns the default value when this flag returns false.
*
* Usage:
* Boolean flags: const b4b_24856_coralogix = useDebouncedFlag('b4b_24856_coralogix')
* JSON flags: const b4b_19026_experience_config = useDebouncedFlag<ExperienceConfig>(
'b4b_19026_experience_config',
defaults.b4b_19026_experience_config,
)
*/
export function useDebouncedFlag<T = boolean>(flagKey: string, defaultValue: any = false): T {
const ldClient = useLDClient()
const [flagValue, setFlagValue] = useState<T>(defaultValue)
const lastEvaluationRef = useRef<{ timestamp: number; value: T }>({
timestamp: 0,
value: defaultValue,
})
const debounceTimeoutRef = useRef<null | ReturnType<typeof setTimeout>>(null)
useEffect(() => {
if (!ldClient) return
// Clear any pending debounced evaluation
if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current)
}
// Debounce flag evaluation - only evaluate if enough time has passed
debounceTimeoutRef.current = setTimeout(() => {
const now = Date.now()
const timeSinceLastEval = now - lastEvaluationRef.current.timestamp
// Only evaluate if it's been at least 30 seconds since last evaluation
if (timeSinceLastEval > 30000) {
const value = ldClient.variation(flagKey, defaultValue) as T
lastEvaluationRef.current = { timestamp: now, value }
setFlagValue(value)
} else {
// Use cached value
setFlagValue(lastEvaluationRef.current.value)
}
}, 50) // 50ms debounce
return () => {
if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current)
}
}
}, [ldClient, flagKey, defaultValue])
return flagValue
}
Cached Method
import { useLDClient } from 'launchdarkly-react-client-sdk'
import { useEffect, useState } from 'react'
type FlagCacheEntry<T = any> = {
timestamp: number
value: T
}
// Shared cache across all hook instances to prevent duplicate LD feature events
// Key: flagKey, Value: { timestamp, value }
const flagCache = new Map<string, FlagCacheEntry>()
/**
* Get a specific flag with shared caching to prevent duplicate evaluations
*
* Uses a shared module-level cache so multiple components using the same flag
* only trigger one LaunchDarkly feature event per 30-second window.
*
* If you need to pass in a default value, then consider making a wrapper around this function that returns the default value when this flag returns false.
*
* Usage:
* Boolean flags: const b4b_24856_coralogix = useCachedFlag('b4b_24856_coralogix')
* JSON flags: const b4b_19026_experience_config = useCachedFlag<ExperienceConfig>(
'b4b_19026_experience_config',
defaults.b4b_19026_experience_config,
)
*/
export function useCachedFlag<T = boolean>(flagKey: string, defaultValue: any = false): T {
const ldClient = useLDClient()
const [flagValue, setFlagValue] = useState<T>(defaultValue)
useEffect(() => {
if (!ldClient) return
const now = Date.now()
const cached = flagCache.get(flagKey)
const timeSinceLastEval = cached ? now - cached.timestamp : Infinity
// Check shared cache first - if valid, use cached value (no LD event)
if (cached && timeSinceLastEval <= 30000) {
setFlagValue(cached.value)
} else {
// Evaluate flag and update shared cache
const value = ldClient.variation(flagKey, defaultValue) as T
flagCache.set(flagKey, { timestamp: now, value })
setFlagValue(value)
}
}, [ldClient, flagKey, defaultValue])
return flagValue
}
Describe alternatives you've considered
I've gone with the cached method in my own codebase. I'm creating this for awareness on the event issue.