Aquí tengo un campo de entrada que en cada tipo, envía una acción de reducción. He puesto un useDebounce para que no sea muy pesado. El problema es que dice que Hooks can only be called inside of the body of a function component. ¿Cuál es la forma correcta de hacerlo?
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 }; }usarDebounce
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, []); }Componente de formulario
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, ]); }} /> ); }No creo que los ganchos React sean adecuados para una función de aceleración o rebote. Por lo que entiendo de su pregunta, efectivamente desea eliminar el rebote de la función handleChangeProductName .
Aquí hay una función simple de orden superior que puede usar para decorar una función de devolución de llamada para eliminar el rebote. Si la función devuelta se vuelve a invocar antes de que expire el tiempo de espera, el tiempo de espera se borra y se vuelve a crear. Solo cuando expira el tiempo de espera, se invoca la función decorada y se pasan los argumentos.
const debounce = (fn, delay) => { let timerId; return (...args) => { clearTimeout(timerId); timerId = setTimeout(() => fn(...args), delay); } };Ejemplo de uso:
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); }} /> ); } Si es posible, el componente principal que pasa la devolución de llamada handleChangeProductName como accesorio probablemente debería manejar la creación de un controlador memorizado y sin rebote, pero lo anterior también debería funcionar.
Echando un vistazo a su implementación de useDebounce , y no parece muy útil como gancho. Parece haberse hecho cargo del trabajo de llamar a su función y no devuelve nada, pero la mayor parte de su implementación se realiza en useTimeout , que tampoco hace mucho...
En mi opinión, useDebounce debería devolver una versión "recuperada" de callback de llamada
Aquí está mi opinión sobre 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; }El uso se verá algo como:
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); }} /> ); }Rebotar onChange en sí mismo tiene advertencias. Digamos que debe ser un componente no controlado, ya que eliminar el rebote de onChange en un componente controlado provocaría retrasos molestos al escribir.
Otro escollo, es posible que tengamos que hacer algo inmediatamente y hacer otra cosa después de un retraso. Por ejemplo, muestre inmediatamente el indicador de carga en lugar de los resultados de búsqueda (obsoletos) después de cualquier cambio, pero envíe la solicitud real solo después de que el usuario deje de escribir.
Con todo esto en mente, en lugar de eliminar el rebote de la devolución de llamada, propongo eliminar el rebote de la sincronización a través de 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)} Y useIsSetlled se recuperará:
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; } donde isFirstRun obviamente nos salva de obtener "oh, no, el usuario cambió algo" después de la representación inicial (cuando el value se cambia de undefined a valor inicial).
Y prevValueRef.current === value no es una parte requerida, pero nos asegura que obtendremos useIsSettled devolviendo false en la misma ejecución de procesamiento, no en la siguiente, solo después de ejecutar useEffect .