When THE author was working on a project, QA feedback said that multiple clicks generated multiple data when submitting a form, obviously because throttling and anti-shock were not done. However, the standard JS version is obviously not applicable to the use of react-hook function components (closure traps in various scenarios are explained in detail in the article). This article will write a react-hook version of throttling and anti-shock (although there are many third-party library solutions, it is recommended to write your own if you only want to use some of the tools).

What are throttling and anti-shaking?

Let’s take a look at the concepts usually found on Baidu:

Stabilization means that the function will only be executed once (the last time) for N seconds, and if it is triggered again within N seconds, the delay time is recalculated. The purpose of the throttling function is to specify a unit of time within which the function execution can be triggered at most once. If the function is triggered multiple times within the unit of time, only one execution can take effect.

What still don’t understand?

If you play a lot of games you can understand this:

  • For anti-shake: Anti-shake is the fixed cast forward shake required for skill release, repeated release will recalculate the forward shake, forward shake ends skill cast.
  • For throttling: throttling is the cooldown time of the skill. While in the cooldown state, nothing happens when the skill is repeatedly cast. Only when the cooldown is good can the skill successfully cast correctly.

That would make sense.

Common JS version of anti-shake throttling

Stabilization:

function debounce(fn, delay) {
    let timer = null
    return function () {
        if (timer) clearTimeout(timer)
        timer = setTimeout(() => {
            fn.call(this)
            timer = null
        }, delay)
    }
}
Copy the code

Throttling:

function throttle(fn, delay) { let timer = null return function () { if (timer) return fn.call(this) timer = setTimeout(() => { timer = null }, delay); }}Copy the code

Why isn’t react used correctly?

Here is an example of anti-shake (TS version) :

function debounce(fn: Function, delay: number) {
  let timer: NodeJS.Timer | null = null
  return function () {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      fn.call(this)
      timer = null
    }, delay)
  }
}
Copy the code

The test code is as follows:

const handleClick = debounce( () => { console.log('you click! ')}, 1001) useEffect (() = > {/ / simulate click twice (() = > {handleClick setTimeout () (() = > {handleClick ()}, 1000)})() const t = setInterval(() => {console.log('1 second passed ')}, 1000); return () => { clearInterval(t) } }, [])Copy the code

You can see that the final correct one was printed two seconds later:

It seems that everything is fine, so let me switch to another use case:

  const [counter1, setCounter1] = useState(0);
  const [counter2, setCounter2] = useState(0);

  const handleClick = debounce(function () {
    console.count('click1')
    setCounter1(counter1 + 1)
  }, 1000)

  useEffect(function () {
    const t = setInterval(() => {
      setCounter2(x => x + 1)
    }, 500);
    return () => clearInterval(t)
  }, [])


  return <div style={{ padding: 30 }}>
    <button
      onClick={handleClick}
    >click</button>
    <p>c1:{counter1}c2:{counter2}</p>
  </div>
Copy the code

When I quickly click, the console prints:

You can see that after I triggered the component update, our Debounce failed. One might say, why not use useCallback for caching? Yes, we can use useCallback to solve this problem, but here’s another use case:

  const [counter1, setCounter1] = useState(0);
  const [counter2, setCounter2] = useState(0);

  const handleClick = useCallback(
    debounce(function () {
      console.count('click1')
      setCounter1(counter1 + 1)
    }, 1000),
    [],
  )

  useEffect(function () {
    const t = setInterval(() => {
      setCounter2(x => x + 1)
    }, 500);
    return () => clearInterval(t)
  }, [])


  return <div style={{ padding: 30 }}>
    <button
      onClick={handleClick}
    >click</button>
    <p>c1:{counter1}c2:{counter2}</p>
  </div>
}
Copy the code

Here’s how I did it: I made 3 combos of 5 in a second

Here is the console and the page:

Although the console looks correct, the latest Counter1 is not available due to closure issues

Suppose we change it to:

  const handleClick = useCallback(
    debounce(function () {
      console.count('click1')
      setCounter1(counter1 + 1)
    }, 1000),
    [counter1],
  )
Copy the code

If setCounter1 is used elsewhere to modify counter1, then the debounce timer is invalid.

  const [counter1, setCounter1] = useState(0);
  const [counter2, setCounter2] = useState(0);

  const handleClick = useCallback(
    debounce(function () {
      console.count('click1')
      setCounter1(counter1 + 1)
    }, 1000),
    [counter1],
  )

  useEffect(function () {
    const t = setInterval(() => {
      setCounter2(x => x + 1)
      setCounter1(x => x + 1)
    }, 500);
    return () => clearInterval(t)
  }, [])


  return <div style={{ padding: 30 }}>
    <button
      onClick={handleClick}
    >click</button>
    <p>c1:{counter1}c2:{counter2}</p>
  </div>
}
Copy the code

The bug here is not easy to demonstrate, you can test yourself, there will be a flash.

You can use the function to retrieve the previous counter1 and update counter1:

  const handleClick = useCallback(
    debounce(function () {
      console.count('click1')
      setCounter1(x => x + 1)
    }, 1000),
    [],
  )
Copy the code

Yes, it is true that setCounter1 anti-shake function works again, but what if I change the requirement again?

  const handleClick = useCallback(
    debounce(function () {
      console.count('click1')
      setCounter1(x => x + 1)
      setCounter2(counter1 + 1)
    }, 1000),
    [],
  )
Copy the code

That’s a dead end if you write a dependency on debounce and it doesn’t work again.

If you are familiar with hook, there must be a hook waiting to be seen.

The ref object generated by useRef is guaranteed to remain unchanged for all rendering cycles, so that we can bind the timer to the Ref to create a reliable timer

Use useRef to build reliable anti-shake throttling

Go straight to the code here!

/** ** @param func callback function * @param delay Delay * @param isImmediate Whether to execute immediately after the first click * @returns */ const useDebounce = (func: Function, delay: number, isImmediate? : boolean) => { let { current } = useRef<{ timer: NodeJS.Timeout | null }>({ timer: null }) let result: unknown function debounced(... Args: unknown[]) {if (current. Timer) clearTimeout(current. Timer) // If (isImmediate) {const callNow =! current.timer current.timer = setTimeout(() => { current.timer = null }, delay) if (callNow) result = func.apply(null, args) } else { current.timer = setTimeout(() => { current.timer = null result = func.apply(null, args) }, delay) } return result }; Clear = () => {if (current-timer) clearTimeout(current-.timer) current-timer = null}; // Clear debounce interface debounce. return debounced } export default useDebounceCopy the code

Save money and leave it to the smart one!

conclusion

Due to the react-Hook update mechanism, each update is re-executed repeatedly, so each rendering is a new closure. In such a scenario, the regular JS version of the anti-throttling can become unreliable or even ineffective. Therefore, we need to rely on the react reliable immutable useRef to mount a reliable timer to implement shock prevention and throttling.

How do you React hooks?