skip to Main Content

When I click ‘CLICK ME’ repeatedly and quickly, why is there a possibility that console.log(`===${innerCount}.2`); is executed earlier over console.log(`===${innerCount}.1`);

Here is the CodeSandBox: https://codesandbox.io/s/loving-blackburn-v4k5y4?file=/src/App.js:651-666

throttle is from lodash.

let count = 0;
const App = () => {
  const throttledFetchData = throttle(async (innerCount) => {
    const result = await callApi();
    console.log(`===${innerCount}.1`);
    return result;
  }, 100);

  const handleClick = throttle(async (e: any) => {
    count++;
    const innerCount = count;
    const result = await throttledFetchData(innerCount);
    console.log(`===${innerCount}.2`);
  }, 50);

  return (
    <div className='container' onClick={handleClick}>
      CLICK ME
    </div>
  );
}

console content

reproduce gif

Why wrapping two throttle?

Because it was a mistake,I’m stuck with the order problem when I debug.

Why did not reproduce the problem?

It only appears when click very quickly.

2

Answers


  1. Don’t know the entire context, but you can try using "useCallback" something like this

    import { useCallback, useRef } from 'react';
    
    const App = () => {
      const countRef = useRef(0);
    
      const throttledFetchData = useCallback(
        throttle(async () => {
          const result = await callApi();
          console.log(`===${countRef.current}.1`);
          return result;
        }, 100),
        []
      );
    
      const handleScroll = useCallback(
        throttle((e) => {
          countRef.current++;
          throttledFetchData();
          console.log(`===${countRef.current}.2`);
        }, 50),
        [throttledFetchData]
      );
    
      return (
        <div className='container' onScroll={handleScroll}>
          SCROLL ME
        </div>
      );
    };
    

    The useCallback hook to memoizes the handleScroll and throttledFetchData functions. This helps prevent unnecessary re-rendering of the component and ensures that we are always working with the latest version of these functions.

    I also added a countRef variable to keep track of the number of times handleScroll is called. This is done using the useRef hook, which creates a mutable variable that persists across re-renders.

    Also, updated the event listener to listen for the "onScroll" event instead of the "onClick" event

    Login or Signup to reply.
  2. The second throttle is not relevant, the cause of the issue is the behaviour of the first throttled function. In particular,

    The func is invoked with the last arguments provided to the throttled function. Subsequent calls to the throttled function return the result of the last func invocation.

    […] func is invoked on the trailing edge of the timeout […] if the throttled function is invoked more than once during the wait timeout.

    lodash documentation

    You can see this more clearly when you return the innerCount, not the callApi() result, from the throttled function, to identify which promise handleClick was waiting for:

    const throttledFetchData = throttle(async (innerCount) => {
      await callApi();
      console.log(`===${innerCount}.1`);
      return innerCount;
    }, 1000);
    
    const handleClick = async (e) => {
      const innerCount = ++count;
      const result = await throttledFetchData(innerCount);
      console.log(`===${innerCount}.2 after ${result}`);
    };
    

    So what’s happening when handleClick is called twice in quick succession:

      • the first handleClick invocation calls throttledFetchData(1)
      • it immediately calls the throttled implementation ("on the leading edge") which starts waiting for callApi()
      • a promise is returned from throttledFetchData(1) and await suspends the execution of handleClick
      • the second handleClick invocation calls throttledFetchData(2)
      • it does defer the execution until 1s after the first call has passed, but immediately returns the promise from the first invocation
      • the await in the second handleClick suspends execution until that old promise is settled
      • after 1s has passed, the deferred execution calls the throttled implementation ("on the trailng edge") which starts waiting for a second callApi()
      • the first callApi() promise fulfills and resumes execution
      • ===1.1 is logged and 1 is returned (fulfilling the promise)
      • the first handleClick is resumed and logs ===1.2 after 1
      • the second handleClick is resumed and logs ===2.2 after 1
      • the second callApi() promise fulfills and resumes execution
      • ===2.1 is logged and 2 is returned, but nobody is waiting for that promise
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search