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 -
isThrottlingtells 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
| Property | Type | Default | Description |
|---|---|---|---|
interval | number | 500 | Minimum time in milliseconds between value updates |
trailing | boolean | true | If true, also update when the throttle period ends |
Return Value
| Property | Type | Description |
|---|---|---|
throttledValue | T | The throttled value |
isThrottling | boolean | true when in a throttle period |
Debounce vs Throttle
| Technique | Behavior | Best For |
|---|---|---|
| Debounce | Waits for pause in activity before firing | Search inputs, form validation |
| Throttle | Fires at most once per interval | Scroll 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 calculationsMouse Tracking
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
const { throttledValue } = useThrottledState(mousePos, { interval: 50 });
// Use throttledValue for smooth animationsHook 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;