Search code examples
javascriptreactjstypescriptlodashthrottling

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;

Solution

  • 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 }),
    []);