Throttling Frequent Data in React

I have a somewhat unique problem. I have a function being called 60 times per second, and it never stops being called. Most examples for throttling or debouncing only invoke the function after a rest period. In the case of an ARKit session, this never ends. The function is called constantly.
In this post we will be making a small hook that will store data in a ref, and then every so often, push the data into state. This way our React tree won't render too often. Instead of 60 renders a second, we can control the frequency. At the same time, we will have the data fully up to date by the time state changes.
Let's take a look at my initial approach. This was copied across multiple files, so after this first attempt I decided to pull it into a custom hook.
export const useCameraPosition = ({debounceTime = 1000}: Props) => {
let cameraPositionRef = useRef<Camera>()
let [cameraPosition, setCameraPosition] = useState<Camera>()
useEffect(() => {
let changed: any
let timer = setInterval(() => {
changed?.()
}, debounceTime)
RNListener.addListener('cameraPositionChanged',
(cameraPosition) => {
cameraPositionRef.current = cameraPosition
changed = () => setCameraPosition(cameraPositionRef.current)
}
)
return () => {
clearInterval(timer)
RNListener.removeAllListeners('trackingStateChanged')
}
}, [debounceTime])
return cameraPosition
}
I am tracking data in a ref and state. This allows me to update a ref constantly, and then on a set interval, call the changed
function to trigger a state update.
It may look a little wonky, but it's pretty simple.
How can we pull this into a custom hook? Like so:
import {useCallback, useEffect, useRef, useState} from 'react'
type Props<DataType> = {
throttleTime?: number
initialData: DataType
}
//
type UpdateOptions = {
immediate?: boolean
}
export const useFrequentData = <DataType>({throttleTime = 2000, initialData}: Props<DataType>) => {
let dataRef = useRef<DataType>(initialData)
let changedRef = useRef<any>()
let [data, setData] = useState<DataType>(initialData)
useEffect(() => {
let timer: any
timer = setInterval(() => {
changedRef.current?.()
changedRef.current = undefined
}, throttleTime)
return () => clearInterval(timer)
}, [throttleTime])
let updateData = useCallback((dataCb: (data: DataType) => DataType, options?: UpdateOptions) => {
let data = dataCb(dataRef.current)
dataRef.current = data
if (options?.immediate) {
setData(dataRef.current)
return (changedRef.current = null)
}
changedRef.current = () => setData(dataRef.current)
}, [])
return {
data,
updateData,
}
}
This hook is complete with TS support. It creates a dataRef
to store the frequent data. The changed
function from before becomes a ref. Then we create our useState
. Whenever the throttleTime
variable changes, we will recreate the useEffect. On a set interval it will call our changedRef
.
Next, we've got our updateData
function. It is wrapped in a useCallback
so that it can be a dependency to other hooks. Whenever you call this function, it will invoke the callback passed in, and then set the dataRef
to the new value. If you pass in immediate: true
it will not wait for the next interval, but rather immediately make a state update. The last step just sets the changedRef
to the most recent setData
state change call.
Here's how you can mentally think of these steps:
- We setup our refs and state
- Our useEffect starts an interval defaulting to every 2 seconds
- We return an
updateData
function - When invoked, it will update the ref immediately, and set the latest state change callback to the
changedRef
- Every two seconds, if there is a state change needed, it will call the ref, and then clear the ref
- Next time, if
updateData
hasn't been called again, it will do nothing in the interval - Otherwise, it will continue to update the state every 2 seconds, even if we are calling
updateData
many times per second!
Here's the final result of the useCameraPosition hook:
import {Camera} from 'AR/ARTypes'
import {useEffect} from 'react'
import {EventData, RNListener} from '../RNListener'
import {useFrequentData} from './useFrequentData'
type Props = {
throttleTime?: number
}
export const useCameraPosition = ({throttleTime = 1000}: Props) => {
let {data, updateData} = useFrequentData<Camera>({
throttleTime,
initialData: {},
})
useEffect(() => {
RNListener.addListener('cameraPositionChanged',
(cameraPosition) => updateData(() => cameraPosition)
)
return () => RNListener.removeAllListeners('trackingStateChanged')
}, [throttleTime, updateData])
return data
}
It's much more readable, and the logic is only related to getting the camera position, instead of the state throttling logic.
Here's one other example of it in use. This time it's a tracking state listener for ARKit:
export const useTrackingState = ({throttleTime = 3000}: Props) => {
let {data, updateData} = useFrequentData<TrackingState>({
initialData: 'not available',
throttleTime,
})
useEffect(() => {
RNListener.addListener('trackingStateChanged',
(trackingState) => updateData(() => trackingState)
)
return () => RNListener.removeAllListeners('trackingStateChanged')
}, [updateData])
return data
}
Conclusion
First off, I have no idea if this is "correct." But it feels like a pretty clean way to handle it. Storing frequent data in a ref, and on a slower interval, throwing it into state.
I write these articles to show my approach, and many times I'll find a tweet from somebody giving an even simpler way to do it! Feel free to take my hook above for your own uses, or tweet me and tell me a better way.