I\'m trying to use the throttle
method from lodash
in a functional component, e.g.:
const App = () => {
const [value, setValue
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.
App.jsx
import { useState } from "react";
import { useDebouncedEffect } from "./useDebouncedEffect";
const App = () => {
const [value, setValue] = useState(0)
useDebouncedEffect(() => console.log(value), 1000, [value])
return (
<button onClick={() => setValue(value + 1)}>{value}</button>
)
}
useDebouncedEffect.js
import { useCallback, useEffect } from "react";
export const useDebouncedEffect = (effect, delay , deps) => {
const callback = useCallback(effect, deps);
useEffect(() => {
const handler = setTimeout(() => {
callback();
}, delay);
return () => {
clearTimeout(handler);
};
}, [callback, delay]);
}
This is my useDebounce
:
export function useDebounce(callback, timeout, deps) {
const timeoutId = useRef();
useEffect(() => {
clearTimeout(timeoutId.current);
timeoutId.current = setTimeout(callback, timeout);
return () => clearTimeout(timeoutId.current);
}, deps);
}
And you can use it like this:
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]
);
//
}
In my case I also needed to pass the event. Went with this:
const MyComponent = () => {
const handleScroll = useMemo(() => {
const throttled = throttle(e => console.log(e.target.scrollLeft), 300);
return e => {
e.persist();
return throttled(e);
};
}, []);
return <div onScroll={handleScroll}>Content</div>;
};
I write a simple useDebounce
hook which takes cleanup into consideration, just as useEffect
works.
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;
}
gist is here: https://gist.github.com/sophister/9cc74bb7f0509bdd6e763edbbd21ba64
and this is live demo: https://codesandbox.io/s/react-hook-debounce-demo-mgr89?file=/src/App.js
useage:
const debounceChange = useDebounce(function (e) {
console.log("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} />
Here is an actual throttle hook. You can use in a screen or component for all of the functions you want to throttle, and they will share the same throttle. Or you can call useThrottle()
multiple times and have different throttles for individual functions.
Use like this:
import useThrottle from '../hooks/useThrottle';
const [navigateToSignIn, navigateToCreateAccount] = useThrottle([
() => { navigation.navigate(NavigationRouteNames.SignIn) },
() => { navigation.navigate(NavigationRouteNames.CreateAccount) }
])
And the hook itself:
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;
}