How to use throttle or debounce with React Hook?
After some time passed I'm sure it's much easier to handle things by your own with setTimeout/clearTimeout
(and moving that into separate custom hook) than working with functional helpers. Handling later one creates additional challenges right after we apply that to useCallback
that can be recreated because of dependency change but we don't want to reset delay running.
original answer below
you may(and probably need) useRef
to store value between renders. Just like it's suggested for timers
Something like that
const App = () => { const [value, setValue] = useState(0) const throttled = useRef(throttle((newValue) => console.log(newValue), 1000)) useEffect(() => throttled.current(value), [value]) return ( <button onClick={() => setValue(value + 1)}>{value}</button> )}
As for useCallback
:
It may work too as
const throttled = useCallback(throttle(newValue => console.log(newValue), 1000), []);
But if we try to recreate callback once value
is changed:
const throttled = useCallback(throttle(() => console.log(value), 1000), [value]);
we may find it does not delay execution: once value
is changed callback is immediately re-created and executed.
So I see useCallback
in case of delayed run does not provide significant advantage. It's up to you.
[UPD] initially it was
const throttled = useRef(throttle(() => console.log(value), 1000)) useEffect(throttled.current, [value])
but that way throttled.current
has bound to initial value
(of 0) by closure. So it was never changed even on next renders.
So be careful while pushing functions into useRef
because of closure feature.
I've created my own custom hook called useDebouncedEffect
that will wait to perform a useEffect
until the state hasn't updated for the duration of the delay.
In this example, your effect will log to the console after you have stopped clicking the button for 1 second.
Sandbox Examplehttps://codesandbox.io/s/react-use-debounced-effect-6jppw
App.jsx
import { useState } from "react";import { useDebouncedEffect } from "./useDebouncedEffect";const App = () => { const [value, setValue] = useState(0) useDebouncedEffect(() => console.log(value), [value], 1000); return ( <button onClick={() => setValue(value + 1)}>{value}</button> )}export default App;
useDebouncedEffect.js
import { 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]);}
The comment to disable exhaustive-deps is required unless you want to see a warning because lint will always complain about not having effect as a dependency. Adding effect as a dependency will trigger the useEffect on every render. Instead, you can add the check to useDebouncedEffect
to make sure it's being passed all of the dependencies. (see below)
Adding exhaustive dependencies check to useDebouncedEffect
If you want to have eslint check useDebouncedEffect
for exhaustive dependencies, you can add it to the eslint config in package.json
"eslintConfig": { "extends": [ "react-app" ], "rules": { "react-hooks/exhaustive-deps": ["warn", { "additionalHooks": "useDebouncedEffect" }] } },
useThrottle
, useDebounce
How to use both
const App = () => { const [value, setValue] = useState(0); // called at most once per second (same API with useDebounce) const throttledCb = useThrottle(() => console.log(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
(Lodash)
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] );}
useDebounce
(Lodash)
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] );}
Customizations
1. You might replace Lodash with your own throttle
or debounce
code, like:
2. useThrottle
can be shortened up, if always used with useEffect
(same for useDebounce
):
const App = () => { // useEffect now is contained inside useThrottle useThrottle(() => console.log(value), 1000, [value]); // ...};