最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Debouncing and Timeout in React - Stack Overflow

programmeradmin3浏览0评论

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 ponent. 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 ponent

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

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 ponent. 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 ponent

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,
        ]);
      }}
    />
  );
}
Share Improve this question edited Dec 8, 2021 at 3:31 Joseph asked Dec 8, 2021 at 3:05 JosephJoseph 7,75531 gold badges104 silver badges225 bronze badges 6
  • yea that's absolutely the wrong place to put a hook. Hooks should be placed outside of the rendered elements. Move it inside the body of the parent ponent of TextField – smac89 Commented Dec 8, 2021 at 3:11
  • Your hook is called from a function inside your ponents, this breaks the rule of hooks You should use the hook at the top level. – vaibhavmande Commented Dec 8, 2021 at 3:11
  • @smac89. so how would you move it and call from that? – Joseph Commented Dec 8, 2021 at 3:20
  • Did you define that useDebounce yourself? How did you intend to use it? – Bergi Commented Dec 8, 2021 at 3:27
  • @Bergi. Updated my question. I want to dispatch an action to redux using handleChangeProductName not on every input cause i have a lot of textfields so it would be heavy – Joseph Commented Dec 8, 2021 at 3:32
 |  Show 1 more ment

3 Answers 3

Reset to default 7

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),
    [handleChangeProductName]
  );

  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 ponent passing the handleChangeProductName callback as a prop should probably handle creating a debounced, memoized handler, but the above should work as well.

Debouncing onChange itself has caveats. Say, it must be uncontrolled ponent, since debouncing onChange on controlled ponent would cause annoying lags on typing.

Another pitfall, we might need to do something immediately and to do something else after a delay. Say, immediately display loading indicator instead of (obsolete) search results after any change, but send actual request only after user stops typing.

With all this in mind, instead of debouncing callback I propose to debounce sync-up through useEffect:

const [text, setText] = useState('');
const isValueSettled = useIsSettled(text);

useEffect(() => {
  if (isValueSettled) {
    props.onChange(text);
  }
}, [text, isValueSettled]);

...
  <input value={value} onChange={({ target: { value } }) => setText(value)}

And useIsSetlled itself will debounce:

function useIsSettled(value, delay = 500) {
  const [isSettled, setIsSettled] = useState(true);
  const isFirstRun = useRef(true);
  const prevValueRef = useRef(value);

  useEffect(() => {
    if (isFirstRun.current) {
      isFirstRun.current = false;
      return;
    }
    setIsSettled(false);
    prevValueRef.current = value;
    const timerId = setTimeout(() => {
      setIsSettled(true);
    }, delay);
    return () => { clearTimeout(timerId); }
  }, [delay, value]);
  if (isFirstRun.current) {
    return true;
  }
  return isSettled && prevValueRef.current === value;
}

where isFirstRun is obviously save us from getting "oh, no, user changed something" after initial rendering(when value is changed from undefined to initial value).

And prevValueRef.current === value is not required part but makes us sure we will get useIsSettled returning false in the same render run, not in next, only after useEffect executed.

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);
      }}
    />
  );
}
发布评论

评论列表(0)

  1. 暂无评论