Implementing Debounce and Throttling in React: A Practical Guide

Oscar Bustos

Learn how to implement debounce and throttling correctly in React with real-world examples

6 min read

Creating a Reusable useDebounce Hook

A practical implementation of a debounce hook for text inputs:

const useDebounce = (callback, delay) => {
  const callbackRef = useRef(callback);
  
  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);
  
  return useMemo(() => {
    const debouncedFn = (...args) => {
      if (debouncedFn.timeout) {
        clearTimeout(debouncedFn.timeout);
      }
      
      debouncedFn.timeout = setTimeout(() => {
        callbackRef.current(...args);
      }, delay);
    };
    
    return debouncedFn;
  }, [delay]);
};

// Usage for Search Input
const SearchComponent = () => {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  
  const searchAPI = async (value) => {
    const data = await fetch(`/api/search?q=${value}`);
    setResults(await data.json());
  };
  
  const debouncedSearch = useDebounce(searchAPI, 300);
  
  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={e => {
          setQuery(e.target.value);
          debouncedSearch(e.target.value);
        }}
      />
      <SearchResults results={results} />
    </div>
  );
};

Implementing useThrottle for Scroll Events

A throttle hook for handling scroll events:

const useThrottle = (callback, limit) => {
  const callbackRef = useRef(callback);
  const throttlingRef = useRef(false);
  
  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);
  
  return useMemo(() => {
    const throttledFn = (...args) => {
      if (!throttlingRef.current) {
        callbackRef.current(...args);
        throttlingRef.current = true;
        
        setTimeout(() => {
          throttlingRef.current = false;
        }, limit);
      }
    };
    
    return throttledFn;
  }, [limit]);
};

// Usage for Infinite Scroll
const InfiniteScroll = () => {
  const [items, setItems] = useState([]);
  const [page, setPage] = useState(1);
  
  const loadMore = async () => {
    const data = await fetch(`/api/items?page=${page}`);
    const newItems = await data.json();
    setItems(prev => [...prev, ...newItems]);
    setPage(p => p + 1);
  };
  
  const handleScroll = (e) => {
    const { scrollTop, clientHeight, scrollHeight } = e.target;
    
    if (scrollHeight - scrollTop <= clientHeight * 1.5) {
      loadMore();
    }
  };
  
  const throttledScroll = useThrottle(handleScroll, 500);
  
  return (
    <div onScroll={throttledScroll} style={{ height: '400px', overflow: 'auto' }}>
      {items.map(item => (
        <ItemCard key={item.id} {...item} />
      ))}
    </div>
  );
};

Form Submission with Debounce

Prevent multiple form submissions:

const DebouncedForm = () => {
  const [formData, setFormData] = useState({
    name: '',
    email: ''
  });
  
  const submitForm = async (data) => {
    await fetch('/api/submit', {
      method: 'POST',
      body: JSON.stringify(data)
    });
  };
  
  const debouncedSubmit = useDebounce(submitForm, 1000);
  
  const handleSubmit = (e) => {
    e.preventDefault();
    debouncedSubmit(formData);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={formData.name}
        onChange={e => setFormData(prev => ({
          ...prev,
          name: e.target.value
        }))}
      />
      <input
        type="email"
        value={formData.email}
        onChange={e => setFormData(prev => ({
          ...prev,
          email: e.target.value
        }))}
      />
      <button type="submit">Submit</button>
    </form>
  );
};

Window Resize with Throttle

Handle window resize events efficiently:

const ResponsiveLayout = () => {
  const [width, setWidth] = useState(window.innerWidth);
  
  const handleResize = () => {
    setWidth(window.innerWidth);
  };
  
  const throttledResize = useThrottle(handleResize, 200);
  
  useEffect(() => {
    window.addEventListener('resize', throttledResize);
    return () => window.removeEventListener('resize', throttledResize);
  }, []);
  
  return (
    <div>
      {width > 768 ? (
        <DesktopLayout />
      ) : (
        <MobileLayout />
      )}
    </div>
  );
};

Cleanup and Memory Management

Always clean up timers to prevent memory leaks:

const useDebouncedEffect = (callback, deps, delay) => {
  useEffect(() => {
    const handler = setTimeout(() => {
      callback();
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [...deps, delay]);
};

// Usage
const AutoSaveComponent = () => {
  const [content, setContent] = useState('');
  
  useDebouncedEffect(
    () => {
      saveToServer(content);
    },
    [content],
    1000
  );
  
  return (
    <textarea
      value={content}
      onChange={e => setContent(e.target.value)}
      placeholder="Auto-saves after 1 second of inactivity"
    />
  );
};

Key Takeaways

  1. Use debounce for text inputs and form submissions
  2. Use throttle for scroll and resize events
  3. Always clean up timers in useEffect
  4. Use refs to keep callbacks up to date
  5. Consider performance impact when choosing delay times

Is it a match?

If you need help or advice, feel free to drop us a message via social media or email

Contact me