【问题标题】:Debouncing and Timeout in ReactReact 中的去抖动和超时
【发布时间】:2022-01-13 01:47:50
【问题描述】:

我在这里有一个输入字段,在每种类型上,它都会调度一个 redux 操作。 我放了一个 useDebounce 以使它不会很重。问题是它说Hooks can only be called inside of the body of a function component. 正确的做法是什么?

使用超时

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

使用去抖动

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

表单组件

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

【问题讨论】:

  • 是的,这绝对是放置钩子的错误位置。钩子应该放置在渲染元素之外。将其移动到TextField的父组件主体内
  • 你的钩子是从你的组件内部的一个函数中调用的,这违反了hooks的规则你应该在顶层使用钩子。
  • @smac89。那么你将如何移动它并从中调用呢?
  • 您自己定义了useDebounce 吗?你打算如何使用它?
  • @Bergi。更新了我的问题。我想使用handleChangeProductName 向redux 发送一个动作,而不是在每个输入上,因为我有很多文本字段,所以它会很重

标签: javascript reactjs ecmascript-6 react-hooks debouncing


【解决方案1】:

我认为 React 钩子不适合用于油门或去抖动功能。根据我对您的问题的理解,您实际上希望消除 handleChangeProductName 函数的抖动。

这是一个简单的高阶函数,您可以使用它来装饰回调函数以消除抖动。如果在超时到期之前再次调用返回的函数,则超时将被清除并重新实例化。只有当超时过期时,装饰函数才会被调用并传递参数。

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

示例用法:

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

如果可能,将 handleChangeProductName 回调作为 prop 传递的父组件应该可以处理创建去抖动、记忆化的处理程序,但上述方法也应该可以工作。

【讨论】:

  • @Joseph 如果用户正在积极输入,您不妨等待他们完成,这样您就不会对已更改的输入进行浪费的中间异步调用。
  • 你怎么能这样做呢?你也可以用它来编辑你的答案吗?谢谢德鲁
  • @Joseph 您将去抖动延迟调整为更长的时间,并在用户完成输入时“猜测”。或者换句话说,延迟是用户停止输入后调用函数所需的时间。
【解决方案2】:

看看你对useDebounce 的实现,它作为一个钩子看起来不是很有用。它似乎已经接管了调用你的函数的工作,并且不返回任何东西,但它的大部分实现都是在 useTimeout 中完成的,它也没有做太多......

在我看来,useDebounce 应该返回 callback 的“去抖动”版本

这是我对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;
}

用法如下所示:

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

【讨论】:

  • 谢谢。但是如何将它集成到 TextField 中?
  • @约瑟夫。见编辑
  • 似乎只是传递了props.handleChangeProductName上的第一个字母
  • @Joseph 尝试减少去抖动时间。坦率地说,1 秒的等待时间有点太长了。尝试像 150 这样更小的值。要考虑的其他一点是 debounce 仅在超时后采用最新值,因此您可能需要更新 useDebounce 挂钩以跟踪传递给 debounce 的最后一个参数并在超时后调用该函数结束了
  • 你能用那个编辑你的答案吗?
【解决方案3】:

去抖onChange 本身有一些注意事项。比如说,它必须是不受控制的组件,因为在受控组件上去抖动 onChange 会导致打字时出现烦人的延迟。

另一个陷阱,我们可能需要立即做某事,延迟后再做其他事情。比如说,在任何更改后立即显示加载指示器而不是(过时的)搜索结果,但只有在用户停止输入后才发送实际请求。

考虑到这一切,我建议通过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)}

useIsSetlled 本身会去抖动:

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

isFirstRun 显然可以避免我们在初始渲染后收到“哦,不,用户更改了某些内容”(当 valueundefined 更改为初始值时)。

并且prevValueRef.current === value 不是必需的部分,但确保我们将在同一渲染运行中得到useIsSettled 返回false,而不是在下一次,只有在执行useEffect 之后。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-11-01
    • 2017-06-16
    • 1970-01-01
    • 1970-01-01
    • 2017-05-03
    • 2020-04-28
    • 1970-01-01
    • 2019-03-20
    相关资源
    最近更新 更多