Skip to content

Instantly share code, notes, and snippets.

@sumn2u
Forked from Danziger/interval.hook.ts
Created November 8, 2022 14:31
Show Gist options
  • Save sumn2u/f3263ac6f3c801a228bb79ca0b68e206 to your computer and use it in GitHub Desktop.
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)
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;
}
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;
}
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);
}
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];
}
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