DEV Community

Alex Wiles
Alex Wiles

Posted on • Originally published at alex.uncomma.com

Debouncing with React hooks

How to use React hooks to debounce an onChange event.

tl;dr

  useEffect(() => {
    const timeout = setTimeout(() => someFunction(), 400);
    return () => { clearTimeout(timeout); };
  }, [dependency]);

Use setTimeout in the effect and clear it before the next effect call. This will only call someFunction() when the dependency array has not changed for 400 ms. This pattern is ideal for making network requests or calling other expensive functions.

Refactoring a laggy input

The Username component has two pieces of state: username and valid. On every input change, we set the username to the new value and we calculate and set the validity.

This works, but it is a bad user experience because the input feels very laggy. The validation takes a long time and keyboard events do not feel instant.

Here, I am burning cycles with a big for loop, but you can imagine making a network request in its place.

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

const validate = value => {
  // expensive validation
  for (var x = 1; x < 500000000; x++) {
    value.length < x;
  }

  if (value.length > 5) {
    return "max length is 5";
  }

  if (value.length === 0) {
    return "please select your username";
  }

  return "looks good";
};


const Username = () => {
  const [username, setUsername] = useState("");
  const [valid, setValid] = useState(undefined);

  return (
    <div>
      <div>Username</div>
      <input
        type="text"
        value={username}
        onChange={e => {
          const value = e.target.value;
          setUsername(value);
          setValid(validate(value));
        }}
      />
      <div>{valid}</div>
    </div>
  );
};

ReactDOM.render(<Username />, document.getElementById("main"));

Refactoring away the lagginess

We still want to check if a username is valid, but we want to make it a good experience for the user. The goal is to validate the user input only after there has been a pause in typing. We don't care about validating the input while the user is typing, we only want to validate it when they have paused for a moment.

Remove the setValid call from the onChange event.
We only want to call setValid after the user stops typing for a set amount of time. Now, onChange only updates the username. The input will not feel laggy anymore, but the validation is not triggered.

The useEffect hook
We will use the "useEffect" hook to calculate and set the validation. The useEffect hook takes two arguments, the effect function and a dependency array. The effect fires when a value in the dependency array changes. In our case, we want to fire the callback when the username changes, so we put it in the dependency array.

Still laggy

  useEffect(() => {
    setValid(validate(username));
  }, [username]);

The effect triggers whenever the username changes. We can't check and set the validity in the effect because we would face the same problem as before: a laggy input. So, we need a way to only call the validation function after the username has not changed for a set amount of time.

Using setTimeout
setTimeout takes two arguments: a callback and a number of milliseconds. In this example, we want to check and set the validity in the callback. I chose 400 milliseconds for the timeout. Now, everytime the username changes a timeout is set and the validation triggers.

Still not good:

  useEffect(() => {
    const timer = setTimeout(() => {
      setValid(validate(username));
    }, 400);
  }, [username]);

Still, this is not ideal. Every key press will create a timeout and a validation is called for each keypress, just delayed a bit.

useEffect cleanup
useEffect provides a way to cleanup effects. If you return a function from an effect, then it will trigger before the next effect. This is exactly what we need. We can return a function that clears the old timeout before creating a new one.

  useEffect(() => {
    setValid("");
    const timer = setTimeout(() => {
      setValid(validate(username));
    }, 400);

    return () => {
      clearTimeout(timer);
    };
  }, [username]);

This is how you call the validation function only after the username has not changed for 400ms.

The full code

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

const validate = value => {
  // expensive validation
  for (var x = 1; x < 500000000; x++) {
    value.length < x;
  }

  if (value.length > 5) {
    return "max length is 5";
  }

  if (value.length === 0) {
    return "please select your username";
  }

  return "looks good";
};

const Username = () => {
  const [username, setUsername] = useState("");
  const [valid, setValid] = useState(undefined);

  useEffect(() => {
    // clear the valid message so nothing is displayed while typing
    setValid("");

    // create the timer
    const timer = setTimeout(() => {
      setValid(validate(username));
    }, 400);

    // return a cleanup function that clears the timeout
    return () => {
      clearTimeout(timer);
    };
  }, [username]);

  return (
    <div>
      <div>Username</div>
      <input
        type="text"
        value={username}
        onChange={e => {
          const value = e.target.value;
          setUsername(value);
        }}
      />
      <div>{valid}</div>
    </div>
  );
};

Further reading

Check out the react docs for useEffect: https://reactjs.org/docs/hooks-effect.html

Top comments (0)