CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-use-debounce

React hooks library for debouncing and throttling functionality with small footprint and comprehensive control features

Pending
Overview
Eval results
Files

throttling.mddocs/

Throttling

Throttling with useThrottledCallback creates functions that execute at most once per specified interval. Unlike debouncing, which delays execution until activity stops, throttling ensures regular execution during continuous activity. This is ideal for high-frequency events like scrolling, resizing, mouse movement, or animation frames.

Capabilities

useThrottledCallback Hook

Creates a throttled version of a callback function that executes at most once per wait interval.

/**
 * Creates a throttled function that only invokes func at most once per
 * every wait milliseconds (or once per browser frame).
 *
 * @param func - The function to throttle
 * @param wait - The number of milliseconds to throttle invocations to
 * @param options - Optional configuration for leading/trailing execution
 * @returns Throttled function with control methods
 */
function useThrottledCallback<T extends (...args: any) => ReturnType<T>>(
  func: T,
  wait: number,
  options?: CallOptions
): DebouncedState<T>;

Parameters:

  • func: T - The function to throttle
  • wait: number - The throttle interval in milliseconds
  • options?: CallOptions - Configuration object with:
    • leading?: boolean - If true, invokes the function on the leading edge (default: true)
    • trailing?: boolean - If true, invokes the function on the trailing edge (default: true)

Returns:

  • DebouncedState<T> - Throttled function with control methods (cancel, flush, isPending)

Usage Examples:

import React, { useState, useEffect } from 'react';
import { useThrottledCallback } from 'use-debounce';

// Scroll position tracking
function ScrollTracker() {
  const [scrollY, setScrollY] = useState(0);

  const throttledScrollHandler = useThrottledCallback(
    () => {
      setScrollY(window.pageYOffset);
    },
    100 // Update scroll position at most once every 100ms
  );

  useEffect(() => {
    window.addEventListener('scroll', throttledScrollHandler);
    return () => {
      window.removeEventListener('scroll', throttledScrollHandler);
    };
  }, [throttledScrollHandler]);

  return (
    <div style={{ position: 'fixed', top: 0, left: 0, background: 'white' }}>
      Scroll Y: {scrollY}px
    </div>
  );
}

// Window resize handling
function ResponsiveComponent() {
  const [windowSize, setWindowSize] = useState({
    width: typeof window !== 'undefined' ? window.innerWidth : 0,
    height: typeof window !== 'undefined' ? window.innerHeight : 0,
  });

  const throttledResizeHandler = useThrottledCallback(
    () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    },
    200 // Recalculate layout at most once every 200ms
  );

  useEffect(() => {
    window.addEventListener('resize', throttledResizeHandler);
    return () => {
      window.removeEventListener('resize', throttledResizeHandler);
    };
  }, [throttledResizeHandler]);

  return (
    <div>
      Window size: {windowSize.width} x {windowSize.height}
    </div>
  );
}

// Mouse movement tracking
function MouseTracker() {
  const [mousePos, setMousePos] = useState({ x: 0, y: 0 });

  const throttledMouseMove = useThrottledCallback(
    (event: MouseEvent) => {
      setMousePos({ x: event.clientX, y: event.clientY });
    },
    50 // Update mouse position at most once every 50ms
  );

  useEffect(() => {
    const handleMouseMove = (e: MouseEvent) => throttledMouseMove(e);
    document.addEventListener('mousemove', handleMouseMove);
    return () => {
      document.removeEventListener('mousemove', handleMouseMove);
    };
  }, [throttledMouseMove]);

  return (
    <div>
      Mouse: ({mousePos.x}, {mousePos.y})
    </div>
  );
}

// API requests with rate limiting
function RateLimitedRequests() {
  const [data, setData] = useState(null);
  const [requestCount, setRequestCount] = useState(0);

  const throttledFetch = useThrottledCallback(
    async (endpoint: string) => {
      setRequestCount(prev => prev + 1);
      const response = await fetch(endpoint);
      const result = await response.json();
      setData(result);
    },
    5000 // Maximum one request every 5 seconds
  );

  return (
    <div>
      <button onClick={() => throttledFetch('/api/data')}>
        Fetch Data (Throttled)
      </button>
      <p>Requests made: {requestCount}</p>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

// Leading edge only (immediate execution, then throttled)
function ImmediateAction() {
  const [count, setCount] = useState(0);

  const throttledIncrement = useThrottledCallback(
    () => {
      setCount(prev => prev + 1);
    },
    1000,
    { leading: true, trailing: false }
  );

  return (
    <div>
      <button onClick={throttledIncrement}>
        Increment (Immediate, then throttled)
      </button>
      <p>Count: {count}</p>
    </div>
  );
}

// Trailing edge only (throttled execution only)
function TrailingAction() {
  const [lastAction, setLastAction] = useState('');

  const throttledLog = useThrottledCallback(
    (action: string) => {
      setLastAction(`${action} at ${new Date().toLocaleTimeString()}`);
    },
    2000,
    { leading: false, trailing: true }
  );

  return (
    <div>
      <button onClick={() => throttledLog('Button clicked')}>
        Click Me (Trailing only)
      </button>
      <p>Last action: {lastAction}</p>
    </div>
  );
}

// Animation frame throttling
function AnimationExample() {
  const [rotation, setRotation] = useState(0);

  const throttledRotate = useThrottledCallback(
    () => {
      setRotation(prev => (prev + 10) % 360);
    },
    16 // ~60fps throttling
  );

  useEffect(() => {
    const interval = setInterval(throttledRotate, 10);
    return () => clearInterval(interval);
  }, [throttledRotate]);

  return (
    <div
      style={{
        width: 100,
        height: 100,
        background: 'blue',
        transform: `rotate(${rotation}deg)`,
        transition: 'transform 0.1s ease',
      }}
    />
  );
}

// Search with throttled API calls
function ThrottledSearch() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isSearching, setIsSearching] = useState(false);

  const throttledSearch = useThrottledCallback(
    async (searchTerm: string) => {
      if (!searchTerm.trim()) {
        setResults([]);
        return;
      }

      setIsSearching(true);
      try {
        const response = await fetch(`/api/search?q=${searchTerm}`);
        const data = await response.json();
        setResults(data.results);
      } catch (error) {
        console.error('Search failed:', error);
      } finally {
        setIsSearching(false);
      }
    },
    800 // Maximum one search every 800ms
  );

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    setQuery(value);
    throttledSearch(value);
  };

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={handleInputChange}
        placeholder="Search (throttled)..."
      />
      {isSearching && <p>Searching...</p>}
      <ul>
        {results.map((result: any) => (
          <li key={result.id}>{result.title}</li>
        ))}
      </ul>
    </div>
  );
}

Control Functions Usage

function ThrottleWithControls() {
  const [value, setValue] = useState(0);

  const throttledUpdate = useThrottledCallback(
    (newValue: number) => {
      setValue(newValue);
    },
    1000
  );

  return (
    <div>
      <button onClick={() => throttledUpdate(value + 1)}>
        Increment (Throttled)
      </button>
      <button onClick={() => throttledUpdate.cancel()}>
        Cancel Pending
      </button>
      <button onClick={() => throttledUpdate.flush()}>
        Execute Now
      </button>
      <p>Value: {value}</p>
      <p>Has Pending: {throttledUpdate.isPending() ? 'Yes' : 'No'}</p>
    </div>
  );
}

CallOptions Interface

interface CallOptions {
  /** 
   * Specify invoking on the leading edge of the timeout
   * Default: true for useThrottledCallback
   */
  leading?: boolean;
  /** 
   * Specify invoking on the trailing edge of the timeout
   * Default: true for useThrottledCallback
   */
  trailing?: boolean;
}

Throttling vs Debouncing

Throttling ensures a function executes at regular intervals during continuous activity:

  • Use case: Scroll handlers, resize handlers, mouse movement, animation
  • Behavior: Executes immediately, then at most once per interval
  • Example: Update scroll position every 100ms while scrolling

Debouncing delays execution until activity stops:

  • Use case: Search input, form validation, API calls
  • Behavior: Waits for quiet period before executing
  • Example: Search API call 500ms after user stops typing
// Throttling - executes regularly during scrolling
const throttledScroll = useThrottledCallback(
  () => console.log('Scroll position:', window.scrollY),
  100 // Every 100ms while scrolling
);

// Debouncing - executes once after scrolling stops
const debouncedScroll = useDebouncedCallback(
  () => console.log('Scroll ended at:', window.scrollY),
  100 // 100ms after scrolling stops
);

Common Patterns

Performance Monitoring

function PerformanceMonitor() {
  const [fps, setFPS] = useState(0);
  const frameCount = useRef(0);
  const lastTime = useRef(performance.now());

  const throttledFPSUpdate = useThrottledCallback(
    () => {
      const now = performance.now();
      const delta = now - lastTime.current;
      const currentFPS = Math.round((frameCount.current * 1000) / delta);
      
      setFPS(currentFPS);
      frameCount.current = 0;
      lastTime.current = now;
    },
    1000 // Update FPS display once per second
  );

  useEffect(() => {
    const animate = () => {
      frameCount.current++;
      throttledFPSUpdate();
      requestAnimationFrame(animate);
    };
    
    const id = requestAnimationFrame(animate);
    return () => cancelAnimationFrame(id);
  }, [throttledFPSUpdate]);

  return <div>FPS: {fps}</div>;
}

Event Delegation with Throttling

function ThrottledEventDelegation() {
  const throttledHandler = useThrottledCallback(
    (event: Event) => {
      const target = event.target as HTMLElement;
      if (target.matches('.interactive')) {
        console.log('Interacted with:', target.textContent);
      }
    },
    200
  );

  useEffect(() => {
    document.addEventListener('click', throttledHandler);
    return () => {
      document.removeEventListener('click', throttledHandler);
    };
  }, [throttledHandler]);

  return (
    <div>
      <div className="interactive">Button 1</div>
      <div className="interactive">Button 2</div>
      <div className="interactive">Button 3</div>
    </div>
  );
}

Progressive Loading

function ProgressiveLoader() {
  const [items, setItems] = useState([]);
  const [page, setPage] = useState(1);

  const throttledLoadMore = useThrottledCallback(
    async () => {
      const response = await fetch(`/api/items?page=${page}`);
      const newItems = await response.json();
      setItems(prev => [...prev, ...newItems]);
      setPage(prev => prev + 1);
    },
    2000 // Prevent rapid successive loads
  );

  const handleScroll = () => {
    if (
      window.innerHeight + window.scrollY >= 
      document.body.offsetHeight - 1000
    ) {
      throttledLoadMore();
    }
  };

  useEffect(() => {
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);

  return (
    <div>
      {items.map((item: any) => (
        <div key={item.id}>{item.title}</div>
      ))}
    </div>
  );
}

Install with Tessl CLI

npx tessl i tessl/npm-use-debounce

docs

callback-debouncing.md

index.md

throttling.md

value-debouncing.md

tile.json