Last active
November 15, 2023 18:00
-
-
Save Danziger/336e75b6675223ad805a88c2dfdcfd4a to your computer and use it in GitHub Desktop.
✨ Declarative useTimeout (setTimeout), useInterval (setInterval) and useThrottledCallback (useCallback combined with setTimeout) hooks for React (in Typescript)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useEffect, useRef } from 'react'; | |
/** | |
* Use setInterval with Hooks in a declarative way. | |
* | |
* @see https://stackoverflow.com/a/59274004/3723993 | |
* @see https://overreacted.io/making-setinterval-declarative-with-react-hooks/ | |
*/ | |
export function useInterval( | |
callback: React.EffectCallback, | |
delay: number | null, | |
): React.MutableRefObject<number | null> { | |
const intervalRef = useRef<number | null>(null); | |
const callbackRef = useRef(callback); | |
// Remember the latest callback: | |
// | |
// Without this, if you change the callback, when setInterval ticks again, it | |
// will still call your old callback. | |
// | |
// If you add `callback` to useEffect's deps, it will work fine but the | |
// interval will be reset. | |
useEffect(() => { | |
callbackRef.current = callback; | |
}, [callback]); | |
// Set up the interval: | |
useEffect(() => { | |
if (typeof delay === 'number') { | |
intervalRef.current = window.setInterval(() => callbackRef.current(), delay); | |
// Clear interval if the components is unmounted or the delay changes: | |
return () => window.clearInterval(intervalRef.current || 0); | |
} | |
}, [delay]); | |
// In case you want to manually clear the interval from the consuming component...: | |
return intervalRef; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useEffect, useRef } from 'react'; | |
/** | |
* Use requestAnimationFrame with Hooks in a declarative way. | |
* | |
* @see https://stackoverflow.com/a/59274004/3723993 | |
* @see https://overreacted.io/making-setinterval-declarative-with-react-hooks/ | |
*/ | |
export function useRAF( | |
callback: React.EffectCallback, | |
isRunning: boolean, | |
): React.MutableRefObject<number | null> { | |
const rafRef = useRef<number | null>(null); | |
const callbackRef = useRef(callback); | |
// Remember the latest callback: | |
// | |
// Without this, if you change the callback, when setInterval ticks again, it | |
// will still call your old callback. | |
// | |
// If you add `callback` to useEffect's deps, it will work fine but the | |
// interval will be reset. | |
useEffect(() => { | |
callbackRef.current = callback; | |
}, [callback]); | |
useEffect(() => { | |
function tick() { | |
rafRef.current = window.requestAnimationFrame(() => { | |
callbackRef.current(); | |
tick(); | |
}); | |
} | |
if (isRunning) { | |
tick(); | |
// Clear RAF if the components is unmounted or the delay changes: | |
return () => { | |
window.cancelAnimationFrame(rafRef.current || 0); | |
}; | |
} | |
}, [isRunning]); | |
// In case you want to manually clear the RAF from the consuming component...: | |
return rafRef; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useCallback, useEffect, useRef } from 'react'; | |
export function useThrottledCallback<A extends any[]>( | |
callback: (...args: A) => void, | |
delay: number, | |
deps?: readonly any[], | |
): (...args: A) => void { | |
const timeoutRef = useRef<number>(); | |
const callbackRef = useRef(callback); | |
const lastCalledRef = useRef(0); | |
// Remember the latest callback: | |
// | |
// Without this, if you change the callback, when setTimeout kicks in, it | |
// will still call your old callback. | |
// | |
// If you add `callback` to useCallback's deps, it will also update, but it | |
// might be called twice if the timeout had already been set. | |
useEffect(() => { | |
callbackRef.current = callback; | |
}, [callback]); | |
// Clear timeout if the components is unmounted or the delay changes: | |
useEffect(() => window.clearTimeout(timeoutRef.current), [delay]); | |
return useCallback((...args: A) => { | |
// Clear previous timer: | |
window.clearTimeout(timeoutRef.current); | |
function invoke() { | |
callbackRef.current(...args); | |
lastCalledRef.current = Date.now(); | |
} | |
// Calculate elapsed time: | |
const elapsed = Date.now() - lastCalledRef.current; | |
if (elapsed >= delay) { | |
// If already waited enough, call callback: | |
invoke(); | |
} else { | |
// Otherwise, we need to wait a bit more: | |
timeoutRef.current = window.setTimeout(invoke, delay - elapsed); | |
} | |
}, deps); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useEffect, useRef } from 'react'; | |
/** | |
* Use requestAnimationFrame + setInterval with Hooks in a declarative way. | |
* | |
* @see https://stackoverflow.com/a/59274004/3723993 | |
* @see https://overreacted.io/making-setinterval-declarative-with-react-hooks/ | |
*/ | |
export function useThrottledRAF( | |
callback: React.EffectCallback, | |
delay: number | null, | |
): [React.MutableRefObject<number | null>, React.MutableRefObject<number | null>] { | |
const intervalRef = useRef<number | null>(null); | |
const rafRef = useRef<number | null>(null); | |
const callbackRef = useRef(callback); | |
// Remember the latest callback: | |
// | |
// Without this, if you change the callback, when setInterval ticks again, it | |
// will still call your old callback. | |
// | |
// If you add `callback` to useEffect's deps, it will work fine but the | |
// interval will be reset. | |
useEffect(() => { | |
callbackRef.current = callback; | |
}, [callback]); | |
// Set up the interval: | |
useEffect(() => { | |
if (typeof delay === 'number') { | |
intervalRef.current = window.setInterval(() => { | |
rafRef.current = window.requestAnimationFrame(() => { | |
callbackRef.current(); | |
}); | |
}, delay); | |
// Clear interval and RAF if the components is unmounted or the delay changes: | |
return () => { | |
window.clearInterval(intervalRef.current || 0); | |
window.cancelAnimationFrame(rafRef.current || 0); | |
}; | |
} | |
}, [delay]); | |
// In case you want to manually clear the interval or RAF from the consuming component...: | |
return [intervalRef, rafRef]; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useEffect, useRef } from 'react'; | |
/** | |
* Use setTimeout with Hooks in a declarative way. | |
* | |
* @see https://stackoverflow.com/a/59274757/3723993 | |
* @see https://overreacted.io/making-setinterval-declarative-with-react-hooks/ | |
*/ | |
export function useTimeout( | |
callback: React.EffectCallback, | |
delay: number | null, | |
): React.MutableRefObject<number | null> { | |
const timeoutRef = useRef<number | null>(null); | |
const callbackRef = useRef(callback); | |
// Remember the latest callback: | |
// | |
// Without this, if you change the callback, when setTimeout kicks in, it | |
// will still call your old callback. | |
// | |
// If you add `callback` to useEffect's deps, it will work fine but the | |
// timeout will be reset. | |
useEffect(() => { | |
callbackRef.current = callback; | |
}, [callback]); | |
// Set up the timeout: | |
useEffect(() => { | |
if (typeof delay === 'number') { | |
timeoutRef.current = window.setTimeout(() => callbackRef.current(), delay); | |
// Clear timeout if the components is unmounted or the delay changes: | |
return () => window.clearTimeout(timeoutRef.current || 0); | |
} | |
}, [delay]); | |
// In case you want to manually clear the timeout from the consuming component...: | |
return timeoutRef; | |
} |
Thanks! Just updated that and added a new useThrottledRAF hook. 🚀
These hooks have been moved to an NPM package you can now directly install in your projects: https://www.npmjs.com/package/@swyg/corre.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Awesome!!! Thanks a bunch!!
I'd add on the
useRef
anull
as this will allow for the correct typing and will signal React that we willown
the ref.DefinitelyTyped/DefinitelyTyped#31065 (comment)
Becomes: