elderlyman
elderlyman

Reputation: 590

Lodash throttle not throttling?

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

Answers (2)

Omar Borji
Omar Borji

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

nanobar
nanobar

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

Related Questions