-
-
Save mudge/eb9178a4b6d595ffde8f9cb31744afcf to your computer and use it in GitHub Desktop.
| /* | |
| * 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; |
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
delaycould result in a negative delay passed tosetTimeoutbut we could easily clamp this withMath.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.
Phew, thanks for putting my mind at ease on both points.
How do I stop the propagation when the handler is an onSubmit function on form?
Thanks for the detailed feedback, @tomstuart; this is all gold!
The stashing of the timeout ID in a ref and not using the cleanup functionality
useEffectwere both things that bothered me and it's now clear from your comments the root issue was trying to do the imperative work when the user clicks rather than deferring the work until next render. This is something I felt uneasy about (I was torn about whether the click handler should follow the React programming model or whether the user can expect thesetTimeoutto fire immediately when they click) but the fact my original solution requires both the extra ref and the manual timeout cleanup is clearly a smell.Responding to changes in
delayis something I conveniently skipped from Dan Abramov's original example withsetIntervalbut I think your changes make sense. I was initially reticent about using realDates 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), e.g.My only concern would be that the
delaycould result in a negative delay passed tosetTimeoutbut we could easily clamp this withMath.max, e.g.