React 中的去抖动和超时
Debouncing and Timeout in React
我这里有一个输入字段,它在每种类型上都会调度一个 redux 操作。
我已经放了一个 useDebounce 以便它不会很重。问题是它说 Hooks can only be called inside of the body of a function component.
正确的方法是什么?
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 };
}
使用去抖
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,
]);
}}
/>
);
}
看看您对 useDebounce
的实现,它作为一个钩子看起来不是很有用。它似乎已经接管了调用你的函数的工作,并且没有 return 任何东西,但它的大部分实现都是在 useTimeout
中完成的,这也没有做太多...
在我看来,useDebounce
应该 return 是 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);
}}
/>
);
}
我认为 React hooks 不适合限制或去抖功能。根据我对你的问题的理解,你实际上想要去抖 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 传递的父组件应该可以处理创建一个去抖动的、记忆化的处理程序,但上面的应该也能工作。
去抖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
执行。
我这里有一个输入字段,它在每种类型上都会调度一个 redux 操作。
我已经放了一个 useDebounce 以便它不会很重。问题是它说 Hooks can only be called inside of the body of a function component.
正确的方法是什么?
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 };
}
使用去抖
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,
]);
}}
/>
);
}
看看您对 useDebounce
的实现,它作为一个钩子看起来不是很有用。它似乎已经接管了调用你的函数的工作,并且没有 return 任何东西,但它的大部分实现都是在 useTimeout
中完成的,这也没有做太多...
在我看来,useDebounce
应该 return 是 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);
}}
/>
);
}
我认为 React hooks 不适合限制或去抖功能。根据我对你的问题的理解,你实际上想要去抖 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 传递的父组件应该可以处理创建一个去抖动的、记忆化的处理程序,但上面的应该也能工作。
去抖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
执行。