Home > Software design >  Debouncing and Timeout in React
Debouncing and Timeout in React

Time:12-08

I have a here a input field that on every type, it dispatches a redux action. I have put a useDebounce in order that it won't be very heavy. The problem is that it says Hooks can only be called inside of the body of a function component. What is the proper way to do it?

useTimeout

import { useCallback, useEffect, useRef } from "react";

export default function useTimeout(callback, delay) {
  const callbackRef = useRef(callback);
  const timeoutRef = useRef();

  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  const set = useCallback(() => {
    timeoutRef.current = setTimeout(() => callbackRef.current(), delay);
  }, [delay]);

  const clear = useCallback(() => {
    timeoutRef.current && clearTimeout(timeoutRef.current);
  }, []);

  useEffect(() => {
    set();
    return clear;
  }, [delay, set, clear]);

  const reset = useCallback(() => {
    clear();
    set();
  }, [clear, set]);

  return { reset, clear };
}

useDebounce

import { useEffect } from "react";
import useTimeout from "./useTimeout";

export default function useDebounce(callback, delay, dependencies) {
  const { reset, clear } = useTimeout(callback, delay);
  useEffect(reset, [...dependencies, reset]);
  useEffect(clear, []);
}

Form component

import React from "react";
import TextField from "@mui/material/TextField";
import useDebounce from "../hooks/useDebounce";

export default function ProductInputs(props) {
  const { handleChangeProductName = () => {} } = props;

  return (
    <TextField
      fullWidth
      label="Name"
      variant="outlined"
      size="small"
      name="productName"
      value={formik.values.productName}
      helperText={formik.touched.productName ? formik.errors.productName : ""}
      error={formik.touched.productName && Boolean(formik.errors.productName)}
      onChange={(e) => {
        formik.setFieldValue("productName", e.target.value);
        useDebounce(() => handleChangeProductName(e.target.value), 1000, [
          e.target.value,
        ]);
      }}
    />
  );
}

CodePudding user response:

I don't think React hooks are a good fit for a throttle or debounce function. From what I understand of your question you effectively want to debounce the handleChangeProductName function.

Here's a simple higher order function you can use to decorate a callback function with to debounce it. If the returned function is invoked again before the timeout expires then the timeout is cleared and reinstantiated. Only when the timeout expires is the decorated function then invoked and passed the arguments.

const debounce = (fn, delay) => {
  let timerId;
  return (...args) => {
    clearTimeout(timerId);
    timerId = setTimeout(() => fn(...args), delay);
  }
};

Example usage:

export default function ProductInputs({ handleChangeProductName }) {
  const debouncedHandler = useCallback(debounce(handleChangeProductName, 200), []);

  return (
    <TextField
      fullWidth
      label="Name"
      variant="outlined"
      size="small"
      name="productName"
      value={formik.values.productName}
      helperText={formik.touched.productName ? formik.errors.productName : ""}
      error={formik.touched.productName && Boolean(formik.errors.productName)}
      onChange={(e) => {
        formik.setFieldValue("productName", e.target.value);
        debouncedHandler(e.target.value);
      }}
    />
  );
}

If possible the parent component passing the handleChangeProductName callback as a prop should probably handle creating a debounced, memoized handler, but the above should work as well.

CodePudding user response:

Taking a look at your implementation of useDebounce, and it doesn't look very useful as a hook. It seems to have taken over the job of calling your function, and doesn't return anything, but most of it's implementation is being done in useTimeout, which also not doing much...

In my opinion, useDebounce should return a "debounced" version of callback

Here is my take on useDebounce:

export default function useDebounce(callback, delay) {
  const [debounceReady, setDebounceReady] = useState(true);

  const debouncedCallback = useCallback((...args) => {
    if (debounceReady) {
      callback(...args);
      setDebounceReady(false);
    }
  }, [debounceReady, callback]);

  useEffect(() => {
    if (debounceReady) {
      return undefined;
    }
    const interval = setTimeout(() => setDebounceReady(true), delay);
    return () => clearTimeout(interval);    
  }, [debounceReady, delay]);

  return debouncedCallback;
}

Usage will look something like:

import React from "react";
import TextField from "@mui/material/TextField";
import useDebounce from "../hooks/useDebounce";

export default function ProductInputs(props) {
  const handleChangeProductName = useCallback((value) => {
    if (props.handleChangeProductName) {
      props.handleChangeProductName(value);
    } else {
      // do something else...
    };
  }, [props.handleChangeProductName]);

  const debouncedHandleChangeProductName = useDebounce(handleChangeProductName, 1000);

  return (
    <TextField
      fullWidth
      label="Name"
      variant="outlined"
      size="small"
      name="productName"
      value={formik.values.productName}
      helperText={formik.touched.productName ? formik.errors.productName : ""}
      error={formik.touched.productName && Boolean(formik.errors.productName)}
      onChange={(e) => {
        formik.setFieldValue("productName", e.target.value);
        debouncedHandleChangeProductName(e.target.value);
      }}
    />
  );
}
  • Related