Reputation: 590
I'm trying to apply a lodash throttle for the first time.
I know that the throttle has to be applied inside of a useCallback
or it will be called every re-render (in my case, with every new keystroke of a user search).
The code I have is valid, and the logic seems to make sense - but the throttle isn't being applied, and so the api call is being made every single keystroke.
Any pointers as to where my logic is failing?
import {
useEffect,
useCallback
} from 'react';
import { throttle } from 'lodash';
import { getAllUsers } from '../../../api/api';
import { USER_ROLE } from '../../../types/types'
interface IProps extends Omit<unknown, 'children'> {
search?: string;
}
const DemoFanManagementTable = ({ search }: IProps): JSX.Element => {
const getFans = (search?: string) => {
console.log("getFans ran")
const fans = getAllUsers({ search }, USER_ROLE.FAN);
//logs a promise
console.log("logging fans ", fans)
return fans;
}
//throttledSearch is running every time search changes
const throttledSearch = useCallback((search?: string) => {
console.log("throttledSearch ran")
return throttle(
//throttle is not throttling, functions run every keystroke
() => {
getFans(search), 10000, { leading: true, trailing: true }
}
)
}, [search])
//useEffect is running every time search changes
useEffect(() => {
return throttledSearch(search)
}, [search]);
return (
<div>
{search}
</div>
);
};
export default DemoFanManagementTable;
Upvotes: 0
Views: 4021
Reputation: 350
is some cases it didnt work with Lodash So i did a custom hook
// Custom hook to create a throttled function that only invokes the first callback within the specified delay period and ignores subsequent ones.
export const useOnPressThrottle = (
callback: ExplicitAny,
delay = 500,
waitingRef: React.MutableRefObject<boolean> | null = null,
) => {
let waitingRefObj = useRef(false);
if (waitingRef) {
waitingRefObj = waitingRef;
}
// Ref to keep track of whether we're waiting to allow another invocation.
return useCallback(
(...args: ExplicitAny) => {
// If we are already waiting, do nothing.
if (waitingRefObj.current) return;
waitingRefObj.current = true;
// Otherwise, invoke the callback and set the waiting flag.
callback?.(...args);
// Set up a timer to reset the waiting flag after the specified delay.
setTimeout(() => {
waitingRefObj.current = false;
}, delay);
},
[callback, delay, waitingRefObj]
); // Dependencies for useCallback.
};
usage:
const onPressThrottle = useOnPressThrottle(onPress);
const onPressThrottle = useOnPressThrottle(onPress, 10000);
Upvotes: 1
Reputation: 66355
There are a few problems here, first you have wrapped the whole throttle
func in an anonymous function instead of just the first param:
throttle(
(search: string) => getFans(search),
1000,
{ leading: true, trailing: true }
)
Second useCallback
is not suitable as each time you call it, it's returning a new throttled function.
Third you have passed [search]
as a dependency of useCallback
so even if it worked as you expected, it would be invalidated each time search
changes and not work anyway.
A better choice is useMemo
as it keeps the same throttled function across renders.
const throttledSearch = useMemo(
() =>
throttle(
(search: string) => getFans(search),
10000,
{ leading: true, trailing: true }
),
[]
);
useEffect(() => {
throttledSearch(search);
}, [search]);
Since getFans takes the same search param you can shorten it to:
const throttledSearch = useMemo(() =>
throttle(getFans, 10000, { leading: true, trailing: true }),
[]);
Upvotes: 1