Debounce — delaying function execution until the user stops calling it. Rate Limiting — limiting the number of function calls per period.
Why it matters: Without debounce, a search input fires an API request on every keystroke — potentially hundreds of requests per minute. Without throttle, scroll handlers execute 60+ times per second. These patterns prevent server overload, reduce bandwidth usage, and keep the UI responsive.
Debounce — for search:
1function useDebounce(value, delay) {2 const [debouncedValue, setDebouncedValue] = useState(value);34 useEffect(() => {5 const timer = setTimeout(() => setDebouncedValue(value), delay);6 return () => clearTimeout(timer);7 }, [value, delay]);89 return debouncedValue;10}1112function Search() {13 const [query, setQuery] = useState("");14 const debouncedQuery = useDebounce(query, 300);1516 useEffect(() => {17 if (debouncedQuery) {18 fetch(`/api/search?q=${debouncedQuery}`).then(/* ... */);19 }20 }, [debouncedQuery]);2122 return <input value={query} onChange={e => setQuery(e.target.value)} />;23}
Throttle — for scroll/resize:
1function useThrottle(callback, delay) {2 const lastCall = useRef(0);34 return useCallback((...args) => {5 const now = Date.now();6 if (now - lastCall.current >= delay) {7 lastCall.current = now;8 callback(...args);9 }10 }, [callback, delay]);11}1213// Usage14const handleScroll = useThrottle(() => {15 console.log("scroll!");16}, 200);1718<div onScroll={handleScroll}>...</div>
Rate Limiting (on client):
1function useRateLimit(fn, limit, period) {2 const calls = useRef([]);34 return useCallback((...args) => {5 const now = Date.now();6 calls.current = calls.current.filter(t => now - t < period);7 if (calls.current.length < limit) {8 calls.current.push(now);9 fn(...args);10 }11 }, [fn, limit, period]);12}
How debounce vs throttle differ:
Common pitfalls:
useCallback for the throttle wrapper (causes unnecessary re-renders).