Fiber UI LogoFiberUI

useThrottledState

A hook for throttling state values

A React hook that throttles a value. The throttled value updates at most once per interval, regardless of how often the source value changes. Perfect for high-frequency events like mouse movements, scroll positions, and real-time sensor data.

Source Code

View the full hook implementation in the Hook Source Code section below.

Related Hook

Need to throttle a function instead of a value? See useThrottledCallback.

Features

  • Value Throttling - Limits how often a value can update
  • Trailing Edge - Optionally capture the final value when throttle period ends
  • Throttling State - isThrottling tells you when in a throttle period
  • SSR Safe - No issues with server-side rendering

Basic Usage

Track mouse position with throttling. Notice how many fewer updates the throttled value receives:

Immediate Position

x: 0, y: 0

Updates: 0

Throttled (500ms)

x: 0, y: 0

Updates: 0

Move your mouse around. The throttled value updates at most every 500ms.

"use client";

import { useState, useEffect } from "react";
import { useThrottledState } from "@repo/hooks/utility/use-throttled-state";

/* BASIC USAGE - Mouse Position Tracker */
export const Example1 = () => {
    const INTERVAL = 500;
    const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
    const { throttledValue, isThrottling } = useThrottledState(mousePos, {
        interval: INTERVAL,
    });
    const [updateCount, setUpdateCount] = useState(0);
    const [throttledUpdateCount, setThrottledUpdateCount] = useState(0);

    useEffect(() => {
        const handleMouseMove = (e: MouseEvent) => {
            setMousePos({ x: e.clientX, y: e.clientY });
            setUpdateCount((c) => c + 1);
        };

        window.addEventListener("mousemove", handleMouseMove);
        return () => window.removeEventListener("mousemove", handleMouseMove);
    }, []);

    useEffect(() => {
        setThrottledUpdateCount((c) => c + 1);
    }, [throttledValue]);

    return (
        <div className="flex flex-col gap-4">
            <div className="grid grid-cols-2 gap-4">
                <div className="bg-muted/50 rounded-md p-3">
                    <p className="text-muted-foreground mb-1 text-xs font-medium">
                        Immediate Position
                    </p>
                    <p className="font-mono text-sm">
                        x: {mousePos.x}, y: {mousePos.y}
                    </p>
                    <p className="text-muted-foreground mt-2 text-xs">
                        Updates:{" "}
                        <span className="font-medium">{updateCount}</span>
                    </p>
                </div>
                <div className="rounded-md border border-purple-500/50 bg-purple-500/10 p-3">
                    <div className="mb-1 flex items-center gap-2">
                        <p className="text-xs font-medium text-purple-600 dark:text-purple-400">
                            Throttled ({INTERVAL}ms)
                        </p>
                        {isThrottling && (
                            <span className="h-2 w-2 animate-pulse rounded-full bg-purple-500" />
                        )}
                    </div>
                    <p className="font-mono text-sm">
                        x: {throttledValue.x}, y: {throttledValue.y}
                    </p>
                    <p className="text-muted-foreground mt-2 text-xs">
                        Updates:{" "}
                        <span className="font-medium text-purple-600 dark:text-purple-400">
                            {throttledUpdateCount}
                        </span>
                    </p>
                </div>
            </div>

            <p className="text-muted-foreground text-xs">
                Move your mouse around. The throttled value updates at most
                every {INTERVAL}ms.
            </p>
        </div>
    );
};

Slider with Trailing Edge

With trailing: true, the throttled value also captures the final value when you stop:

Immediate

50

Throttled (500ms)

50

The throttled value updates at most every 500ms. With trailing: true, it also captures the final value when you stop.

"use client";

import { useState } from "react";
import { useThrottledState } from "@repo/hooks/utility/use-throttled-state";

/* SLIDER INPUT - Throttled Value Updates */
export const Example2 = () => {
    const INTERVAL = 500;
    const [sliderValue, setSliderValue] = useState(50);
    const { throttledValue, isThrottling } = useThrottledState(sliderValue, {
        interval: INTERVAL,
        trailing: true,
    });

    return (
        <div className="flex flex-col gap-4">
            <div>
                <label className="text-muted-foreground mb-2 block text-sm font-medium">
                    Drag the slider rapidly
                </label>
                <input
                    type="range"
                    min="0"
                    max="100"
                    value={sliderValue}
                    onChange={(e) => setSliderValue(Number(e.target.value))}
                    className="w-full"
                />
            </div>

            <div className="grid grid-cols-2 gap-4">
                <div className="bg-muted/50 rounded-md p-4 text-center">
                    <p className="text-muted-foreground mb-1 text-xs font-medium">
                        Immediate
                    </p>
                    <p className="text-3xl font-bold">{sliderValue}</p>
                </div>
                <div className="relative rounded-md border border-purple-500/50 bg-purple-500/10 p-4 text-center">
                    <p className="mb-1 text-xs font-medium text-purple-600 dark:text-purple-400">
                        Throttled ({INTERVAL}ms)
                    </p>
                    <p className="text-3xl font-bold text-purple-600 dark:text-purple-400">
                        {throttledValue}
                    </p>
                    {isThrottling && (
                        <span className="absolute right-2 top-2 h-2 w-2 animate-pulse rounded-full bg-purple-500" />
                    )}
                </div>
            </div>

            <p className="text-muted-foreground text-xs">
                The throttled value updates at most every {INTERVAL}ms. With{" "}
                <code className="bg-muted rounded px-1">trailing: true</code>,
                it also captures the final value when you stop.
            </p>
        </div>
    );
};

API Reference

Hook Signature

function useThrottledState<T>(
    value: T,
    options?: UseThrottledStateOptions,
): UseThrottledStateReturn<T>;

Options

PropertyTypeDefaultDescription
intervalnumber500Minimum time in milliseconds between value updates
trailingbooleantrueIf true, also update when the throttle period ends

Return Value

PropertyTypeDescription
throttledValueTThe throttled value
isThrottlingbooleantrue when in a throttle period

Debounce vs Throttle

TechniqueBehaviorBest For
DebounceWaits for pause in activity before firingSearch inputs, form validation
ThrottleFires at most once per intervalScroll handlers, resize events

Visual Comparison

Input:    ──●●●●●●●●●──────●●●●●●●●●──

Debounce: ──────────●──────────────●──  (fires after pause)
Throttle: ──●───●───●──────●───●───●──  (fires at intervals)

Common Patterns

Scroll Position

const [scrollY, setScrollY] = useState(0);
const { throttledValue } = useThrottledState(scrollY, { interval: 100 });

// Use throttledValue for expensive calculations

Mouse Tracking

const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
const { throttledValue } = useThrottledState(mousePos, { interval: 50 });

// Use throttledValue for smooth animations

Hook Source Code

import { useState, useEffect, useCallback, useRef } from "react";

/**
 * Options for the useThrottledState hook
 */
export interface UseThrottledStateOptions {
    /** Interval in milliseconds between updates (default: 500) */
    interval?: number;
    /** If true, also update on the trailing edge after throttle period ends */
    trailing?: boolean;
}

/**
 * Return type for useThrottledState hook
 */
export interface UseThrottledStateReturn<T> {
    /** The throttled value */
    throttledValue: T;
    /** Whether currently in a throttle period */
    isThrottling: boolean;
}

/**
 * A React hook that throttles a value. The throttled value updates at most
 * once per interval, regardless of how often the source value changes.
 *
 * @param value - The value to throttle
 * @param options - Configuration options
 * @returns UseThrottledStateReturn object with throttled value
 *
 * @example
 * ```tsx
 * const [scrollY, setScrollY] = useState(0);
 * const { throttledValue } = useThrottledState(scrollY, { interval: 100 });
 *
 * // throttledValue updates at most every 100ms, even if scrollY changes rapidly
 * ```
 */
export function useThrottledState<T>(
    value: T,
    options: UseThrottledStateOptions = {},
): UseThrottledStateReturn<T> {
    const { interval = 500, trailing = true } = options;

    const [throttledValue, setThrottledValue] = useState<T>(value);
    const [isThrottling, setIsThrottling] = useState(false);
    const lastExecuted = useRef<number>(0);
    const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
    const latestValue = useRef<T>(value);

    // Keep track of the latest value
    latestValue.current = value;

    useEffect(() => {
        const now = Date.now();
        const timeSinceLastExecution = now - lastExecuted.current;

        // If enough time has passed, update immediately
        if (timeSinceLastExecution >= interval) {
            setThrottledValue(value);
            lastExecuted.current = now;
            setIsThrottling(false);
        } else {
            // Otherwise, schedule an update for the trailing edge
            setIsThrottling(true);

            if (trailing) {
                // Clear any existing timeout
                if (timeoutRef.current) {
                    clearTimeout(timeoutRef.current);
                }

                // Schedule update at the end of the interval
                const timeRemaining = interval - timeSinceLastExecution;
                timeoutRef.current = setTimeout(() => {
                    setThrottledValue(latestValue.current);
                    lastExecuted.current = Date.now();
                    setIsThrottling(false);
                    timeoutRef.current = null;
                }, timeRemaining);
            }
        }

        return () => {
            if (timeoutRef.current) {
                clearTimeout(timeoutRef.current);
            }
        };
    }, [value, interval, trailing]);

    // Cleanup on unmount
    useEffect(() => {
        return () => {
            if (timeoutRef.current) {
                clearTimeout(timeoutRef.current);
            }
        };
    }, []);

    return {
        throttledValue,
        isThrottling,
    };
}

export default useThrottledState;