139

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 (without moving the throttle implementation outside of the component) ?

Alexandre Annic
  • 9,942
  • 5
  • 36
  • 50
  • 1
    Is it an option for you to defined the throttled function outside of the `App` component and just call it in the `useEffect` function? – Tholle Feb 13 '19 at 09:16
  • Yes, I tried and it works, but in my case, it's not very elegant, because I use component variables inside the throttling method. – Alexandre Annic Feb 13 '19 at 09:23

28 Answers28

119

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.

skyboyer
  • 22,209
  • 7
  • 57
  • 64
  • `useRef` should be the answer but this code does not seem to work. I'm trying to fix it. – Alexandre Annic Feb 13 '19 at 09:30
  • 1
    maybe I missed that part initial value for `useRef` makes closure to initial value – skyboyer Feb 13 '19 at 09:30
  • Throttle-debounce uses first DELAY, then CALLBACK ;) – Michał J. Gąsior Sep 11 '19 at 20:05
  • 1
    @mikes it depends(for lodash's version therer are `leading` and `trailing` options to config that https://github.com/lodash/lodash/blob/master/throttle.js) – skyboyer Sep 11 '19 at 20:20
  • since we use hooks, it means it might be necessary to redefine effect or callback every time we render and at the same time debouncing should work. so this is not an acceptable answer – Hossein Alipour Dec 06 '19 at 11:24
  • @hossein alipour cannot agree with you more. after almost a year passed since I've answered now I see it's better to control things directly within `useEffect` with `setTimeout/clearTimeout` then using debounce helper function. Thanks for raising that. – skyboyer Dec 07 '19 at 10:50
  • 2
    We can use `useRef` to create the callback and keep it, but I believe it is better to use `useCallback` even to pass the variables that needed if necessary which it will rarely be the case. We can use the `setValue` to change the value inside `useCallback` without adding `value` to the dependency array and even access the previous value using `setValue(previous => ...)`. If we need access to the value directly without changing it, we can pass it as an argument as you're doing with the `useRef` in your example like `useCallback(throttle((value) => { ... }, 1000), [])`. – Christos Lytras May 27 '20 at 09:48
  • 1
    Thankyou @ChristosLytras I have given the Answer below that how to implement it with useCallback hook. – Jaskaran Singh Dec 11 '20 at 08:06
  • 39
    So what part of this answer is the actual answer? It is a bit meandering. – coler-j Apr 01 '21 at 12:47
  • 19
    This answer is so confusing, agree with @coler-j – alexr89 Aug 04 '21 at 08:40
77

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

kernel
  • 3,654
  • 3
  • 25
  • 33
Todd Skelton
  • 6,839
  • 3
  • 36
  • 48
  • 7
    If you are wondering why `useCallback` is needed, I believe this is the reason: Functions in JavaScript do not have referential equality (ie `() => {} === () => {} // false`). So every time the component rerenders `effect` is not the same as it was previously. However, with the use of `useCallback` you are telling React 'please only consider me changed when my `deps` have changed too!' – David Jan 09 '21 at 10:05
  • 2
    @David Functions absolutely do have referential equality which is why you need `useCallback` in the first place. Your example is of structural equality, not referential equality. – Kevin Beal May 21 '21 at 19:39
  • @KevinBeal, I don't think I've heard of the term structural equality before and a quick internet search (in Kotlin) says that referential is `===` and structural is `==`. According to that logic, it seams to me that functions have structural equality in JavaScript – David May 27 '21 at 15:09
  • @David structural equality just mean the values are the same inside, with the same keys, values, etc. It's value equality or whatever else you would call that. – Kevin Beal May 27 '21 at 22:08
  • Why do you have the delay as a dependency in the useDebouncedEffect's useEffect ? – Muhammad Jan 25 '23 at 22:38
  • @Muhammad If the delay changes, you want to update the `useEffect`. This would only matter if the delay isn't hard-coded. – Todd Skelton Mar 18 '23 at 20:46
46

useThrottle , useDebounce

How to use both

const App = () => {
  const [value, setValue] = useState(0);
  // called at most once per second (same API with useDebounce)
  const throttledCb = useThrottle(() => console.log(value), 1000);
  // usage with useEffect: invoke throttledCb on value change
  useEffect(throttledCb, [value]);
  // usage as event handler
  <button onClick={throttledCb}>log value</button>
  // ... other render code
};

useThrottle (Lodash)

import _ from "lodash"

function useThrottle(cb, delay) {
  const options = { leading: true, trailing: false }; // add custom lodash options
  const cbRef = useRef(cb);
  // use mutable ref to make useCallback/throttle not depend on `cb` dep
  useEffect(() => { cbRef.current = cb; });
  return useCallback(
    _.throttle((...args) => cbRef.current(...args), delay, options),
    [delay]
  );
}

const App = () => {
  const [value, setValue] = useState(0);
  const invokeDebounced = useThrottle(
    () => console.log("changed throttled value:", value),
    1000
  );
  useEffect(invokeDebounced, [value]);
  return (
    <div>
      <button onClick={() => setValue(value + 1)}>{value}</button>
      <p>value will be logged at most once per second.</p>
    </div>
  );
};

function useThrottle(cb, delay) {
  const options = { leading: true, trailing: false }; // pass custom lodash options
  const cbRef = useRef(cb);
  useEffect(() => {
    cbRef.current = cb;
  });
  return useCallback(
    _.throttle((...args) => cbRef.current(...args), delay, options),
    [delay]
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js" integrity="sha256-VeNaFBVDhoX3H+gJ37DpT/nTuZTdjYro9yBruHjVmoQ=" crossorigin="anonymous"></script>
<script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>
<div id="root"></div>

useDebounce (Lodash)

import _ from "lodash"

function useDebounce(cb, delay) {
  // ...
  const inputsRef = useRef({cb, delay}); // mutable ref like with useThrottle
  useEffect(() => { inputsRef.current = { cb, delay }; }); //also track cur. delay
  return useCallback(
    _.debounce((...args) => {
        // Debounce is an async callback. Cancel it, if in the meanwhile
        // (1) component has been unmounted (see isMounted in snippet)
        // (2) delay has changed
        if (inputsRef.current.delay === delay && isMounted())
          inputsRef.current.cb(...args);
      }, delay, options
    ),
    [delay, _.debounce]
  );
}

const App = () => {
  const [value, setValue] = useState(0);
  const invokeDebounced = useDebounce(
    () => console.log("debounced", value),
    1000
  );
  useEffect(invokeDebounced, [value]);
  return (
    <div>
      <button onClick={() => setValue(value + 1)}>{value}</button>
      <p> Logging is delayed until after 1 sec. has elapsed since the last invocation.</p>
    </div>
  );
};

function useDebounce(cb, delay) {
  const options = {
    leading: false,
    trailing: true
  };
  const inputsRef = useRef(cb);
  const isMounted = useIsMounted();
  useEffect(() => {
    inputsRef.current = { cb, delay };
  });

  return useCallback(
    _.debounce(
      (...args) => {
        // Don't execute callback, if (1) component in the meanwhile 
        // has been unmounted or (2) delay has changed
        if (inputsRef.current.delay === delay && isMounted())
          inputsRef.current.cb(...args);
      },
      delay,
      options
    ),
    [delay, _.debounce]
  );
}

function useIsMounted() {
  const isMountedRef = useRef(true);
  useEffect(() => {
    return () => {
      isMountedRef.current = false;
    };
  }, []);
  return () => isMountedRef.current;
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js" integrity="sha256-VeNaFBVDhoX3H+gJ37DpT/nTuZTdjYro9yBruHjVmoQ=" crossorigin="anonymous"></script>
<script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>
<div id="root"></div>

Customizations

1. You might replace Lodash with your own throttle or debounce code, like:

const debounceImpl = (cb, delay) => {
  let isDebounced = null;
  return (...args) => {
    clearTimeout(isDebounced);
    isDebounced = setTimeout(() => cb(...args), delay);
  };
};

const throttleImpl = (cb, delay) => {
  let isThrottled = false;
  return (...args) => {
    if (isThrottled) return;
    isThrottled = true;
    cb(...args);
    setTimeout(() => {
      isThrottled = false;
    }, delay);
  };
};

const App = () => {
  const [value, setValue] = useState(0);
  const invokeThrottled = useThrottle(
    () => console.log("throttled", value),
    1000
  );
  const invokeDebounced = useDebounce(
    () => console.log("debounced", value),
    1000
  );
  useEffect(invokeThrottled, [value]);
  useEffect(invokeDebounced, [value]);
  return <button onClick={() => setValue(value + 1)}>{value}</button>;
};

function useThrottle(cb, delay) {
  const cbRef = useRef(cb);
  useEffect(() => {
    cbRef.current = cb;
  });
  return useCallback(
    throttleImpl((...args) => cbRef.current(...args), delay),
    [delay]
  );
}

function useDebounce(cb, delay) {
  const cbRef = useRef(cb);
  useEffect(() => {
    cbRef.current = cb;
  });
  return useCallback(
    debounceImpl((...args) => cbRef.current(...args), delay),
    [delay]
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>
<div id="root"></div>

2. useThrottle can be shortened up, if always used with useEffect (same for useDebounce):

const App = () => {
  // useEffect now is contained inside useThrottle
  useThrottle(() => console.log(value), 1000, [value]);
  // ...
};

const App = () => {
  const [value, setValue] = useState(0);
  useThrottle(() => console.log(value), 1000, [value]);
  return (
    <div>
      <button onClick={() => setValue(value + 1)}>{value}</button>
      <p>value will be logged at most once per second.</p>
    </div>
  );
};

function useThrottle(cb, delay, additionalDeps) {
  const options = { leading: true, trailing: false }; // pass custom lodash options
  const cbRef = useRef(cb);
  const throttledCb = useCallback(
    _.throttle((...args) => cbRef.current(...args), delay, options),
    [delay]
  );
  useEffect(() => {
    cbRef.current = cb;
  });
  // set additionalDeps to execute effect, when other values change (not only on delay change)
  useEffect(throttledCb, [throttledCb, ...additionalDeps]);
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js" integrity="sha256-VeNaFBVDhoX3H+gJ37DpT/nTuZTdjYro9yBruHjVmoQ=" crossorigin="anonymous"></script>
<script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>
<div id="root"></div>
Crutchcorn
  • 199
  • 5
  • 14
ford04
  • 66,267
  • 20
  • 199
  • 171
  • Why use `useEffect(() => { cbRef.current = cb; });` without any dependency? That means we run effect on each re-render, so why not simply assign without useEffect? – ogostos Jul 14 '20 at 06:25
  • 3
    Good question - this is intended to always contain the most recent callback inside `cbRef`. A mutable ref can be used like an [instance variable](https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables) for Hooks - [here](https://overreacted.io/making-setinterval-declarative-with-react-hooks/#just-show-me-the-code) is an example with `setInterval` from the Overreacted blog. The render phase should also be pure without side effects, e.g. to be compatible with React concurrent mode. This is, why we wrap the assignment inside `useEffect`. – ford04 Jul 14 '20 at 07:55
  • I seem to get an error when using the useThrottle (Lodash): "TypeError: Cannot read property 'apply' of undefined". Coupled with that, I have an ESLint error saying "React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead." – alexr89 Aug 05 '21 at 19:52
  • Why are the implementations of `useThrottle` and `useDebounce` different, that is, why does `useDebounce` have a depencency on `_.debounce` and handles cancellation, but `useThrottle` does not? You wrote the comment `// Debounce is an async callback`, but the same is true for `throttle()`, so that's not an explanation for the difference. – nh2 Jan 14 '23 at 22:47
  • Further, you write `Debounce is an async callback. Cancel it`, but nowhere do you cancel it (that is, you don't call lodash's `cancel` function). – nh2 Jan 14 '23 at 23:15
  • This works great! The only thing I had to do was remove the options parameter in useDebounce, as that isn't declared anywhere. – Justin Mar 24 '23 at 12:16
21

It could be a tiny custom hook, like this:

useDebounce.js

import React, { useState, useEffect } from 'react';

export default (value, timeout) => {
    const [state, setState] = useState(value);

    useEffect(() => {
        const handler = setTimeout(() => setState(value), timeout);

        return () => clearTimeout(handler);
    }, [value, timeout]);

    return state;
}

Usage example:

import React, { useEffect } from 'react';

import useDebounce from '/path/to/useDebounce';

const App = (props) => {
    const [state, setState] = useState({title: ''});    
    const debouncedTitle = useDebounce(state.title, 1000);

    useEffect(() => {
        // do whatever you want with state.title/debouncedTitle
    }, [debouncedTitle]);        

    return (
        // ...
    );
}
// ...

Note: As you probably know, useEffect always run on initial render, and because of that if you use my answer, you will probably see your component's render runs twice, don't worry, you just need to writing another custom hook. check out my other answer for more info.

Mehdi Dehghani
  • 10,970
  • 6
  • 59
  • 64
  • I don't understand how to avoid the second (or first) render, even using the linked hook. Can you please provide an example? Thanks – andreapier Aug 19 '21 at 08:45
  • @andreapier I already added link to another custom hook in order to prevent render on initial render, in you didn't see it, here is the link: https://stackoverflow.com/a/57941438/3367974 – Mehdi Dehghani Aug 19 '21 at 09:17
  • Yes, I saw it. My question was about how to get the two working together. However, I switched to another kind of solution as this one (in my opinion) presents too many issues. – andreapier Aug 21 '21 at 16:13
  • If you mean using `useDebounce` together with [`useDidMountEffect`](https://stackoverflow.com/a/57941438/3367974), you just need to replace `useEffect` with `useDidMountEffect` in above example and you are good to go. – Mehdi Dehghani Aug 21 '21 at 17:08
20

Debounce with help of useCallback hook.

import React, { useState, useCallback } from 'react';
import debounce from 'lodash.debounce';

function App() {
    const [value, setValue] = useState('');
    const [dbValue, saveToDb] = useState(''); // would be an API call normally

    // highlight-starts
    const debouncedSave = useCallback(
        debounce(nextValue => saveToDb(nextValue), 1000),
        [], // will be created only once initially
    );
    // highlight-ends

    const handleChange = event => {
        const { value: nextValue } = event.target;
        setValue(nextValue);
        // Even though handleChange is created on each render and executed
        // it references the same debouncedSave that was created initially
        debouncedSave(nextValue);
    };

    return <div></div>;
}
Jaskaran Singh
  • 2,392
  • 24
  • 39
8

I wrote two simple hooks (use-throttled-effect and use-debounced-effect) for this use case maybe it wil be useful for someone else looking for a simple solution.

import React, { useState } from 'react';
import useThrottledEffect  from 'use-throttled-effect';

export default function Input() {
  const [count, setCount] = useState(0);

  useEffect(()=>{
    const interval = setInterval(() => setCount(count=>count+1) ,100);
    return ()=>clearInterval(interval);
  },[])

  useThrottledEffect(()=>{
    console.log(count);     
  }, 1000 ,[count]);

  return (
    {count}
  );
}
Saman Mohamadi
  • 4,454
  • 4
  • 38
  • 58
7

I'd like to join the party with my throttlled and debounced input using useState:

// import { useState, useRef } from 'react' // nomral import
const { useState, useRef } = React // inline import

// Throttle

const ThrottledInput = ({ onChange, delay = 500 }) => {
  const t = useRef()
  
  const handleChange = ({ target }) => {
    if (!t.current) {
      t.current = setTimeout(() => {
        onChange(target.value)
        clearTimeout(t.current)
        t.current = null
      }, delay)
    }
  }
  
  return (
    <input
      placeholder="throttle"
      onChange={handleChange}
    />
  )
}


// Debounce

const DebouncedInput = ({ onChange, delay = 500 }) => {
  const t = useRef()
  
  const handleChange = ({ target }) => {
    clearTimeout(t.current)
    t.current = setTimeout(() => onChange(target.value), delay)
  }
  
  return (
    <input
      placeholder="debounce"
      onChange={handleChange}
    />
  )
}

// ----

ReactDOM.render(<div>
  <ThrottledInput onChange={console.log} />
  <DebouncedInput onChange={console.log} />
</div>, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
gazdagergo
  • 6,187
  • 1
  • 31
  • 45
  • 1
    Neat, using and upvoting! Just a minor thing: shouldn't `clearTimeout(t)` on the Throttle example be `clearTimeout(t.current)` ? – HynekS May 22 '22 at 07:15
  • in deed, thanks. I've fixed it! – gazdagergo May 23 '22 at 11:38
  • nice solution, it is the one I am using, ty. One question please, assuming that we have big delay and the component unmounts, we then need to add a wrapper with another parameter of reset or just clear the timeouts? (trying to emulate the .cancel equivalent of libraries) – nagiatzi Jun 01 '22 at 09:43
  • What if we simply add an empty `useEffect` hook which returns a cleanup function with a `clearTimeout(t)` in it? Does this solve the issue? – gazdagergo Jun 02 '22 at 07:01
  • or `t.current` sorry – gazdagergo Jun 02 '22 at 07:09
6

And one more implementation. Custom hook:

function useThrottle (func, delay) {
  const [timeout, saveTimeout] = useState(null);
    
  const throttledFunc = function () {
    if (timeout) {
      clearTimeout(timeout);
    }

    const newTimeout = setTimeout(() => {
      func(...arguments);
      if (newTimeout === timeout) {
        saveTimeout(null);
      }
    }, delay);

    saveTimeout(newTimeout);
  }

  return throttledFunc;
}

and usage:

const throttledFunc = useThrottle(someFunc, 200);

Hope that will help someone.

Aliaksei
  • 1,094
  • 11
  • 20
4

Using lodash's debounce function here is what I do:

import debounce from 'lodash/debounce'

// The function that we want to debounce, for example the function that makes the API calls
const getUsers = (event) => {
// ...
}


// The magic!
const debouncedGetUsers = useCallback(debounce(getUsers, 500), [])

In your JSX:

<input value={value} onChange={debouncedGetUsers} />
Engr.MTH
  • 1,002
  • 1
  • 11
  • 23
4

You can use useMemo hook to optimize your throttled event handler

Example code below:

const App = () => {
  const [value, setValue] = useState(0);

  // ORIGINAL EVENT HANDLER
  function eventHandler(event) {
    setValue(value + 1);
  }

  // THROTTLED EVENT HANDLER
  const throttledEventHandler = useMemo(() => throttle(eventHandler, 1000), [value]);
  
  return (
    <button onClick={throttledEventHandler}>Throttled Button with value: {value}</button>
  )
}
Rahul Gupta
  • 9,775
  • 7
  • 56
  • 69
  • This memo updates the state, is this OK? I am wondering about this instruction from React: "Remember that the function passed to useMemo runs during rendering. Don’t do anything there that you wouldn’t normally do while rendering. For example, side effects belong in useEffect, not useMemo." – user2078023 Dec 02 '21 at 09:16
3

This is my useDebounce:

export function useDebounce(callback, timeout, deps) {
    const timeoutId = useRef();

    useEffect(() => {
        clearTimeout(timeoutId.current);
        timeoutId.current = setTimeout(callback, timeout);

        return () => clearTimeout(timeoutId.current);
    }, deps);
}

And you can use it like this:

const TIMEOUT = 500; // wait 500 milliseconds;

export function AppContainer(props) {
    const { dataId } = props;
    const [data, setData] = useState(null);
    //
    useDebounce(
        async () => {
            data = await loadDataFromAPI(dataId);
            setData(data);
        }, 
        TIMEOUT, 
        [dataId]
    );
    //
}
Agus Syahputra
  • 436
  • 4
  • 12
3

I just came up with the following pattern when trying to solve an issue with stale state:

We can store the debounced function in a ref and update it each time the component rerenders in useEffect like this:

  // some state
  const [counter, setCounter] = useState(0);

  // store a ref to the function we will debounce
  const increment = useRef(null);

  // update the ref every time the component rerenders
  useEffect(() => {
    increment.current = () => {
      setCounter(counter + 1);
    };
  });

  // debounce callback, which we can call (i.e. in button.onClick)
  const debouncedIncrement = useCallback(
    debounce(() => {
      if (increment) {
        increment.current();
      }
    }, 1500),
    []
  );

  // cancel active debounces on component unmount
  useEffect(() => {
    return () => {
      debouncedIncrement.cancel();
    };
  }, []);

Code sandbox: https://codesandbox.io/s/debounced-function-ref-pdrfu?file=/src/index.js

I hope this will save someone a few hours of struggling

nsimeonov
  • 704
  • 8
  • 18
1

I'm pretty late to this, but here's a way to debounce setState()

/**
 * Like React.setState, but debounces the setter.
 * 
 * @param {*} initialValue - The initial value for setState().
 * @param {int} delay - The debounce delay, in milliseconds.
 */
export const useDebouncedState = (initialValue, delay) => {
  const [val, setVal] = React.useState(initialValue);
  const timeout = React.useRef();
  const debouncedSetVal = newVal => {
    timeout.current && clearTimeout(timeout.current);
    timeout.current = setTimeout(() => setVal(newVal), delay);
  };

  React.useEffect(() => () => clearTimeout(timeout.current), []);
  return [val, debouncedSetVal];
};
Whatabrain
  • 238
  • 4
  • 7
1
const useDebounce = (func: any) => {
    const debounceFunc = useRef(null);

    useEffect(() => {
        if (func) {
            // @ts-ignore
            debounceFunc.current = debounce(func, 1000);
        }
    }, []);

    const debFunc = () => {
        if (debounceFunc.current) {
            return debounceFunc.current;
        }
        return func;
    };
    return debFunc();
};
comalex3
  • 2,497
  • 4
  • 26
  • 47
1

I made a simple hook to create throttle instances.

It takes a slightly different approach, passing in the function to call each time rather that trying to wrap it and manage mutations. A lot of the other solutions don't account for the function to call potentially changing. Pattern works well with throttle or debounce.

// useThrottle.js
import React, { useCallback } from 'react';
import throttle from 'lodash/throttle';

export function useThrottle(timeout = 300, opts = {}) {
  return useCallback(throttle((fn, ...args) => {
    fn(...args);
  }, timeout, opts), [timeout]);
}

Sample usage:

...
const throttleX = useThrottle(100);

const updateX = useCallback((event) => {
  // do something!
}, [someMutableValue])

return ( 
 <div onPointerMove={(event) => throttleX(updateX, event)}></div>
)
...
nverba
  • 3,803
  • 2
  • 18
  • 21
1

I believe this hook works properly by giving the option to fire immediately.

import { useState, useRef, useEffect } from 'react';

const useDebounce = <T>(
  value: T,
  timeout: number,
  immediate: boolean = true
): T => {
  const [state, setState] = useState<T>(value);
  const handler = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);

  useEffect(() => {
    if (handler.current) {
      clearTimeout(handler.current);
      handler.current = undefined;
    } else if (immediate) {
      setState(value);
    }

    handler.current = setTimeout(() => {
      setState(value);
      handler.current = undefined;
    }, timeout);
  }, [value, timeout, immediate]);

  return state;
};

export default useDebounce;
0

If you are using it in handler, I am fairly certain this is the way to do it.

function useThrottleScroll() {
  const savedHandler = useRef();

  function handleEvent() {}

  useEffect(() => {
    savedHandleEvent.current = handleEvent;
  }, []);

  const throttleOnScroll = useRef(throttle((event) => savedHandleEvent.current(event), 100)).current;

  function handleEventPersistence(event) {
    return throttleOnScroll(event);
  }

  return {
    onScroll: handleEventPersistence,
  };
}
user1730335
  • 59
  • 1
  • 5
0

I use something like this and it works great:

let debouncer = debounce(
  f => f(),
  1000,
  { leading: true }, // debounce one on leading and one on trailing
);

function App(){
   let [state, setState] = useState();

   useEffect(() => debouncer(()=>{
       // you can use state here for new state value
   }),[state])

   return <div />
}
Hossein Alipour
  • 575
  • 5
  • 9
0

I write a simple useDebounce hook which takes cleanup into consideration, just as useEffect works.

import { useState, useEffect, useRef, useCallback } from "react";

export function useDebounceState<T>(initValue: T, delay: number) {
  const [value, setValue] = useState<T>(initValue);
  const timerRef = useRef(null);
  // reset timer when delay changes
  useEffect(
    function () {
      if (timerRef.current) {
        clearTimeout(timerRef.current);
        timerRef.current = null;
      }
    },
    [delay]
  );
  const debounceSetValue = useCallback(
    function (val) {
      if (timerRef.current) {
        clearTimeout(timerRef.current);
        timerRef.current = null;
      }
      timerRef.current = setTimeout(function () {
        setValue(val);
      }, delay);
    },
    [delay]
  );
  return [value, debounceSetValue];
}

interface DebounceOptions {
  imediate?: boolean;
  initArgs?: any[];
}

const INIT_VALUE = -1;
export function useDebounce(fn, delay: number, options: DebounceOptions = {}) {
  const [num, setNum] = useDebounceState(INIT_VALUE, delay);
  // save actual arguments when fn called
  const callArgRef = useRef(options.initArgs || []);
  // save real callback function
  const fnRef = useRef(fn);
  // wrapped function
  const trigger = useCallback(function () {
    callArgRef.current = [].slice.call(arguments);
    setNum((prev) => {
      return prev + 1;
    });
  }, []);
  // update real callback
  useEffect(function () {
    fnRef.current = fn;
  });
  useEffect(
    function () {
      if (num === INIT_VALUE && !options.imediate) {
        // prevent init call
        return;
      }
      return fnRef.current.apply(null, callArgRef.current);
    },
    [num, options.imediate]
  );
  return trigger;
}

gist is here: https://gist.github.com/sophister/9cc74bb7f0509bdd6e763edbbd21ba64

and this is live demo: https://codesandbox.io/s/react-hook-debounce-demo-mgr89?file=/src/App.js

useage:

const debounceChange = useDebounce(function (e) {
    console.log("debounced text change: " + e.target.value);
  }, 500);
  // can't use debounceChange directly, since react using event pooling
  function deboucnedCallback(e) {
    e.persist();
    debounceChange(e);
  }

// later the jsx
<input onChange={deboucnedCallback} />
Jess
  • 620
  • 1
  • 7
  • 18
0

Here is an actual throttle hook. You can use in a screen or component for all of the functions you want to throttle, and they will share the same throttle. Or you can call useThrottle() multiple times and have different throttles for individual functions.

Use like this:

import useThrottle from '../hooks/useThrottle';

const [navigateToSignIn, navigateToCreateAccount] = useThrottle([
        () => { navigation.navigate(NavigationRouteNames.SignIn) },
        () => { navigation.navigate(NavigationRouteNames.CreateAccount) }
    ])

And the hook itself:

import { useCallback, useState } from "react";

// Throttles all callbacks on a component within the same throttle.  
// All callbacks passed in will share the same throttle.

const THROTTLE_DURATION = 500;

export default (callbacks: Array<() => any>) => {
    const [isWaiting, setIsWaiting] = useState(false);

    const throttledCallbacks = callbacks.map((callback) => {
        return useCallback(() => {
            if (!isWaiting) {
                callback()
                setIsWaiting(true)
                setTimeout(() => {
                    setIsWaiting(false)
                }, THROTTLE_DURATION);
            }
        }, [isWaiting]);
    })

    return throttledCallbacks;
}
Jeff Padgett
  • 2,380
  • 22
  • 34
0

Here's a simple hook to debounce your calls.

To use the below code, all you have to do is declare it as so

const { debounceRequest } = useDebounce(someFn);

And, then call it as so

debounceRequest(); 

Implementation is shown below

import React from "react";

const useDebounce = (callbackFn: () => any, timeout: number = 500) => {
const [sends, setSends] = React.useState(0);

const stabilizedCallbackFn = React.useCallback(callbackFn, [callbackFn]);

const debounceRequest = () => {
  setSends(sends + 1);
};

// 1st send, 2nd send, 3rd send, 4th send ...
// when the 2nd send comes, then 1st set timeout is cancelled via clearInterval
// when the 3rd send comes, then 2nd set timeout is cancelled via clearInterval
// process continues till timeout has passed, then stabilizedCallbackFn gets called
// return () => clearInterval(id) is critical operation since _this_ is what cancels 
//  the previous send.
// * return () => clearInterval(id) is called for the previous send when a new send 
// is sent. Essentially, within the timeout all but the last send gets called.

React.useEffect(() => {
  if (sends > 0) {
     const id = window.setTimeout(() => {
       stabilizedCallbackFn();
       setSends(0);
     }, timeout);
     return () => {
      return window.clearInterval(id);
     };
  }
 }, [stabilizedCallbackFn, sends, timeout]);

 return {
   debounceRequest,
 };
};

export default useDebounce;
Sangeet Agarwal
  • 1,674
  • 16
  • 25
0

react-table has a nice useAsyncDebounce function featured at https://react-table.tanstack.com/docs/faq#how-can-i-debounce-rapid-table-state-changes

Edgar Manukyan
  • 1,153
  • 10
  • 21
0
function myThrottle(callback, delay) {
  var previousTime = 0;
  return function (...args) {
    let currentTime = Date.now();
    let gap = currentTime - previousTime;
    if (gap > 0) {
      previousTime = currentTime + delay;
      callback.call(this, ...args);
    }
    return;
  };
}

Use the below code inside your functional component.

const memoizedCallback = useMemo(() => myThrottle(callback, 3000), []);

Use memoizedCallback as a callback

GAURAV
  • 647
  • 6
  • 18
  • "Use memoizedCallback as a callback" : Isn't it what [useCallback](https://reactjs.org/docs/hooks-reference.html#usecallback) does ? – challet Jan 08 '23 at 18:43
0

You can write a custom hook just like here https://usehooks.com/useDebounce/

  // Hook
  // T is a generic type for value parameter, our case this will be string
function useDebounce<T>(value: T, delay: number): T {
  // State and setters for debounced value
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(
    () => {
      // Update debounced value after delay
      const handler = setTimeout(() => {
        setDebouncedValue(value);
      }, delay);

      // Cancel the timeout if value changes (also on delay change or unmount)
      // This is how we prevent debounced value from updating if value is changed ...
      // .. within the delay period. Timeout gets cleared and restarted.
      return () => {
        clearTimeout(handler);
      };
    },
    [value, delay] // Only re-call effect if value or delay changes
  );

  return debouncedValue;
}
0

An attempt to add clarity to the accepted answer.

You can use throttle from lodash:

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

If you only want to execute the function once at the end of the interval then use:

const throttled = useCallback(throttle(newValue => console.log(newValue), 1000, { leading: false, trailing: true }), []);
Daryn
  • 3,394
  • 5
  • 30
  • 41
0

Simplest solution in Typescript:

const useThrottle = <T extends any>(f: (...args: T[]) => void, delay: number) =>
  useCallback(_.throttle(f, delay), [delay])

Usage:

const fooThrottled = useThrottle(foo, 500)
Raine Revere
  • 30,985
  • 5
  • 40
  • 52
-1

In my case I also needed to pass the event. Went with this:

const MyComponent = () => {
  const handleScroll = useMemo(() => {
    const throttled = throttle(e => console.log(e.target.scrollLeft), 300);
    return e => {
      e.persist();
      return throttled(e);
    };
  }, []);
  return <div onScroll={handleScroll}>Content</div>;
};
Nelu
  • 16,644
  • 10
  • 80
  • 88
-1

My solution is similar to this https://stackoverflow.com/a/68357888/6083689 (features useMemo), however I'm passing the argument directly to debounced function in useEffect, instead of treating it as dependency. It solves the problem of re-creating the hooks by separating the arguments (which supposed to be re-created) and debounced function (which shouldn't be re-created).

const MyComponent: FC<Props> = ({ handler, title }) => {
  const payload = useMemo<Payload>(() => ({ title }), [title])
  const debouncedHandler = useMemo(() => debounce(handler, 1000), [handler])

  useEffect(() => debouncedHandler(payload), [payload, debouncedHandler])
}
Artur Bilski
  • 89
  • 1
  • 2