No Deploy Friday logoNo Deploy FridayHomeShopBlogAbout

How to Debounce Props With React Hooks

by Ezra Bowman

Debounce user input and props with useCallback, useEffect, & useState. Learn how to create your own useDebounce custom React hook.

Debounce user input and props with useCallback, useEffect, & useState.

If you are reading this post, you probably know what it means to debounce user input and want to see how to use it in your React app. Just in case you read the title of this post and said, "Debounce - that sounds cool. What is that?" I'll go over what debounce is and why you may need it in your app. Scroll down if you know what this is and just want to see the code.

This post shows three debounce examples:

  1. Debouncing user input as they type
  2. Debouncing a prop that is changing rapidly from a parent component
  3. Write a custom debounce hook - useDebounce()

Debounce is a function used to prevent time-consuming tasks from firing too often.

Time-consuming tasks can include fetching data from a server, processing a credit card payment, or doing some complex calculation. These tasks are started by some user action like clicking a button or typing in an input box. In JavaScript (and React), these user actions fire events that trigger the functions that carry out our time-consuming task. React Dbounce

Now, to be fair, there are other techniques to prevent these tasks from firing too often. You could disable a button after the first click while the job runs. You could also use the onBlur event, which only fires when the user moves the cursor out of the text box.

Sometimes you can't (or don't want to) use those methods.

You want a function that says "when x hasn't changed for some period of time, do this thing." That is what a debounce function does.

For user input, if we use the onChange event on our input field, the event will fire every time the user presses a key. If we have an event handler attached to this event, it runs every time the user presses a key. We probably don't want to send every keystroke to the server. So we debounce the handling of this event, which says "when the user has stopped typing for 1 sec, then send the search to the server."

In React, whenever props change, the component gets re-rendered. If props from the parent are changing rapidly, you may have effects in your component that you don't want to fire every time the props change. If this is the case, you can debounce the handling the prop change with the useEffect and useCallback hooks.


1. User Input Example

The full code example is available here: https://codesandbox.io/s/react-debounce-demo-0cewm

To set up this example, let's create a component with an input box for the user to type something in and output that echos the current value in the text box.


const App = () => {
  const [state, setState] = useState("");

  const handleChange = (event: any) => {
    setState(event.target.value);
    // or send request to server here
  };

  return (
    <div className="App">
      <h1>React Debounce Demo</h1>
      <form>
        <label>
          Search:
          <input type="text" onChange={handleChange}/>
        </label>
      </form>
      <p>Search Value: {state}</p>
    </div>
  );
};

export default App;

If you run this app, as you type in the input box, the 'Search Value' updates with every keystroke. Here we are just setting the state of the component. The handleChange function would be where we want to send the search to the server, but doing this on every keystroke would be too much.

Enter debounce.

import React, { useCallback, useState } from "react";
import _ from "lodash";

const App = () => {
  const [state, setState] = useState("");
  const [debouncedState, setDebouncedState] = useState("");

  const handleChange = (event: any) => {
    setState(event.target.value);
    debounce(event.target.value);
  };

  const debounce = useCallback(
    _.debounce((_searchVal: string) => {
      setDebouncedState(_searchVal);
      // send the server request here		
    }, 1000),
    []
  );

  return (
    <div className="App">
      <h1>React Debounce Demo</h1>
      <form>
        <label>
          Search:
          <input type="text" onChange={handleChange} />
        </label>
      </form>
      <p>Search Value: {state}</p>
      <p>Debounced Value: {debouncedState}</p>
    </div>
  );
};

export default App;

Running the app now, you can see both the value and the debounced value at the same time. If you type something in the search box, you can see the debounced value only updates after you have stopped typing for 1000 ms.

Two magical things are going on here that make this work:

First is the lodash debounce function. Lodash is a javascript utility library (see https://lodash.com) that has several handy functions (it exports as an underscore “_”). The lodash _.debounce() function takes 2 arguments. One is the function you actually want to run (just not too often), and the other is the time (in milliseconds) to wait for the value to stop changing. This means you can call _.debounce() as many times as you want, and it will wait to run the inner function until X ms after the last time you call it.

Second is the useCallback() hook. You may wonder why you can't just use _.debounce() in the handleChange function. This would make sense, right? The handleChange function gets called every time you type in the input box, so putting _.debounce() there, it should get called over and over until you stop typing, and when the 1000ms time is up, it should update the debouncedState value.

But this does NOT work.

To make this work, you must put the _.debounce() function call in a useCallback hook. This has to do with how React works under the covers. Essentially, every time the component renders, each const function (like handleChange) is re-created.

That's right, every time you type a character in the input field, it fires the change event, which triggers the handleChange function, which sets the state (setState), which triggers a re-render, which recreates the handleChange function!

So the problem is that the _.debounce() function (when placed inside the handleChange function) is NOT the same instance of the _.debounce() function from one render to the next, and therefore it never fires its internal function, which sets the debouncedState value.

Enter useCallback().

The useCallback hook 'memoizes' a function. This is a fancy way of saying that the argument to useCallback (a function itself) will be the same instance between renders, which solves the problem described above.


2. Want to debounce a prop instead?

Sometimes incoming props may be changing fast, and in our component, we may want to debounce that individual prop to (again) prevent some time-consuming task from running too often.

The full code example is available here: https://codesandbox.io/s/react-debounce-prop-demo-8ui6c

For this example, we will modify the file above to pass the input box's value to a child component called DebounceViewer.

import React, { useState } from "react";
import DebounceViewer from "./DebounceViewer";

const App = () => {
  const [state, setState] = useState("");

  const handleChange = (event: any) => {
    setState(event.target.value);
  };

  return (
    <div className="App">
      <h1>React Debounce Demo</h1>
      <form>
        <label>
          Search:
          <input type="text" onChange={handleChange} />
        </label>
      </form>
      <p>Search Value: {state}</p>
      <DebounceViewer val={state}></DebounceViewer>
    </div>
  );
};

export default App;

The value we pass to this child component changes with every keystroke, and therefore we will debounce the prop in the DebounceViewer component as follows:


import React, { useCallback, useEffect, useState } from "react";
import _ from "lodash";

const DebounceViewer = ({ val }) => {
  const [state, setState] = useState("");

  const debounce = useCallback(
    _.debounce((_searchVal: string) => {
      console.log("updating search");
      setState(_searchVal);
    }, 1000),
    []
  );

  useEffect(() => debounce(val), [val]);

  return <p>DebounceViewer: {state}</p>;
};

export default DebounceViewer;

The solution to debouncing is very similar to the first example in that we still put the _.debounce() function in the useCallback hook for the same reasons listed in the first example.

Here, the magic part is that we added the useEffect hook to call our debounce function whenever the val prop changes. Notice the [val] array as the second argument to the useEffect call. You may know that useEffect is called after every render of a component. This second argument tells React to run this effect only when the props in the array have changed. In our case, we only have the one prop ("val"); however, there could be any number of additional props, and the call to debounce will only happen when "val" changes.


3. Custom useDebounce() Hook

Let's take this even farther. The examples above work great to debounce inputs or props in the specific components shown, but what if we wanted a reusable debounce function that we could use all over the place?

Let's create our own custom hook!

The full code example is available here: https://codesandbox.io/s/react-debounce-hook-demo-3kxy1

Custom React hooks aren't really that hard. A hook is a JavaScript function that uses other hooks. That's literally the only criteria. Here's what our custom debounce hook would look like. Add the following to a separate file (I called mine debounce-hook.ts).

import React, { useCallback, useState } from "react";
import _ from "lodash";

export const useDebounce = (obj: any = null, wait: number = 1000) => {
  const [state, setState] = useState(obj);

  const setDebouncedState = (_val: any) => {
    debounce(_val);
  };

  const debounce = useCallback(
    _.debounce((_prop: string) => {
      console.log("updating search");
      setState(_prop);
    }, wait),
    []
  );

  return [state, setDebouncedState];
};

We set up the hook to take in an optional initial value, and we return a reference to the current value and a function up update the value. This matches the setState signature, so it will be intuitive to use. The one additional parameter is the debounce wait time. Notice we have moved the lodash _.debounce() function here to the custom hook.

Here is how to use our new custom hook:

import React, { useState } from "react";
import { useDebounce } from "./debounce-hook";
import "./styles.css";

const App = () => {
  const [state, setState] = useState("");
  const [debouncedState, setDebouncedState] = useDebounce(state);

  const handleChange = (event: any) => {
    setState(event.target.value);
    setDebouncedState(event.target.value);
  };

  return (
    <div className="App">
      <h1>React Debounce Demo</h1>
      <form>
        <label>
          Search:
          <input type="text" onChange={handleChange} />
        </label>
      </form>
      <p>Search Value: {state}</p>
      <p>Debounced Value: {debouncedState}</p>
    </div>
  );
};

You now have a completely reusable custom debounce hook you can use all over your app.

See how we can use the useDebounce hook just like we use the setState hook? Now we can just call the useDebouncedState function directly in the handleChange function. Remember this is called every time you type something in the input box, only now our debounce hook will wait to update its output.

Conclusion

Thanks for reading! Please leave feedback and let me know if you enjoyed this post. I have other articles on DevOps, cloud architectures, and web development here and on Medium - https://medium.com/@ezrabowman.



No Deploy Friday logo

Related Posts