useDebouncedEffect Hook
Tags: react, typescript, javascript, webdev
Published: 2020-03-24 15:54:19

This hook is for when we want to track something that changes quickly (mouse moves, user typing, etc...), but wait to trigger the `onChange` until the updates stop streaming in.  You'd typically do this when your `onChange` does something expensive, like a make network call.

In the following example, pretend `onChange` prop is a function that makes an API call to the server. Here's where we'll get by the end of the Post. 

```typescript
function SearchBox({ onChange, defaultValue = "" }) {
  // We store one value for the instant updates
  let [value, setValue] = useState(defaultValue);

  useDebouncedEffect(
    (debouncedValue) => {
      console.log("Firing onchange", debouncedValue);
      onChange(debouncedValue);
    },
    value,
    300
  );

  return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}
```

A simple first approach might be to leverage `useEffect`, but we'd be making a network call for every keystroke.

```typescript
function SearchBox({ onChange, defaultValue = "" }) {
  // We store one value for the instant updates
  let [value, setValue] = useState(defaultValue);

  // Whenever the value changes, call the passed in 'onChange'
  useEffect(() => {
    console.log("Firing onchange", value);
    onChange(value);
  }, [value]);

  return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}

```
We want to create a way to only trigger the `onChange` once the value has stopped updating for a specified amount of time.

Lets create a custom hook, `useDebouncedValue`, that keeps track of a frequently changing value with state, but only updates the state once the value stops updating.

```typescript
export function useDebouncedValue(value, delay) {
  // State and setters for debounced value
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    // Update state to the passed in value after the specified delay
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      // If our value changes (or the component unmounts), React will
      // run this cleanup function to cancel the state update.
      clearTimeout(handler);
    };
    // These are the dependencies, if the value or the delay amount
    // changes, then cancel any existing timeout and start waiting again
  }, [value, delay]);

  return debouncedValue;
}
```

Now we could use our new hook like so:

```typescript
function SearchBox({ onChange, defaultValue = ""}) {
  // We store one value for the instant updates
  let [value, setValue] = useState(defaultValue);
  // We use our new hook to track a value that only changes
  // when the user stops typing
  let debouncedValue = useDebouncedValue(value, 300);

  // We perform an effect anytime the user stops typing
  useEffect(() => {
    console.log("Firing onchange", debouncedValue);
    onChange(debouncedValue);
  }, [debouncedValue]);

  return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}

```

That works. But what if we tried to get rid of some of the boilerplate with one more custom hook, `useDebouncedEffect`.

```typescript
export function useDebouncedEffect(effectFn, value, delay = 250) {
  // Store the effect function as a ref so that we don't
  // trigger a re-render each time the function changes
  let effectRef = useRef(effectFn);
  // Leverage the hook we just created above 
  let debouncedValue = useDebouncedValue(value, delay);

  // Run an effect whenever the debounced value
  useEffect(() => {
    if (effectRef.current) {
      // Invoke the effect function, passing the debouncedValue
      return effectRef.current(debouncedValue);
    }
  }, [debouncedValue]);
}

```

The final solution feels very similar to the original `useEffect` strategy but we get the performance benefits of the debounce.

```typescript
function SearchBox({ onChange, defaultValue = "" }) {
  // We store one value for the instant updates
  let [value, setValue] = useState(defaultValue);

  useDebouncedEffect(
    (debouncedValue) => {
      console.log("Firing onchange", debouncedValue);
      onChange(debouncedValue);
    },
    value,
    300
  );

  return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}
```