DEV Community

Andrew Petersen
Andrew Petersen

Posted on

useDebouncedEffect Hook

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.

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.

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.

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:

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.

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.

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)} />;
}

Top comments (0)