Web Development - Frontend - React

Build and Use a Custom Throttling Hook in React

Learn how to create a reusable useThrottle hook in React to control the frequency of state updates and improve app performance.

Illustration of React hooks concept with throttle icon overlay

How to Build and Use a Custom Throttling Hook in React

When working with React applications, performance optimization is key - especially when dealing with events that trigger frequent state updates (like scrolling, resizing, or typing). Without some control, these updates can overwhelm your app, causing unnecessary re-renders and laggy UI.

This is where throttling comes into play. Throttling ensures that a function or state update is executed at most once during a specified time window, no matter how many times the triggering event occurs. In this article, we’ll build a reusable useThrottle hook in React and learn how to integrate it in your projects.

Why Throttling Matters

Imagine attaching an event listener to window.scroll that updates some state on every pixel scroll. Without throttling, React will attempt to re-render on every scroll event - which could fire dozens of times per second. By throttling, we reduce the frequency of updates while still keeping the UI responsive.

Creating the useThrottle Hook

Here’s the full implementation of our custom hook:

useThrottle.tsx
import { useEffect, useRef, useState } from "react";

export function useThrottle<T>(value: T, timeout: number = 1000) {
  const [throttled, setThrottled] = useState<T>();
  const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const latestValue = useRef(value);
  const hasInitialized = useRef(false);

  useEffect(() => {
    latestValue.current = value;

    if (!hasInitialized.current) {
      setThrottled(value); // Set the throttled value immediately
      hasInitialized.current = true;
      return;
    }

    // If no timer running, start throttle window
    if (timeoutRef.current === null) {
      timeoutRef.current = setTimeout(() => {
        timeoutRef.current = null; // window ends; allow a new schedule next time
        setThrottled(latestValue.current);
      }, timeout);
    }
  }, [value]);

  useEffect(() => {
    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
        timeoutRef.current = null;
      }
    };
  }, []);

  return throttled;
}

How It Works

  • useState - Stores the throttled value
  • useRef - Keeps track of:
    • The latest value (latestValue)
    • The active timeout (timeoutRef)
    • Whether the hook has initialized (hasInitialized)
  • First Render - Immediately sets the initial throttled value.
  • Subsequent Updates - If there’s no active timeout, starts one.Once the timeout expires, the latest value is applied.
  • Cleanup - Clears any active timeout when the component unmounts.


Example

User types "hello"

// User types "h" - First character
hasInitialized = false
setThrottled("h")  // Show immediately
→ hasInitialized = true
→ No timer started yet

// User types "e" - Second character  
timeoutRef.current === null  // No window open
→ Start new window (setTimeout)
→ timeoutRef.current = timerID  // "Window is now open!"
→ Captured value at window opening = "he"
→ Continue accepting input but don't display it

// User types "l", "l", "o" - More characters
→ latestValue.current keeps updating: "hel""hell""hello"
→ timeoutRef.current !== null  // Window still open
→ Do nothing, keep waiting

// 1000ms later - Window closes
setTimeout callback runs:
→ timeoutRef.current = null  // "Window is closed, reference cleared!"
setThrottled("hello")  // Show the final value from this window