Skip to content

Instantly share code, notes, and snippets.

@ryanflorence
Created October 12, 2025 23:41
Show Gist options
  • Select an option

  • Save ryanflorence/fd25a7e28716538ebc55386f02d2dd20 to your computer and use it in GitHub Desktop.

Select an option

Save ryanflorence/fd25a7e28716538ebc55386f02d2dd20 to your computer and use it in GitHub Desktop.
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
type DependencyList,
} from "react";
import {
DynamicInterval,
type DynamicIntervalOptions,
type TaskFn,
} from "./dynamic-interval";
export type UseDynamicIntervalOptions = DynamicIntervalOptions;
export type UseDynamicIntervalReturn = {
info: DynamicInterval["info"];
start: () => void;
pause: () => void;
resume: () => void;
stop: () => void;
runNow: () => void;
resetBackoff: () => void;
instance: DynamicInterval | null;
};
/**
* React hook wrapper for DynamicInterval.
* Creates and manages a DynamicInterval instance with reactive info state and lifecycle controls.
*
* Recreate behavior is controlled by the deps array. Pass deps to recreate the scheduler when inputs change.
*/
export function useDynamicInterval(
options: UseDynamicIntervalOptions,
deps: DependencyList = [],
): UseDynamicIntervalReturn {
const diRef = useRef<DynamicInterval | null>(null);
// Keep the latest task without forcing a re-instantiation of the scheduler
const taskRef = useRef<TaskFn>(options.task);
useEffect(() => {
taskRef.current = options.task;
}, [options.task]);
const notifyInfoUpdate = useCallback(() => {
const inst = diRef.current;
if (inst) {
// Queue on next macrotask to ensure DynamicInterval has updated its internal state after a tick
setTimeout(() => {
const current = diRef.current;
if (current) setInfo(current.info);
}, 0);
}
}, []);
const wrappedTask = useMemo<TaskFn>(() => {
return async () => {
try {
await taskRef.current();
} finally {
notifyInfoUpdate();
}
};
}, [notifyInfoUpdate]);
const initialInfo: DynamicInterval["info"] = useMemo(() => {
return {
name: options.name,
paused: true,
stopped: false,
running: false,
lastDelayMs: null,
lastOutcome: null,
runCount: 0,
} as DynamicInterval["info"];
}, [options.name]);
const [info, setInfo] = useState<DynamicInterval["info"]>(initialInfo);
useEffect(() => {
const inst = new DynamicInterval({ ...options, task: wrappedTask });
diRef.current = inst;
setInfo(inst.info);
return () => {
try {
inst.stop();
} finally {
diRef.current = null;
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
const start = useCallback(() => {
const inst = diRef.current;
if (!inst) return;
inst.start();
setInfo(inst.info);
}, []);
const pause = useCallback(() => {
const inst = diRef.current;
if (!inst) return;
inst.pause();
setInfo(inst.info);
}, []);
const resume = useCallback(() => {
const inst = diRef.current;
if (!inst) return;
inst.resume();
setInfo(inst.info);
}, []);
const stop = useCallback(() => {
const inst = diRef.current;
if (!inst) return;
inst.stop();
setInfo(inst.info);
}, []);
const runNow = useCallback(() => {
const inst = diRef.current;
if (!inst) return;
inst.runNow();
setInfo(inst.info);
}, []);
const resetBackoff = useCallback(() => {
const inst = diRef.current;
if (!inst) return;
inst.resetBackoff();
setInfo(inst.info);
}, []);
return {
info,
start,
pause,
resume,
stop,
runNow,
resetBackoff,
instance: diRef.current,
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment