Debounce and throttle in React

Author of the article
Chaeah Park
15 Feb 2024

In the fast-paced world of web development, performance is not just an option; it's a necessity. Apps must be fast and responsive to keep users active and engaged. A key aspect of achieving these expectations is to optimize user interactions and events. Debounce and throttle are techniques that offer enhanced user experience by reducing unnecessary executions of event handlers (callback functions).

Understanding Event Handling in React

Before diving into debounce and throttle, let's first understand a well-known challenges in React. React applications often need to respond to user actions in real-time. It could be a situation where a user types in a search box, scrolls infinitely through a list, or resizes their browser window. Each action of the user can trigger a cascade of event handlers. Without careful implementation, these handlers can execute functions more frequently than necessary, leading to performance bottlenecks and degrading user experiences.

For instance, consider a search bar that fetches results as the user types. Without optimization, every keystroke would send a http request to the server, triggering unnecessary load and causing delays in the user interface. Similarly, handling scroll events without caution can lead to jittery scrolling experiences.

Introducing Debounce and Throttle

The solution to these challenges lies in two key concepts: debouncing and throttling. Both techniques aim to control the frequency of function execution, but they do so in slightly different ways.

debounce and throttle

Debounce and throttle. Image from SleekSky

What is debounce?

Debounce is a method to executes a callback function when a certain period time is passed after a user event is detected. In other words, it waits for a pause in the events before executing an event handler. This technique can be particulary useful for actions like live search. It enable to fetch search results only when the user has stopped typing, reducing the number of calls to the server.

Implementing debounce in React

import { memo, useState, useEffect, useCallback } from 'react';

// Custom debounce hook
function useDebounce(callback, delay) {
  const [timer, setTimer] = useState(null);

  // return callback wrapping with setTimeout.
  const debouncedCallback = (...args) => {
    if (!timer) {
      setTimer(setTimeout(() => {
        callback(...args)
      }, delay);)
    }
  };

  useEffect(() => {
    // if a timer already exists, remove it.
    return () => {
      if (timer) clearTimeout(timer);
    };
  }, [timer]);

  return debouncedCallback;
}

// SearchInput: Example component using the useDebounce hook
function SearchInput() {
  const [query, setQuery] = useState('');
  const debouncedSearchRequest = useDebounce(()=>{console.log('fetch search results of', query)}, 500);

  useEffect(() => {
    if (query) {
      debouncedSearchRequest()
    }
  }, [query]);

  return (
    <input
      type="text"
      value={query}
      onChange={e => setQuery(e.target.value)}
      placeholder="Type to search..."
    />
  );
}

export default memo(SearchInput)

What is throttle?

Throttling, on the other hand, ensures that a function is only executed once every specified period. Unlike debouncing, which waits for a pause, throttling will allow the function to execute periodically, ensuring that it's not called more often than the specified rate. This approach is ideal for handling actions like scrolling or resizing, where you want to ensure that the event handler is called at a controlled, steady pace.

Implementing throttle in React

import { useState, useEffect } from 'react';

function useThrottle(callback, delay) {
  const [waiting, setWaiting] = useState('false');

  const throttleCallback = (..args) => {
    if (waiting) return;

    callback(...args);
    setWaiting(true);
    const id = setTimeout(()=>{
      setWaiting(false);
    }, delay)
  };

  return throttleCallback;
}

function ResizeListner() {
  const [width, setWidth] = useState(window.innerWidth);

  const handleResize = useThrottle(()=>{
    setWidth(window.innerWidth)
  }, 1000)

  useEffect(()=>{
    window.addEventListners('resize', handleResize);

    return ()=> window.removeEventListner('resize', handleResize);
  }, [handleResize])

  return <p>inner width: {width}</p>
}

When to use Debounce vs. Throttle

Debounce is your go-to when you need to ensure that a function is executed only after a certain amount of idle time has passed since the last invocation. This makes it ideal for handling events that don't require immediate feedback for every action. Common use cases include:

  • Validating text inputs or search fields as the user types.
  • Applying filters based on user input.
  • Autosaving functionality that triggers after typing pauses.

Throttle, on the other hand, ensures that a function is called at most once in a specified time frame. Use throttle when you want to maintain regular execution intervals for actions that occur frequently. Typical scenarios include:

  • Tracking scroll positions to trigger animations or loading.
  • Resizing handlers for dynamic layout adjustments.

Mistsakes must be avoided

  • Overuse: Applying debounce or throttle universally without considering the nature of the event can lead to sluggish interfaces or missed user inputs. Assess the necessity on a case-by-case basis.

  • Incorrect Implementation: Mixing up debounce and throttle logic, or setting inappropriate timings, can degrade the user experience. Ensure you're using the right technique for the right scenario.

  • Memory Leaks: Not cleaning up event listeners or timeouts/intervals can lead to memory leaks, especially in Single Page Applications. Always provide cleanup functions in your useEffect hooks.

Advanced Topics

Other Performance Optimization Techniques in React Memoization: Use React's React.memo, useMemo, and useCallback to prevent unnecessary re-renders by memoizing components and functions. This is especially beneficial for components that receive complex objects as props or rely on computationally expensive calculations.

Lazy Loading: React's React.lazy and Suspense let you split your component code into separate chunks that are loaded only when needed. This can significantly reduce the initial load time of your app, making it feel snappier.

Virtualization: For lists or grids displaying a large amount of data, consider using windowing or virtualization libraries like react-window or react-virtualized. These libraries render only the items currently visible to the user, reducing the number of DOM nodes and improving performance.

Copyright © Chaeah Park