Отключение и тайм-аут в React

#javascript #reactjs #ecmascript-6 #react-hooks #debouncing

#javascript #reactjs #ecmascript-6 #реагирующие хуки #отключение

Вопрос:

У меня здесь есть поле ввода, которое для каждого типа отправляет действие 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 amp;amp; 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, []);
}
 

Компонент формы

 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 amp;amp; Boolean(formik.errors.productName)}
      onChange={(e) => {
        formik.setFieldValue("productName", e.target.value);
        useDebounce(() => handleChangeProductName(e.target.value), 1000, [
          e.target.value,
        ]);
      }}
    />
  );
}
 

Комментарии:

1. да, это абсолютно неправильное место для размещения хука. Хуки должны быть размещены за пределами отображаемых элементов. Переместите его внутрь тела родительского компонента TextField

2. Ваш хук вызывается из функции внутри ваших компонентов, это нарушает правило хуков , вы должны использовать хук на верхнем уровне.

3. @smac89. итак, как бы вы переместили его и вызвали из этого?

4. Вы useDebounce сами это определили? Как вы намеревались его использовать?

5. @Bergi. Обновил мой вопрос. Я хочу отправить действие для сокращения, используя handleChangeProductName не для каждого ввода, потому что у меня много текстовых полей, поэтому это было бы тяжело

Ответ №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 amp;amp; Boolean(formik.errors.productName)}
      onChange={(e) => {
        formik.setFieldValue("productName", e.target.value);
        debouncedHandler(e.target.value);
      }}
    />
  );
}
 

Если возможно, родительский компонент, передающий handleChangeProductName обратный вызов в качестве prop, вероятно, должен обрабатывать создание дебатируемого, запоминаемого обработчика, но вышеупомянутое также должно работать.

Комментарии:

1. @Joseph Если пользователь активно вводит текст, вы можете также дождаться его завершения, чтобы не тратить впустую промежуточные асинхронные вызовы на измененный ввод.

2. Как вы можете это сделать с этим? Можете ли вы также отредактировать свой ответ с помощью этого? Спасибо, Дрю

3. @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 amp;amp; Boolean(formik.errors.productName)}
      onChange={(e) => {
        formik.setFieldValue("productName", e.target.value);
        debouncedHandleChangeProductName(e.target.value);
      }}
    />
  );
}
 

Комментарии:

1. Кажется, он просто передает первую букву в props.handleChangeProductName

2. @Joseph попробуйте уменьшить время удаления. Честно говоря, 1 секунда — это слишком долго, чтобы ждать. Попробуйте что-нибудь меньшее, например 150. Что еще следует учитывать, так это то, что debounce принимает только последнее значение после тайм-аута, поэтому вам может потребоваться обновить useDebounce перехват, чтобы отслеживать последние аргументы, переданные в debounce, и вызывать функцию по истечении тайм-аута

3. можете ли вы отредактировать свой ответ с помощью этого?

Ответ №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 amp;amp; prevValueRef.current === value;
}
 

где isFirstRun , очевидно, спасает нас от получения «о, нет, пользователь что-то изменил» после первоначального рендеринга (при value изменении с undefined на начальное значение).

И prevValueRef.current === value не является обязательной частью, но дает нам уверенность, что мы получим useIsSettled возврат false в том же прогоне рендеринга, а не в следующем, только после useEffect выполнения.