ReactHustle

Speed Up Your React Apps By using Debounce (with Typescript)

Jasser Mark Arioste

Jasser Mark Arioste

Speed Up Your React Apps By using Debounce (with Typescript)

 If you're calling the search API for every input onChange event, not only would your app feel slow, it also fetches unnecessary data due to the repeated API calls. Using debounce is one of the techniques you should learn to speed up your apps by limiting function executing on user-generated input (e.g., onChange events). In React, this translates to limiting the re-rendering of your component. 

In this tutorial, we'll cover how to implement debounce in react functional components. We'll start on how to implement debounce without react, why this doesn't work in react functional components, and move to different ways to implement debounce in functional components.

What is debouncing in JavaScript? #

In Ondrej Polesny's article about debounce, he stated the following: "In JavaScript, a debounce function makes sure that your code is only triggered once per user input. Search box suggestions, text-field auto-saves, and eliminating double-button clicks are all use cases for debounce".   Debouncing means that we only execute the onChange event handler when the user has finished typing, instead of executing it on every change.

Implement Debounce without React #

In the olden days (before react hooks), we only had to use lodash.debounce then use the returned function to an onChange input event. Take a look at the example below:

Error:  The Parser function of type "raw" is not defined. Define your custom parser functions as: https://github.com/pavittarx/editorjs-html#extend-for-custom-blocks 

It's very simple and straight forward, we just wrap our handle with the lodash.debounce function and it takes care of limiting the function execution. 

Why is it bad practice to use this technique in React? #

You might be thinking, "Oh I can use this technique in react" right? Well, you can but this is NOT a good practice. Every change in input will trigger a change in the state, which triggers a re-render. This also means that react will re-calculate the debounce function on every render. For example:


import { useState } from 'react';
import * as React from 'react';
import { debounce } from 'lodash';
export const DebouncedComponent = () => {
  const [val, setVal] = useState('');
  const [values, setValues] = useState([]);
  const _handler = (e: React.ChangeEvent<HTMLInputElement>) => {
    const newVal = e.target.value;
    setVal(newVal);
    setValues([...values, newVal]);
  };

  //will get calculated in each render, so the timer gets reset.
  const debouncedHandler = debounce(_handler, 500);
  return (
    <React.Fragment>
      <h2>Using Debounce + Handler directly</h2>
      <input type="text" value={val} onChange={debouncedHandler} />
      <div>
        {values.map((val, index) => {
          return <li key={index}>{val}</li>;
        })}
      </div>
    </React.Fragment>
  );
};
123456789101112131415161718192021222324252627
Error:  The Parser function of type "raw" is not defined. Define your custom parser functions as: https://github.com/pavittarx/editorjs-html#extend-for-custom-blocks 

Like the example without react, this should print the debounced values after the user has finished typing right? Nope. It only prints an empty string. It doesn't work since we're using a controlled component.

In a related article, Dimitri solves this problem by using a useMemo or useCallback.

To me, this seems to be the wrong approach.

React is all about states. We should debounce the state rather than creating a debounce handler. We can do this by implementing a useDebounce hook.

Implementing the useDebounce hook #

Implementation for useDebounce hook is simple.  See the code below:

import { useEffect, useState } from 'react';

function useDebounce<T>(value: T, delay?: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  
  useEffect(() => {
    //create a timer to delay setting the value.
    const timer = setTimeout(() => setDebouncedValue(value), delay || 500);

    //if the value changes, we clear the timeout and do not change the value
    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);

  return debouncedValue;
}

export default useDebounce;
1234567891011121314151617181920

It follows the principles of debouncing. We delay function execution. But if there's a change in the value, we reset the timer and delay it again until there are no more updates.

To use the useDebounce hook it's very simple:


import { useState } from 'react';
import * as React from 'react';
import useDebounce from './useDebounce';
export const DebouncedComponent = () => {
  const [val, setVal] = useState('');
  //use debounce here by observing val
  const debouncedVal = useDebounce(val, 500);
  const [values, setValues] = useState([]);
  const _handler = (e: React.ChangeEvent<HTMLInputElement>) => {
    const newVal = e.target.value;
    setVal(newVal);
  };

  //only set the values when debouncedVal changes and print them.
  React.useEffect(() => {
    setValues([...values, debouncedVal]);
  }, [debouncedVal]);

  return (
    <React.Fragment>
      <h2>Using Debounce + Handler directly</h2>
      <input type="text" value={val} onChange={_handler} />
      <div>
        {values.map((val, index) => {
          return <li key={index}>{val}</li>;
        })}
      </div>
    </React.Fragment>
  );
};
12345678910111213141516171819202122232425262728293031

Demo:

Error:  The Parser function of type "raw" is not defined. Define your custom parser functions as: https://github.com/pavittarx/editorjs-html#extend-for-custom-blocks 

This is definitely a much cleaner way to handle debounce in React. We keep the synthetic onChange event handler. We still re-render the component on every input change. However, we only print the debounce values after the user has finished typing, or in your case call the backend API.

NOTE: We don't have to implement it ourselves. We can install usehooks-ts package fully supported with typescript and use the hook from there.

Implementing the useDebounceEffect  hook #

The useDebounce hook is useful for observing and delaying updates to a single state. What if we have multiple states to observe and just want execute a function just like useEffect?

We can implement a useDebounceEffect hook for this use case. 

//useDebounceEffect.tsx

import { useEffect, useCallback, DependencyList } from 'react';

function useDebounceEffect(effect: any, deps: DependencyList, delay = 250) {
  const callback = useCallback(effect, deps);

  useEffect(() => {
    const timeout = setTimeout(callback, delay);
    return () => clearTimeout(timeout);
  }, [callback, delay]);
}

export default useDebounceEffect;
1234567891011121314

We can further simplify our previous example with this hook:


import { useState } from 'react';
import * as React from 'react';
import useDebounceEffect from './useDebounceEffect';
export const DebouncedComponent = () => {
  const [val, setVal] = useState('');

  const [values, setValues] = useState([]);
  const _handler = (e: React.ChangeEvent<HTMLInputElement>) => {
    const newVal = e.target.value;
    setVal(newVal);
  };
  
  //directly use val but just use the useDebounceEffect hook.
  useDebounceEffect(() => {
    setValues([...values, val]);
  }, [val]);

  return (
    <React.Fragment>
      <h2>Using useDebounce hook</h2>
      <input type="text" value={val} onChange={_handler} />
      <div>
        {values.map((val, index) => {
          return <li key={index}>{val}</li>;
        })}
      </div>
    </React.Fragment>
  );
};
123456789101112131415161718192021222324252627282930

Demo:

Error:  The Parser function of type "raw" is not defined. Define your custom parser functions as: https://github.com/pavittarx/editorjs-html#extend-for-custom-blocks 

Conclusion #

Thank you for reaching this far!  So far, we've learned about debounce in javascript, how to implement it the old way, and how to implement it in react. We're now able to speedup our react apps by executing our functions efficiently.

If you found this tutorial helpful, please leave a like or share! If you'd like more tutorials like this in the future, be sure to subscribe to our newsletter or follow me on twitter!

Resources #

Credits: Image by David Mark from Pixabay

Share this post!

Related Posts

Disclaimer

This content may contain links to products, software and services. Please assume all such links are affiliate links which may result in my earning commissions and fees.
As an Amazon Associate, I earn from qualifying purchases. This means that whenever you buy a product on Amazon from a link on our site, we receive a small percentage of its price at no extra cost to you. This helps us continue to provide valuable content and reviews to you. Thank you for your support!
Donate to ReactHustle