-
-
Save sumn2u/f3263ac6f3c801a228bb79ca0b68e206 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; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment