Debouncing with React hooks
Tags: react, javascript
Published: 2020-05-03 03:36:45

How to use React hooks to debounce an onChange event.

### tl;dr
```javascript
  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.

```javascript
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*
```javascript
  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:*
```javascript
  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.

```javascript
  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
```javascript
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