Я пытаюсь использовать метод throttle из lodash в функциональном компоненте, например:
const App = () => {
const [value, setValue] = useState(0)
useEffect(throttle(() => console.info(value), 1000), [value])
return (
<button onClick = {() => setValue(value + 1)}>{value}</button>
)
}
Поскольку метод внутри useEffect переопределяется при каждом рендеринге, эффект дросселирования не работает.
У кого-нибудь есть простое решение?
Да, я пробовал, и это работает, но в моем случае это не очень элегантно, потому что я использую переменные компонента внутри метода дросселирования.





По прошествии некоторого времени я уверен, что гораздо проще справляться с вещами самостоятельно с помощью setTimeout/clearTimeout (и перемещая это в отдельный пользовательский хук), чем работать с функциональными помощниками. Обработка позже одного создает дополнительные проблемы сразу после того, как мы применим это к useCallback, который может быть воссоздан из-за изменения зависимости, но мы не хотим сбрасывать отложенное выполнение.
оригинальный ответ ниже
вам может (и, вероятно, понадобится) useRef для хранения значения между рендерами. Так же, как это рекомендуется для таймеров
Что-то такое
const App = () => {
const [value, setValue] = useState(0)
const throttled = useRef(throttle((newValue) => console.info(newValue), 1000))
useEffect(() => throttled.current(value), [value])
return (
<button onClick = {() => setValue(value + 1)}>{value}</button>
)
}
Что касается useCallback:
Это может работать также как
const throttled = useCallback(throttle(newValue => console.info(newValue), 1000), []);
Но если мы попытаемся воссоздать обратный вызов после изменения value:
const throttled = useCallback(throttle(() => console.info(value), 1000), [value]);
мы можем обнаружить, что это не задерживает выполнение: как только value изменяется, обратный вызов немедленно воссоздается и выполняется.
Так что я вижу useCallback в случае отложенного запуска не дает существенного преимущества. Тебе решать.
[UPD] изначально было
const throttled = useRef(throttle(() => console.info(value), 1000))
useEffect(throttled.current, [value])
но таким образом throttled.current связался с начальным value(0) замыканием. Поэтому он никогда не менялся даже на следующих рендерах.
Так что будьте осторожны, помещая функции в useRef из-за функции закрытия.
useRef должен быть ответом, но этот код не работает. Я пытаюсь это исправить.
возможно, я пропустил начальное значение этой части, поскольку useRef делает закрытие до начального значения
Throttle-debounce сначала использует DELAY, затем CALLBACK;)
@mikes это зависит (для версии lodash есть опции leading и trailing для настройки github.com/lodash/lodash/blob/master/throttle.js)
поскольку мы используем хуки, это означает, что может быть необходимо переопределять эффект или обратный вызов каждый раз, когда мы рендерим, и в то же время должно работать устранение дребезга. так что это не приемлемый ответ
@hossein alipur не может не согласиться с вами. после того, как прошел почти год с тех пор, как я ответил, теперь я вижу, что лучше контролировать вещи непосредственно внутри useEffect с помощью setTimeout/clearTimeout, чем использовать вспомогательную функцию debounce. Спасибо, что подняли это.
Мы можем использовать useRef для создания обратного вызова и его сохранения, но я считаю, что лучше использовать useCallback даже для передачи необходимых переменных, что случается редко. Мы можем использовать setValue, чтобы изменить значение внутри useCallback, не добавляя value в массив зависимостей, и даже получить доступ к предыдущему значению, используя setValue(previous => ...). Если нам нужен прямой доступ к значению без его изменения, мы можем передать его в качестве аргумента, как вы делаете с useRef в своем примере, например useCallback(throttle((value) => { ... }, 1000), []).
Спасибо @ChristosLytras. Я дал ответ ниже, как реализовать его с помощью хука useCallback.
Итак, какая часть этого ответа является фактическим ответом? Это немного извилисто.
Этот ответ настолько сбивает с толку, согласен с @coler-j
Я написал два простых хука (использовать эффект дросселирования и использовать дебаунс-эффект) для этого варианта использования, возможно, это будет полезно для кого-то еще, кто ищет простое решение.
import React, { useState } from 'react';
import useThrottledEffect from 'use-throttled-effect';
export default function Input() {
const [count, setCount] = useState(0);
useEffect(()=>{
const interval = setInterval(() => setCount(count=>count+1) ,100);
return ()=>clearInterval(interval);
},[])
useThrottledEffect(()=>{
console.info(count);
}, 1000 ,[count]);
return (
{count}
);
}
сработало отлично, спасибо ? Протестировал оба крючка
Если вы используете его в обработчике, я совершенно уверен, что это способ сделать это.
function useThrottleScroll() {
const savedHandler = useRef();
function handleEvent() {}
useEffect(() => {
savedHandleEvent.current = handleEvent;
}, []);
const throttleOnScroll = useRef(throttle((event) => savedHandleEvent.current(event), 100)).current;
function handleEventPersistence(event) {
return throttleOnScroll(event);
}
return {
onScroll: handleEventPersistence,
};
}
Я использую что-то вроде этого, и он отлично работает:
let debouncer = debounce(
f => f(),
1000,
{ leading: true }, // debounce one on leading and one on trailing
);
function App(){
let [state, setState] = useState();
useEffect(() => debouncer(()=>{
// you can use state here for new state value
}),[state])
return <div />
}
откуда debounce()?
Это может быть крошечный пользовательский хук, например:
useDebounce.js
import React, { useState, useEffect } from 'react';
export default (value, timeout) => {
const [state, setState] = useState(value);
useEffect(() => {
const handler = setTimeout(() => setState(value), timeout);
return () => clearTimeout(handler);
}, [value, timeout]);
return state;
}
Пример использования:
import React, { useEffect } from 'react';
import useDebounce from '/path/to/useDebounce';
const App = (props) => {
const [state, setState] = useState({title: ''});
const debouncedTitle = useDebounce(state.title, 1000);
useEffect(() => {
// do whatever you want with state.title/debouncedTitle
}, [debouncedTitle]);
return (
// ...
);
}
// ...
Примечание: Как вы, наверное, знаете, useEffect всегда запускается при начальном рендеринге, и из-за этого, если вы используете мой ответ, вы, вероятно, увидите, что рендеринг вашего компонента выполняется дважды, не волнуйтесь, вам просто нужно написать еще один пользовательский хук. проверьте мой другой ответ для получения дополнительной информации.
Я не понимаю, как избежать второго (или первого) рендера, даже используя связанный хук. Не могли бы вы привести пример? Спасибо
@andreapier Я уже добавил ссылку на другой пользовательский хук, чтобы предотвратить рендеринг при начальном рендеринге, если вы его не видели, вот ссылка: stackoverflow.com/a/57941438/3367974
Да, я это видел. Мой вопрос был о том, как заставить их работать вместе. Однако я переключился на другое решение, так как это (на мой взгляд) создает слишком много проблем.
В моем случае мне также нужно было пройти событие. Пошел с этим:
const MyComponent = () => {
const handleScroll = useMemo(() => {
const throttled = throttle(e => console.info(e.target.scrollLeft), 300);
return e => {
e.persist();
return throttled(e);
};
}, []);
return <div onScroll = {handleScroll}>Content</div>;
};
Я создал свой собственный хук под названием useDebouncedEffect, который будет ждать выполнения useEffect до тех пор, пока состояние не обновится на время задержки.
В этом примере ваш эффект будет зарегистрирован в консоли после того, как вы перестанете нажимать кнопку в течение 1 секунды.
Пример песочницыhttps://codesandbox.io/s/react-use-debounced-effect-6jppw
App.jsximport { useState } from "react";
import { useDebouncedEffect } from "./useDebouncedEffect";
const App = () => {
const [value, setValue] = useState(0)
useDebouncedEffect(() => console.info(value), [value], 1000);
return (
<button onClick = {() => setValue(value + 1)}>{value}</button>
)
}
export default App;
useDebouncedEffect.jsimport { useEffect } from "react";
export const useDebouncedEffect = (effect, deps, delay) => {
useEffect(() => {
const handler = setTimeout(() => effect(), delay);
return () => clearTimeout(handler);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...deps || [], delay]);
}
Комментарий для отключения исчерпывающих отложений обязателен, если вы не хотите видеть предупреждение, потому что lint всегда будет жаловаться на отсутствие эффекта в качестве зависимости. Добавление эффекта в качестве зависимости будет запускать useEffect при каждом рендеринге. Вместо этого вы можете добавить проверку в useDebouncedEffect, чтобы убедиться, что он передает все зависимости. (увидеть ниже)
Добавление исчерпывающей проверки зависимостей в useDebouncedEffect
Если вы хотите, чтобы eslint проверял useDebouncedEffect исчерпывающие зависимости, вы можете добавить его в конфигурацию eslint в package.json
"eslintConfig": {
"extends": [
"react-app"
],
"rules": {
"react-hooks/exhaustive-deps": ["warn", {
"additionalHooks": "useDebouncedEffect"
}]
}
},
Если вам интересно, зачем нужен useCallback, я думаю, причина в следующем: функции в JavaScript не имеют ссылочного равенства (то есть () => {} === () => {} // false). Поэтому каждый раз, когда компонент перерисовывается effect, он не такой, как был раньше. Однако, используя useCallback, вы говорите React: «Пожалуйста, считайте, что я изменился только тогда, когда мои deps тоже изменились!»
Функции @David действительно имеют ссылочное равенство, поэтому вам нужно useCallback в первую очередь. Ваш пример относится к структурному равенству, а не к ссылочному равенству.
@KevinBeal, я не думаю, что слышал термин «структурное равенство» раньше, и быстрый поиск в Интернете (на языке Kotlin) говорит, что ссылочное — это ===, а структурное — ==. Согласно этой логике, мне кажется, что функции имеют структурное равенство в JavaScript.
Структурное равенство @David просто означает, что значения одинаковы внутри, с одинаковыми ключами, значениями и т. д. Это равенство значений или как бы вы это ни называли.
useThrottle , useDebounceconst App = () => {
const [value, setValue] = useState(0);
// called at most once per second (same API with useDebounce)
const throttledCb = useThrottle(() => console.info(value), 1000);
// usage with useEffect: invoke throttledCb on value change
useEffect(throttledCb, [value]);
// usage as event handler
<button onClick = {throttledCb}>log value</button>
// ... other render code
};
useThrottle (Лодаш)import _ from "lodash"
function useThrottle(cb, delay) {
const options = { leading: true, trailing: false }; // add custom lodash options
const cbRef = useRef(cb);
// use mutable ref to make useCallback/throttle not depend on `cb` dep
useEffect(() => { cbRef.current = cb; });
return useCallback(
_.throttle((...args) => cbRef.current(...args), delay, options),
[delay]
);
}
const App = () => {
const [value, setValue] = useState(0);
const invokeDebounced = useThrottle(
() => console.info("changed throttled value:", value),
1000
);
useEffect(invokeDebounced, [value]);
return (
<div>
<button onClick = {() => setValue(value + 1)}>{value}</button>
<p>value will be logged at most once per second.</p>
</div>
);
};
function useThrottle(cb, delay) {
const options = { leading: true, trailing: false }; // pass custom lodash options
const cbRef = useRef(cb);
useEffect(() => {
cbRef.current = cb;
});
return useCallback(
_.throttle((...args) => cbRef.current(...args), delay, options),
[delay]
);
}
ReactDOM.render(<App />, document.getElementById("root"));<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity = "sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4 = " crossorigin = "anonymous"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity = "sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w = " crossorigin = "anonymous"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js" integrity = "sha256-VeNaFBVDhoX3H+gJ37DpT/nTuZTdjYro9yBruHjVmoQ = " crossorigin = "anonymous"></script>
<script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>
<div id = "root"></div>useDebounce (Лодаш)import _ from "lodash"
function useDebounce(cb, delay) {
// ...
const inputsRef = useRef({cb, delay}); // mutable ref like with useThrottle
useEffect(() => { inputsRef.current = { cb, delay }; }); //also track cur. delay
return useCallback(
_.debounce((...args) => {
// Debounce is an async callback. Cancel it, if in the meanwhile
// (1) component has been unmounted (see isMounted in snippet)
// (2) delay has changed
if (inputsRef.current.delay === delay && isMounted())
inputsRef.current.cb(...args);
}, delay, options
),
[delay, _.debounce]
);
}
const App = () => {
const [value, setValue] = useState(0);
const invokeDebounced = useDebounce(
() => console.info("debounced", value),
1000
);
useEffect(invokeDebounced, [value]);
return (
<div>
<button onClick = {() => setValue(value + 1)}>{value}</button>
<p> Logging is delayed until after 1 sec. has elapsed since the last invocation.</p>
</div>
);
};
function useDebounce(cb, delay) {
const options = {
leading: false,
trailing: true
};
const inputsRef = useRef(cb);
const isMounted = useIsMounted();
useEffect(() => {
inputsRef.current = { cb, delay };
});
return useCallback(
_.debounce(
(...args) => {
// Don't execute callback, if (1) component in the meanwhile
// has been unmounted or (2) delay has changed
if (inputsRef.current.delay === delay && isMounted())
inputsRef.current.cb(...args);
},
delay,
options
),
[delay, _.debounce]
);
}
function useIsMounted() {
const isMountedRef = useRef(true);
useEffect(() => {
return () => {
isMountedRef.current = false;
};
}, []);
return () => isMountedRef.current;
}
ReactDOM.render(<App />, document.getElementById("root"));<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity = "sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4 = " crossorigin = "anonymous"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity = "sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w = " crossorigin = "anonymous"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js" integrity = "sha256-VeNaFBVDhoX3H+gJ37DpT/nTuZTdjYro9yBruHjVmoQ = " crossorigin = "anonymous"></script>
<script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>
<div id = "root"></div>1. Вы можете заменить Lodash своим собственным кодом throttle или debounce, например:
const debounceImpl = (cb, delay) => {
let isDebounced = null;
return (...args) => {
clearTimeout(isDebounced);
isDebounced = setTimeout(() => cb(...args), delay);
};
};
const throttleImpl = (cb, delay) => {
let isThrottled = false;
return (...args) => {
if (isThrottled) return;
isThrottled = true;
cb(...args);
setTimeout(() => {
isThrottled = false;
}, delay);
};
};
const App = () => {
const [value, setValue] = useState(0);
const invokeThrottled = useThrottle(
() => console.info("throttled", value),
1000
);
const invokeDebounced = useDebounce(
() => console.info("debounced", value),
1000
);
useEffect(invokeThrottled, [value]);
useEffect(invokeDebounced, [value]);
return <button onClick = {() => setValue(value + 1)}>{value}</button>;
};
function useThrottle(cb, delay) {
const cbRef = useRef(cb);
useEffect(() => {
cbRef.current = cb;
});
return useCallback(
throttleImpl((...args) => cbRef.current(...args), delay),
[delay]
);
}
function useDebounce(cb, delay) {
const cbRef = useRef(cb);
useEffect(() => {
cbRef.current = cb;
});
return useCallback(
debounceImpl((...args) => cbRef.current(...args), delay),
[delay]
);
}
ReactDOM.render(<App />, document.getElementById("root"));<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity = "sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4 = " crossorigin = "anonymous"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity = "sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w = " crossorigin = "anonymous"></script>
<script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>
<div id = "root"></div>2. useThrottle можно сократить, если всегда использовать с useEffect (то же самое для useDebounce):
const App = () => {
// useEffect now is contained inside useThrottle
useThrottle(() => console.info(value), 1000, [value]);
// ...
};
const App = () => {
const [value, setValue] = useState(0);
useThrottle(() => console.info(value), 1000, [value]);
return (
<div>
<button onClick = {() => setValue(value + 1)}>{value}</button>
<p>value will be logged at most once per second.</p>
</div>
);
};
function useThrottle(cb, delay, additionalDeps) {
const options = { leading: true, trailing: false }; // pass custom lodash options
const cbRef = useRef(cb);
const throttledCb = useCallback(
_.throttle((...args) => cbRef.current(...args), delay, options),
[delay]
);
useEffect(() => {
cbRef.current = cb;
});
// set additionalDeps to execute effect, when other values change (not only on delay change)
useEffect(throttledCb, [throttledCb, ...additionalDeps]);
}
ReactDOM.render(<App />, document.getElementById("root"));<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity = "sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4 = " crossorigin = "anonymous"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity = "sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w = " crossorigin = "anonymous"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js" integrity = "sha256-VeNaFBVDhoX3H+gJ37DpT/nTuZTdjYro9yBruHjVmoQ = " crossorigin = "anonymous"></script>
<script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>
<div id = "root"></div>Зачем использовать useEffect(() => { cbRef.current = cb; }); без какой-либо зависимости? Это означает, что мы запускаем эффект при каждом повторном рендеринге, так почему бы просто не назначить без useEffect?
Хороший вопрос — он предназначен для того, чтобы всегда содержать самый последний обратный вызов внутри cbRef. Изменяемую ссылку можно использовать как переменная экземпляра для хуков — здесь является примером с setInterval из блога Overreacted. Фаза рендеринга также должна быть чистой без побочных эффектов, например. для совместимости с параллельным режимом React. Вот почему мы заключаем задание внутрь useEffect.
Кажется, я получаю сообщение об ошибке при использовании useThrottle (Lodash): «TypeError: Cannot read property 'apply' of undefined». В сочетании с этим у меня есть ошибка ESLint, говорящая: «React Hook useCallback получил функцию, зависимости которой неизвестны. Вместо этого передайте встроенную функцию».
Используя функцию debounce lodash, вот что я делаю:
import debounce from 'lodash/debounce'
// The function that we want to debounce, for example the function that makes the API calls
const getUsers = (event) => {
// ...
}
// The magic!
const debouncedGetUsers = useCallback(debounce(getUsers, 500), [])
В вашем JSX:
<input value = {value} onChange = {debouncedGetUsers} />
Я довольно поздно с этим, но вот способ опровергнуть setState()
/**
* Like React.setState, but debounces the setter.
*
* @param {*} initialValue - The initial value for setState().
* @param {int} delay - The debounce delay, in milliseconds.
*/
export const useDebouncedState = (initialValue, delay) => {
const [val, setVal] = React.useState(initialValue);
const timeout = React.useRef();
const debouncedSetVal = newVal => {
timeout.current && clearTimeout(timeout.current);
timeout.current = setTimeout(() => setVal(newVal), delay);
};
React.useEffect(() => () => clearTimeout(timeout.current), []);
return [val, debouncedSetVal];
};
Это мой useDebounce:
export function useDebounce(callback, timeout, deps) {
const timeoutId = useRef();
useEffect(() => {
clearTimeout(timeoutId.current);
timeoutId.current = setTimeout(callback, timeout);
return () => clearTimeout(timeoutId.current);
}, deps);
}
И вы можете использовать его следующим образом:
const TIMEOUT = 500; // wait 500 milliseconds;
export function AppContainer(props) {
const { dataId } = props;
const [data, setData] = useState(null);
//
useDebounce(
async () => {
data = await loadDataFromAPI(dataId);
setData(data);
},
TIMEOUT,
[dataId]
);
//
}
Я пишу простой useDebounce хук, который учитывает очистку, как работает useEffect.
import { useState, useEffect, useRef, useCallback } from "react";
export function useDebounceState<T>(initValue: T, delay: number) {
const [value, setValue] = useState<T>(initValue);
const timerRef = useRef(null);
// reset timer when delay changes
useEffect(
function () {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
},
[delay]
);
const debounceSetValue = useCallback(
function (val) {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
timerRef.current = setTimeout(function () {
setValue(val);
}, delay);
},
[delay]
);
return [value, debounceSetValue];
}
interface DebounceOptions {
imediate?: boolean;
initArgs?: any[];
}
const INIT_VALUE = -1;
export function useDebounce(fn, delay: number, options: DebounceOptions = {}) {
const [num, setNum] = useDebounceState(INIT_VALUE, delay);
// save actual arguments when fn called
const callArgRef = useRef(options.initArgs || []);
// save real callback function
const fnRef = useRef(fn);
// wrapped function
const trigger = useCallback(function () {
callArgRef.current = [].slice.call(arguments);
setNum((prev) => {
return prev + 1;
});
}, []);
// update real callback
useEffect(function () {
fnRef.current = fn;
});
useEffect(
function () {
if (num === INIT_VALUE && !options.imediate) {
// prevent init call
return;
}
return fnRef.current.apply(null, callArgRef.current);
},
[num, options.imediate]
);
return trigger;
}
суть здесь: https://gist.github.com/sophister/9cc74bb7f0509bdd6e763edbbd21ba64
а это живая демонстрация: https://codesandbox.io/s/react-hook-debounce-demo-mgr89?file=/src/App.js
использование:
const debounceChange = useDebounce(function (e) {
console.info("debounced text change: " + e.target.value);
}, 500);
// can't use debounceChange directly, since react using event pooling
function deboucnedCallback(e) {
e.persist();
debounceChange(e);
}
// later the jsx
<input onChange = {deboucnedCallback} />
Я хотел бы присоединиться к вечеринке со своим заблокированным и отклоненным вводом, используя useState:
// import { useState, useRef } from 'react' // nomral import
const { useState, useRef } = React // inline import
// Throttle
const ThrottledInput = ({ onChange, delay = 500 }) => {
const t = useRef()
const handleChangeEvent = ({ target }) => {
if (!t.current) {
t.current = setTimeout(() => {
onChange(target.value)
clearTimeout(t)
t.current = null
}, delay)
}
}
return (
<input
placeholder = "throttle"
onChange = {handleChangeEvent}
/>
)
}
// Debounce
const DebouncedInput = ({ onChange, delay = 500 }) => {
const t = useRef()
const handleChangeEvent = ({ target }) => {
clearTimeout(t.current)
t.current = setTimeout(() => onChange(target.value), delay)
}
return (
<input
placeholder = "debounce"
onChange = {handleChangeEvent}
/>
)
}
// ----
ReactDOM.render(<div>
<ThrottledInput onChange = {console.info} />
<DebouncedInput onChange = {console.info} />
</div>, document.getElementById('root'))<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/16.8.1/umd/react.production.min.js"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.1/umd/react-dom.production.min.js"></script>
<div id = "root"></div>Вот настоящий дроссельный крюк. Вы можете использовать экран или компонент для всех функций, которые хотите регулировать, и они будут использовать одно и то же регулирование. Или вы можете вызывать useThrottle() несколько раз и иметь разные дроссели для отдельных функций.
Используйте так:
import useThrottle from '../hooks/useThrottle';
const [navigateToSignIn, navigateToCreateAccount] = useThrottle([
() => { navigation.navigate(NavigationRouteNames.SignIn) },
() => { navigation.navigate(NavigationRouteNames.CreateAccount) }
])
И сам крючок:
import { useCallback, useState } from "react";
// Throttles all callbacks on a component within the same throttle.
// All callbacks passed in will share the same throttle.
const THROTTLE_DURATION = 500;
export default (callbacks: Array<() => any>) => {
const [isWaiting, setIsWaiting] = useState(false);
const throttledCallbacks = callbacks.map((callback) => {
return useCallback(() => {
if (!isWaiting) {
callback()
setIsWaiting(true)
setTimeout(() => {
setIsWaiting(false)
}, THROTTLE_DURATION);
}
}, [isWaiting]);
})
return throttledCallbacks;
}
Вот простой хук, чтобы отклонить ваши звонки.
Чтобы использовать приведенный ниже код, все, что вам нужно сделать, это объявить его как таковой.
const { debounceRequest } = useDebounce(someFn);
И тогда назовите это так
debounceRequest();
Реализация показана ниже
import React from "react";
const useDebounce = (callbackFn: () => any, timeout: number = 500) => {
const [sends, setSends] = React.useState(0);
const stabilizedCallbackFn = React.useCallback(callbackFn, [callbackFn]);
const debounceRequest = () => {
setSends(sends + 1);
};
// 1st send, 2nd send, 3rd send, 4th send ...
// when the 2nd send comes, then 1st set timeout is cancelled via clearInterval
// when the 3rd send comes, then 2nd set timeout is cancelled via clearInterval
// process continues till timeout has passed, then stabilizedCallbackFn gets called
// return () => clearInterval(id) is critical operation since _this_ is what cancels
// the previous send.
// *? return () => clearInterval(id) is called for the previous send when a new send
// is sent. Essentially, within the timeout all but the last send gets called.
React.useEffect(() => {
if (sends > 0) {
const id = window.setTimeout(() => {
stabilizedCallbackFn();
setSends(0);
}, timeout);
return () => {
return window.clearInterval(id);
};
}
}, [stabilizedCallbackFn, sends, timeout]);
return {
debounceRequest,
};
};
export default useDebounce;
const useDebounce = (func: any) => {
const debounceFunc = useRef(null);
useEffect(() => {
if (func) {
// @ts-ignore
debounceFunc.current = debounce(func, 1000);
}
}, []);
const debFunc = () => {
if (debounceFunc.current) {
return debounceFunc.current;
}
return func;
};
return debFunc();
};
Дебунс с помощью хука useCallback.
import React, { useState, useCallback } from 'react';
import debounce from 'lodash.debounce';
function App() {
const [value, setValue] = useState('');
const [dbValue, saveToDb] = useState(''); // would be an API call normally
// highlight-starts
const debouncedSave = useCallback(
debounce(nextValue => saveToDb(nextValue), 1000),
[], // will be created only once initially
);
// highlight-ends
const handleChangeEvent = event => {
const { value: nextValue } = event.target;
setValue(nextValue);
// Even though handleChangeEvent is created on each render and executed
// it references the same debouncedSave that was created initially
debouncedSave(nextValue);
};
return <div></div>;
}
react-table имеет хорошую useAsyncDebounce функцию, представленную на https://react-table.tanstack.com/docs/faq#how-can-i-debounce-rapid-table-state-changes
И еще одна реализация. Пользовательский крючок:
function useThrottle (func, delay) {
const [timeout, saveTimeout] = useState(null);
const throttledFunc = function () {
if (timeout) {
clearTimeout(timeout);
}
const newTimeout = setTimeout(() => {
func(...arguments);
if (newTimeout === timeout) {
saveTimeout(null);
}
}, delay);
saveTimeout(newTimeout);
}
return throttledFunc;
}
и использование:
const throttledFunc = useThrottle(someFunc, 200);
Надеюсь, это поможет кому-то.
Я сделал простой хук для создания экземпляров дросселя.
Он использует немного другой подход, передавая функцию для вызова каждый раз, а не пытаясь обернуть ее и управлять мутациями. Многие другие решения не учитывают, что функция может вызвать потенциальное изменение. Паттерн хорошо работает с газом или дебаунсом.
// useThrottle.js
import React, { useCallback } from 'react';
import throttle from 'lodash/throttle';
export function useThrottle(timeout = 300, opts = {}) {
return useCallback(throttle((fn, ...args) => {
fn(...args);
}, timeout, opts), [timeout]);
}
Пример использования:
...
const throttleX = useThrottle(100);
const updateX = useCallback((event) => {
// do something!
}, [someMutableValue])
return (
<div onPointerMove = {(event) => throttleX(updateX, event)}></div>
)
...
Я считаю, что этот хук работает правильно, давая возможность немедленно выстрелить.
import { useState, useRef, useEffect } from 'react';
const useDebounce = <T>(
value: T,
timeout: number,
immediate: boolean = true
): T => {
const [state, setState] = useState<T>(value);
const handler = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
useEffect(() => {
if (handler.current) {
clearTimeout(handler.current);
handler.current = undefined;
} else if (immediate) {
setState(value);
}
handler.current = setTimeout(() => {
setState(value);
handler.current = undefined;
}, timeout);
}, [value, timeout, immediate]);
return state;
};
export default useDebounce;
Я только что придумал следующий шаблон, пытаясь решить проблему с устаревшим состоянием:
Мы можем сохранить отмененную функцию в ref и обновлять ее каждый раз, когда компонент перерисовывается в useEffect следующим образом:
// some state
const [counter, setCounter] = useState(0);
// store a ref to the function we will debounce
const increment = useRef(null);
// update the ref every time the component rerenders
useEffect(() => {
increment.current = () => {
setCounter(counter + 1);
};
});
// debounce callback, which we can call (i.e. in button.onClick)
const debouncedIncrement = useCallback(
debounce(() => {
if (increment) {
increment.current();
}
}, 1500),
[]
);
// cancel active debounces on component unmount
useEffect(() => {
return () => {
debouncedIncrement.cancel();
};
}, []);
Песочница кода: https://codesandbox.io/s/debounced-function-ref-pdrfu?file=/src/index.js
Я надеюсь, что это сэкономит кому-то несколько часов борьбы
Вы можете использовать хук useMemo для оптимизации обработчика регулируемых событий.
Пример кода ниже:
const App = () => {
const [value, setValue] = useState(0);
// ORIGINAL EVENT HANDLER
function eventHandler(event) {
setValue(value + 1);
}
// THROTTLED EVENT HANDLER
const throttledEventHandler = useMemo(() => throttle(eventHandler, 1000), [value]);
return (
<button onClick = {throttledEventHandler}>Throttled Button with value: {value}</button>
)
}
Эта памятка обновляет состояние, это нормально? Меня интересует эта инструкция от React: «Помните, что функция, переданная в useMemo, запускается во время рендеринга. Не делайте там ничего, что вы обычно не делаете во время рендеринга. Например, побочные эффекты относятся к useEffect, а не к useMemo».
Можно ли определить функцию дросселирования вне компонента
Appи просто вызвать ее в функцииuseEffect?