React 中的去抖动和超时
Posted
技术标签:
【中文标题】React 中的去抖动和超时【英文标题】:Debouncing and Timeout in React 【发布时间】: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 发送一个动作,而不是在每个输入上,因为我有很多文本字段,所以它会很重
【参考方案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
显然可以避免我们在初始渲染后收到“哦,不,用户更改了某些内容”(当 value
从 undefined
更改为初始值时)。
并且prevValueRef.current === value
不是必需的部分,但确保我们将在同一渲染运行中得到useIsSettled
返回false
,而不是在下一次,只有在执行useEffect
之后。
【讨论】:
以上是关于React 中的去抖动和超时的主要内容,如果未能解决你的问题,请参考以下文章