Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use throttle or debounce with React Hook?

I'm trying to use the throttle method from lodash in a functional component, e.g.:

const App = () => {   const [value, setValue] = useState(0)   useEffect(throttle(() => console.log(value), 1000), [value])   return (     <button onClick={() => setValue(value + 1)}>{value}</button>   ) } 

Since the method inside useEffect is redeclared at each render, the throttling effect does not work.

Does anyone have a simple solution ?

like image 864
Alexandre Annic Avatar asked Feb 13 '19 09:02

Alexandre Annic


People also ask

How do you debounce in React Hooks?

Debouncing enforces that there is a minimum time gap between two consecutive invocations of a function call. For example, a debounce interval of 500ms means that if 500ms hasn't passed from the previous invocation attempt, we cancel the previous invocation and schedule the next invocation of the function after 500ms.

Should I use debounce or throttle?

While both are used to limit the number of times a function executes, throttling delays execution, thus reducing notifications of an event that fires multiple times. On the other hand, debouncing bunches together a series of calls into a single call to a function, ensuring one notification for multiple fires.

What is throttling and Debouncing in React?

If we decide to prevent the second process from happening by making sure that our function can only run once in a given interval, that would be throttling. For our city filter app, we'll be using debouncing to solve our problem.


2 Answers

After some time passed I'm sure it's much easier to handle things by your own with setTimeout/clearTimeout(and moving that into separate custom hook) than working with functional helpers. Handling later one creates additional challenges right after we apply that to useCallback that can be recreated because of dependency change but we don't want to reset delay running.

original answer below

you may(and probably need) useRef to store value between renders. Just like it's suggested for timers

Something like that

const App = () => {   const [value, setValue] = useState(0)   const throttled = useRef(throttle((newValue) => console.log(newValue), 1000))    useEffect(() => throttled.current(value), [value])    return (     <button onClick={() => setValue(value + 1)}>{value}</button>   ) } 

As for useCallback:

It may work too as

const throttled = useCallback(throttle(newValue => console.log(newValue), 1000), []); 

But if we try to recreate callback once value is changed:

const throttled = useCallback(throttle(() => console.log(value), 1000), [value]); 

we may find it does not delay execution: once value is changed callback is immediately re-created and executed.

So I see useCallback in case of delayed run does not provide significant advantage. It's up to you.

[UPD] initially it was

  const throttled = useRef(throttle(() => console.log(value), 1000))    useEffect(throttled.current, [value]) 

but that way throttled.current has bound to initial value(of 0) by closure. So it was never changed even on next renders.

So be careful while pushing functions into useRef because of closure feature.

like image 142
skyboyer Avatar answered Sep 21 '22 04:09

skyboyer


I've created my own custom hook called useDebouncedEffect that will wait to perform a useEffect until the state hasn't updated for the duration of the delay.

In this example, your effect will log to the console after you have stopped clicking the button for 1 second.

Sandbox Example https://codesandbox.io/s/react-use-debounced-effect-6jppw

App.jsx

import { useState } from "react"; import { useDebouncedEffect } from "./useDebouncedEffect";  const App = () => {   const [value, setValue] = useState(0)    useDebouncedEffect(() => console.log(value), [value], 1000);    return (     <button onClick={() => setValue(value + 1)}>{value}</button>   ) }  export default App; 

useDebouncedEffect.js

import { useEffect } from "react";  export const useDebouncedEffect = (effect, deps, delay) => {     useEffect(() => {         const handler = setTimeout(() => effect(), delay);          return () => clearTimeout(handler);     // eslint-disable-next-line react-hooks/exhaustive-deps     }, [...deps || [], delay]); } 

The comment to disable exhaustive-deps is required unless you want to see a warning because lint will always complain about not having effect as a dependency. Adding effect as a dependency will trigger the useEffect on every render. Instead, you can add the check to useDebouncedEffect to make sure it's being passed all of the dependencies. (see below)

Adding exhaustive dependencies check to useDebouncedEffect

If you want to have eslint check useDebouncedEffect for exhaustive dependencies, you can add it to the eslint config in package.json

  "eslintConfig": {     "extends": [       "react-app"     ],     "rules": {       "react-hooks/exhaustive-deps": ["warn", {         "additionalHooks": "useDebouncedEffect"       }]     }   }, 

https://github.com/facebook/react/tree/master/packages/eslint-plugin-react-hooks#advanced-configuration

like image 36
Todd Skelton Avatar answered Sep 18 '22 04:09

Todd Skelton