DEV Community

Cover image for React Hooks: useThrottledValue and useThrottledFunction
Dennis Persson
Dennis Persson Subscriber

Posted on • Originally published at perssondennis.com

React Hooks: useThrottledValue and useThrottledFunction

This article explains the new React 18 hooks useDeferredValue and useTransition and compares them to throttle and debounce functions. It also presents two similar custom hooks, useThrottledValue and useThrottledFunction, which can be used to throttle a function or value change when the React hooks isn't sufficient.

In This Article

useThrottledFunction Hook

useThrottledFunction is a hook you can use when you need to prevent a function from running too often. It works similar to React 18's useTransition hook but has a slightly different use case. I will provide the code for it later, but before doing that, we will look at the new hooks in React 18, useTransition and useDeferredValue. We will also look at what throttling and debounce actually means and how they differ from each other.

useThrottledValue Hook

useThrottledValue is a hook similar to useThrottledFunction. The difference is that useThrottledValue simply throttles a value change instead of a function call. The code for it will be provided later in this article.

useDeferredValue and useTransition

useDeferredValue is a new hook available in React 18. I recommend you to read this article about why they added it here, but in short, it's because they want to give us an opportunity to postpone an update of a value until more important code has run. It essentially burns down to deferring code execution so more prioritized UI updates can be render quicker.

To use useDeferredValue, simply pass a value to it and it will automatically be deferred if necessary.

import { useDeferredValue } from 'react'

const UseDeferredValueExample = ({ items }) => {
  const deferredItems = useDeferredValue(items)

  return (<ul>
    {deferredItems.map((item) => <li key={item.id}>{item.text}</li>)}
  </ul>)
}

export default UseDeferredValueExample
Enter fullscreen mode Exit fullscreen mode

With React 18, came also a similar hook called useTransition. useTransition defers an update just like useDeferredValue does, but instead of merely updating a value it allows to customize the state update more granularly.

import { useState, useTransition } from 'react'

const UseTransitionExample = ({ text }) => {
  const [isPending, startTransition] = useTransition()
  const [shouldShow, setShouldShow] = useState(false)

  const showEventually = () => {
    startTransition(() => {
      setShouldShow(true)
    })
  }

  return (<div>
    <button onClick={showEventually}>Show Text</button>
    {isPending && <p>Text will show soon!</p>}
    {shouldShow && <p>{text}</p>}
  </div>)
}

export default UseTransitionExample
Enter fullscreen mode Exit fullscreen mode

What is Throttle and Debounce?

Throttle and debounce are two terms that often are mixed up together. The purpose of them both are to prevent a function from running too often. A similar use case is not to update a value for a certain amount of time.

A throttle and a debounce both take a callback function as an argument and a time interval that decides how often the callback function should be allowed to be invoked. The return value is a new function that is the throttled/debounced callback function.

The difference between them is that a throttle will run multiple times while a debounce only will run once. When a function is being throttled for X seconds, it will at maximum run once every X second, regardless of how many times the function is called upon.

In other words, a throttle allows the function to run every X second, but will only run if it has been invoked one or more times during those X seconds.

Unlike a throttle, the time interval passed to a debounce will not make a function run periodically. A time interval passed to a debounce function can be seen as a cooldown time for the callback function that reset itself every time someone tries to trigger it.

A debounce is like a stubborn child that has made up its mind not to eat the food until the parents have stopped nagging about it for at least X seconds. As soon as the parents have been silent for X seconds, the child eats its vegetables.

Stubborn child debounce meme
Mama' needs to learn how debounce works

The picture below depicts the usage of a throttle and a debounce. The lines labeled regular represents when the function is being called. You can see that the stubborn debounce only invokes the function as soon as the function has stopped being invoked while the throttled function is invoked periodically with a minimum time between every invocation. You can try it yourself at this site.

Debounce and throttle explained
Throttle will trigger periodically while debounce will trigger when invocation has stopped

Note that throttle and debounce functions often comes with settings. A debounce function can usually be configured to either run before or after the provided time interval. For the stubborn child, that would mean it would eat its vegetables the first time the parents asked, but would not eat another piece of it until the parents had been quiet for X seconds.

Throttle vs New React 18 Hooks

As described above, both a throttle and the new React hooks can be used to defer a function call or an update of a value. There is a slight difference between throttling and using the new React hooks though. useTranstition and useDeferredValue will update the state as soon as React has time for it. That's not the case with a throttle.

A throttle will wait for a specified amount of time before regardless of whether it is necessary for performance or not. This means that useDeferredValue and useTransition will be able to update the state sooner since they don't have to postpone it if it isn't really necessary.

A common reason to use a throttle is to prevent overheating an application with more function calls than the computer can handle. Such overheating can often be prevented or mitigated with the new useDeferredValue or useTransition hooks, since those hooks can detect when React has time to update the state. For that reason, many people claim that useDeferredValue and useTransition hooks removes the need for manually use a throttle or debounce.

The truth is, overheating an application isn't the only use case of a throttle or a debounce. Another use case is to prevent multiple invocations of a function in use cases where it could hurt the application in some way.

Maybe a backend service would return a 429 HTTP error code when too many requests are sent, or maybe a resource intensive or expensive job would run to often without a throttle. In those cases, it's still necessary to use a throttle or debounce. There are often other solutions for such problems, but React's new hooks isn't what you search for in those cases.

429 Http Error Code
Totally legit HTTP error code

When Not To Use useThrottledFunction or useThrottledValue

As described above, there are some scenarios where you should use useDeferredValue or useTransition rather than using the useThrottledValue or useThrottledFunction hook. Here are some examples of when to prefer the built-in React 18 hooks.

  1. When the reason to use the hook is to let more important code or UI updates run first.
  2. When the reason to use the hook is to optimize performance when a value updates a few times.

Well, the first use case is obvious. That's exactly what React's new hooks are supposed to do. To let you prioritize some updates as more important than other.

The second use case is maybe a bit more obvious, why wouldn't we throttle a function to optimize performance? The thing is, a lot of developers try to micro-optimize their code. Preventing a function from being called a few times is most often not a problem for performance. Failing to design a good frontend architecture, misusing the framework of use, or neglecting the importance of managing states and data flows correctly are big issue though. Handle those things properly and you won't need to micro-optimize your code on a function call level.

If you still would judge your use case as a case where it's important to micro-optimize, useDeferredValue and useTransition can help you out with that. They will help you deferring the updates until React feels there is time for it.

When To Use useThrottledFunction and useThrottledValue

Now when we know when not to use the hooks, we will look at when to use them.

  1. When the hook triggers a function that could be harmful to any other service or code.
  2. When the function or value change triggers a resource intensive or expensive job.
  3. When the reason to use the hook is to optimize performance when a value updates a lot of times.

We did previously touch upon the two first scenarios under the Throttle vs New React 18 Hooks heading. We mentioned that a network service could respond with a 429 HTTP error code. Other scenarios could be that we would want to prevent a user from spamming a functionality that is allowed to be triggered multiple times.

Regarding the second use case, when an intensive job is triggered. A typical scenario could be when the value is listed as a dependency to a useMemo hook. The useMemo hook is usually used to prevent heavy calculations from running multiple times. Using a throttle to prevent the memo from updating too many times could therefore be a valid use case.

The third use case is almost the same as the second scenario of when not to use the hooks. The argument not to use the hooks was because it wasn't important to micro-optimize code on a function level. However, there is of course a threshold for when it would be necessary. An example would be when listening on mouse movement.

Remember the picture describing throttle and debounce? That picture was actually captured from a mouse movement. In that picture (duplicated here below), we can see that the debounce and throttle prevents a huge amount of function calls. If the function call is fairly heavy to run, it could be a good idea to throttle or debounce it.

Debounce and throttle explained
A debounce or throttle can prevent a lot of unnecessary function calls when it is triggered by mouse movement

useThrottledValue Implementation

A lot of text above, but finally we can look at the first hook implementation! Let's start with useThrottledValue, there's both a JavaScript and a TypeScript implementation of it.

The hook takes a single argument, an object containing a value and optionally throttleMs. The optional throttleMs is the throttle time for how often the value should be allowed to update. If left out, we have a default time at 800 ms (DEFAULT_THROTTLE_MS).

The hook consists of a single useEffect that will trigger as soon as a new value is passed in to the hook. If the hook hasn't updated the value for throttleMs milliseconds, it will update the value and save the time for the last update.

If the value updates more times within throttleMs milliseconds, it will set a timeout that will update the value as soon as it is time for it to update. To prevent memory leaks for the timers, the timeout is cleaned up every time the useEffect runs.

JavaScript Implementation

import {
  useCallback, useEffect, useRef, useState,
} from 'react'

const DEFAULT_THROTTLE_MS = 3000

const getRemainingTime = (lastTriggeredTime, throttleMs) => {
  const elapsedTime = Date.now() - lastTriggeredTime
  const remainingTime = throttleMs - elapsedTime

  return (remainingTime < 0) ? 0 : remainingTime
}

const useThrottledValue = ({
  value,
  throttleMs = DEFAULT_THROTTLE_MS,
}) => {
  const [throttledValue, setThrottledValue] = useState(value)
  const lastTriggered = useRef(Date.now())
  const timeoutRef = useRef(null)

  const cancel = useCallback(() => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current)
      timeoutRef.current = null
    }
  }, [])

  useEffect(() => {
    let remainingTime = getRemainingTime(lastTriggered.current, throttleMs)

    if (remainingTime === 0) {
      lastTriggered.current = Date.now()
      setThrottledValue(value)
      cancel()
    } else if (!timeoutRef.current) {
      timeoutRef.current = setTimeout(() => {
        remainingTime = getRemainingTime(lastTriggered.current, throttleMs)

        if (remainingTime === 0) {
          lastTriggered.current = Date.now()
          setThrottledValue(value)
          cancel()
        }
      }, remainingTime)
    }

    return cancel
  }, [cancel, throttleMs, value])

  return throttledValue
}

export default useThrottledValue
Enter fullscreen mode Exit fullscreen mode

TypeScript Implementation

import {
  useCallback, useEffect, useRef, useState,
} from 'react'

const DEFAULT_THROTTLE_MS = 3000

const getRemainingTime = (lastTriggeredTime: number, throttleMs: number) => {
  const elapsedTime = Date.now() - lastTriggeredTime
  const remainingTime = throttleMs - elapsedTime

  return (remainingTime < 0) ? 0 : remainingTime
}

export type useThrottledValueProps<T> = {
  value: T
  throttleMs?: number
}

const useThrottledValue = <T, >({
  value,
  throttleMs = DEFAULT_THROTTLE_MS,
}: useThrottledValueProps<T>) => {
  const [throttledValue, setThrottledValue] = useState<T>(value)
  const lastTriggered = useRef<number>(Date.now())
  const timeoutRef = useRef<NodeJS.Timeout|null>(null)

  const cancel = useCallback(() => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current)
      timeoutRef.current = null
    }
  }, [])

  useEffect(() => {
    let remainingTime = getRemainingTime(lastTriggered.current, throttleMs)

    if (remainingTime === 0) {
      lastTriggered.current = Date.now()
      setThrottledValue(value)
      cancel()
    } else if (!timeoutRef.current) {
      timeoutRef.current = setTimeout(() => {
        remainingTime = getRemainingTime(lastTriggered.current, throttleMs)

        if (remainingTime === 0) {
          lastTriggered.current = Date.now()
          setThrottledValue(value)
          cancel()
        }
      }, remainingTime)
    }

    return cancel
  }, [cancel, throttleMs, value])

  return throttledValue
}

export default useThrottledValue
Enter fullscreen mode Exit fullscreen mode

useThrottledFunction Implementation

The next hook, useThrottledFunction, works very similar to useThrottledValue and the implementations is nearly identical. The passed in value argument has been replaced with a callbackFn, which is the function that should be throttled.

The function returns an object. The object contains throttledFn, which is a throttled version of the passed in callbackFn. It also returns a cancel function, which can be called whenever the throttle timer's needs to be stopped.

JavaScript Implementation

import { useCallback, useEffect, useRef } from 'react'

const DEFAULT_THROTTLE_MS = 800

const getRemainingTime = (lastTriggeredTime, throttleMs) => {
  const elapsedTime = Date.now() - lastTriggeredTime
  const remainingTime = throttleMs - elapsedTime

  return (remainingTime < 0) ? 0 : remainingTime
}

const useThrottledFunction = ({
  callbackFn,
  throttleMs = DEFAULT_THROTTLE_MS,
}) => {
  const lastTriggered = useRef(Date.now())
  const timeoutRef = useRef(null)

  const cancel = useCallback(() => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current)
      timeoutRef.current = null
    }
  }, [])

  const throttledFn = useCallback((args) => {
    let remainingTime = getRemainingTime(lastTriggered.current, throttleMs)

    if (remainingTime === 0) {
      lastTriggered.current = Date.now()
      callbackFn(args)
      cancel()
    } else if (!timeoutRef.current) {
      timeoutRef.current = setTimeout(() => {
        remainingTime = getRemainingTime(lastTriggered.current, throttleMs)

        if (remainingTime === 0) {
          lastTriggered.current = Date.now()
          callbackFn(args)
          cancel()
        }
      }, remainingTime)
    }
  }, [callbackFn, cancel])

  useEffect(() => cancel, [cancel])

  return { cancel, throttledFn }
}

export default useThrottledFunction
Enter fullscreen mode Exit fullscreen mode

TypeScript Implementation

import { useCallback, useEffect, useRef } from 'react'

const DEFAULT_THROTTLE_MS = 800

const getRemainingTime = (lastTriggeredTime: number, throttleMs: number) => {
  const elapsedTime = Date.now() - lastTriggeredTime
  const remainingTime = throttleMs - elapsedTime

  return (remainingTime < 0) ? 0 : remainingTime
}

export type useThrottledFunctionProps = {
    callbackFn: <T, >(args?: T) => any
    throttleMs?: number
}

const useThrottledFunction = ({
  callbackFn,
  throttleMs = DEFAULT_THROTTLE_MS,
}: useThrottledFunctionProps) => {
  const lastTriggered = useRef<number>(Date.now())
  const timeoutRef = useRef<NodeJS.Timeout|null>(null)

  const cancel = useCallback(() => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current)
      timeoutRef.current = null
    }
  }, [])

  const throttledFn = useCallback(<T, >(args?: T) => {
    let remainingTime = getRemainingTime(lastTriggered.current, throttleMs)

    if (remainingTime === 0) {
      lastTriggered.current = Date.now()
      callbackFn(args)
      cancel()
    } else if (!timeoutRef.current) {
      timeoutRef.current = setTimeout(() => {
        remainingTime = getRemainingTime(lastTriggered.current, throttleMs)

        if (remainingTime === 0) {
          lastTriggered.current = Date.now()
          callbackFn(args)
          cancel()
        }
      }, remainingTime)
    }
  }, [callbackFn, cancel])

  useEffect(() => cancel, [cancel])

  return { cancel, throttledFn }
}

export default useThrottledFunction
Enter fullscreen mode Exit fullscreen mode

Examples

The code below shows how the useThrottledValue may be used. When a button is clicked, a value state variable is updated. After the user have clicked the button, a heavy calculation is done.

To prevent the heavy calculation from running too many times if the user spams the button, we use this hook to throttle the recalculation of the memorized value. You have a CodeSandbox of it here to try it, and if you want to clone, star or watch it on GitHub you have the repository for it here.

import { useMemo, useState } from "react";
import useThrottledValue from "./useThrottledValue";

// Note that this will be called twice with React StrictMode because
// it's a callback provided to a useMemo.
const performHeavyCalculation = (value) => {
  console.log("Heavy calculation for value:", value);
  return value;
};

export default function App() {
  const [value, setValue] = useState(0);
  const throttledValue = useThrottledValue({ value, throttleMs: 5000 });

  const memoizedValue = useMemo(() => {
    return performHeavyCalculation(throttledValue);
  }, [throttledValue]);

  return (
    <div>
      <button onClick={() => setValue(value + 1)}>Increment value</button>
      <p>Calculates a new value every fifth second.</p>
      <p>Value: {value}</p>
      <p>Last caculated result: {memoizedValue}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Below code depicts a use case of useThrottledFunction. In this case, a function performHeavyCalculation is throttled to prevent it from being called for every fired scroll event. CodeSandbox to try it code. GitHub repository here.

import { useCallback, useEffect } from "react";
import useThrottledFunction from "./useThrottledFunction";

const performHeavyCalculation = () => {
  console.log("Heavy calculation");
};

export default function App() {
  const callbackFnToThrottle = useCallback(() => {
    performHeavyCalculation();
  }, []);

  const { throttledFn } = useThrottledFunction({
    callbackFn: callbackFnToThrottle,
    throttleMs: 5000
  });

  useEffect(() => {
    window.addEventListener("scroll", throttledFn);

    return () => {
      window.removeEventListener("scroll", throttledFn);
    };
  }, [throttledFn]);

  return (
    <div>
      <p>Scroll and look in console.</p>
      <p>Code uses a throttle of 5 seconds.</p>
      <div style={{ height: "4000px" }} />
      <p>End of scroll...</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Note that there are two things to note for the code above. First thing is that the callback function called callbackFnToThrottle isn't necessary in this case. It would be possible to directly pass the performHeavyCalculation function to the callbackFn argument attribute. The callback is only added for demonstration.

The second thing to mention is that this use case isn't necessarily optimal. When looking at scroll events, there are many times often better solutions to apply. Using the Intersection Observer API could be a better solution than listening for scroll events if the use case is to detect if an element is visible on screen.

Long article meme
I rather wonder why you are still reading it?

Summary

useThrottledValue and useThrottledFunction are hooks you can use in a few use cases.

  1. To throttle a function call that could be harmful to run many times.
  2. To throttle a function call or value change that triggers a resource intensive or expensive job.
  3. To optmize performance when a value is being updated lot of times.

React 18 also introduced two new hooks, useDeferredValue and useTransition. These hooks can be used to run code with lower priority, to allow for more important code to run first. In some cases, it is better to use one of those hooks instead. That is the case when:

  1. When the reason to use the hook is to let more important code or UI updates run first.
  2. When the reason to use the hook is to optimize performance when a value updates a few times.

This article also described the difference between throttle and debounce. While both are used to avoid running code too often, they differ in how many times they will invoke the function. A throttle will periodically invoke the function dependent on a throttle time while a debounce will run the function only once, either at the start of or at the end of a series of invocations.

Where To Learn More

If you liked this article, you are maybe interested to read similar articles. You can do that here on DEV or by checking out my website. I'm also active on Instagram where I post more programmer memes. Make sure to follow me if you find it interesting!

 

Top comments (1)

Collapse
 
perssondennis profile image
Dennis Persson

Thanks for reading! Let me know if I should post more articles like this. It's way easier to know what people enjoy if they speak out. Add a comment, a ❤️ or a 🦄, or save for a future reminder to read again. For more articles, follow me on Instagram or here on DEV.