Skip to content

Instantly share code, notes, and snippets.

@mudge
Last active September 22, 2022 10:14
Show Gist options
  • Save mudge/eb9178a4b6d595ffde8f9cb31744afcf to your computer and use it in GitHub Desktop.
Save mudge/eb9178a4b6d595ffde8f9cb31744afcf to your computer and use it in GitHub Desktop.
A custom React Hook for a debounced click handler with a given callback and delay.
/*
* Inspired by Dan Abramov's "Making setInterval Declarative with React Hooks",
* this is a custom hook for debouncing a callback (e.g. for click handlers) such
* that a callback will not be fired until some delay has passed since the last click.
* The callback will automatically be updated with the latest props and state on every
* render meaning that users don't need to worry about stale information being used.
*
* See https://overreacted.io/making-setinterval-declarative-with-react-hooks/ for the
* original inspiration.
*/
import React, { useState, useEffect, useRef } from 'react';
const useDebounce = (callback, delay) => {
const latestCallback = useRef();
const latestTimeout = useRef();
useEffect(() => {
latestCallback.current = callback;
}, [callback]);
return () => {
if (latestTimeout.current) {
clearTimeout(latestTimeout.current);
}
latestTimeout.current = setTimeout(() => { latestCallback.current(); }, delay);
};
};
const App = () => {
const [count, setCount] = useState(0);
const handleIncrement = () => setCount(count => count + 1);
const handleClick = useDebounce(() => alert(`I've been clicked ${count} times`), 3000);
return (
<>
<button onClick={handleClick}>Click</button>
<button onClick={handleIncrement}>Increment {count}</button>
</>
);
}
export default App;
@tomstuart
Copy link

tomstuart commented Aug 26, 2019

Thanks for the detailed feedback, @tomstuart; this is all gold!

You’re very welcome! It was a lot of fun to think about.

I was initially reticent about using real Dates but it seems that JavaScript should do the right thing with edge cases like Daylight Saving Time (but I'm not certain about leap seconds)

Fear not: Date.now() returns a number rather than a Date instance, and the spec explicitly says “leap seconds are ignored”, so stuff like daylight savings and leap seconds [0] shouldn’t be a problem (although I guess that depends on what exactly “ignored” means). Of course we’re exposed to all the usual issues that can occur if the system clock is set backwards or forwards in the middle of our code, but I think that’s the price we pay for involving global time.

My only concern would be that the delay could result in a negative delay passed to setTimeout but we could easily clamp this with Math.max

Fear not: setTimeout()’s delay argument is already clamped to zero (see timer initialisation step 10) so we don’t need to do this ourselves.

[0] Having thought about this, I suspect “leap seconds are ignored” probably means that it is a problem. If that were important then I guess we could use performance.now() as our clock and fall back to Date.now() only when it’s unsupported, or of course switch back to the callCount Lamport clock and avoid global time altogether. Pragmatically it seems likely that leap seconds aren’t significant enough to worry about.

@mudge
Copy link
Author

mudge commented Aug 26, 2019

Phew, thanks for putting my mind at ease on both points.

@vivekmittal
Copy link

How do I stop the propagation when the handler is an onSubmit function on form?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment