Throttling Frequent Data in React
3 min read

Throttling Frequent Data in React

In this post we will make a custom hook that will update state on a set interval, even if it is passed new data 60 times per second
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.

Enjoying these posts? Subscribe for more