Skip to content

Instantly share code, notes, and snippets.

@letelete
Last active March 8, 2025 05:03
Show Gist options
  • Save letelete/36440d2689ab1bc07178d960d72510db to your computer and use it in GitHub Desktop.
Save letelete/36440d2689ab1bc07178d960d72510db to your computer and use it in GitHub Desktop.
A hook for scheduling function executions with deduplication and priority management. Requires: https://usehooks-ts.com/react-hook/use-unmount.
import { useCallback, useMemo, useRef } from 'react';
import { useUnmount } from '~lib/hooks/use-unmount';
import type { VoidFn } from '~lib/utils/types';
interface ScheduleOptions {
tag?: string;
}
interface ScheduleOnceOptions {
tag: string;
}
interface ScheduledInstance {
timeoutId: NodeJS.Timeout;
tag?: string;
}
/**
* A hook for scheduling function executions with deduplication and priority management.
*/
const useSchedule = () => {
const scheduled = useRef<ScheduledInstance[]>([]);
const clearInstance = useCallback((instance: ScheduledInstance) => {
scheduled.current = scheduled.current.filter(({ tag, timeoutId }) => {
if (instance.tag !== undefined) {
return tag !== instance.tag;
}
return timeoutId !== instance.timeoutId;
});
}, []);
const unschedule = useCallback(
(instance: ScheduledInstance) => {
clearInstance(instance);
clearTimeout(instance.timeoutId);
},
[clearInstance]
);
/**
* Schedules a function to execute after a given delay, ensuring deduplication based on tags.
*
* If a new execution request with the same tag is made before the previous one completes,
* the previous execution is canceled.
*
* @example Without tags (both queries execute independently):
* 0ms | schedule(() => console.log('A'), 1000);
* 500ms | schedule(() => console.log('B'), 1000);
* 1000ms | > "A"
* 1500ms | > "B"
*
* @example With tags (A is ignored in favor of B):
* 0ms | schedule(() => console.log('A'), 1000, { tag: 'log' });
* 500ms | schedule(() => console.log('B'), 1000, { tag: 'log' });
* 1000ms | >
* 1500ms | > "B"
*
* @property tag - A unique identifier used to deduplicate scheduled queries.
*/
const schedule = useCallback(
(fn: VoidFn, delay: number, options: ScheduleOptions = {}) => {
const timeoutId = setTimeout(() => {
fn();
clearInstance({ timeoutId });
}, delay);
if (options.tag) {
const instance = scheduled.current.find(
({ tag }) => tag === options.tag
);
if (instance) {
unschedule(instance);
}
}
scheduled.current.push({
tag: options.tag,
timeoutId,
});
},
[clearInstance, unschedule]
);
/**
* Ensures only one execution per tag at a time. If a new execution request with the same tag is made
* while a previous one is pending, the new request is ignored.
*
* @remarks This method does not queue ignored executions; they are simply discarded.
*
* @example
* 0ms | scheduleOnce(() => console.log('A'), 1000, { tag: 'log' });
* 500ms | scheduleOnce(() => console.log('B'), 1000, { tag: 'log' });
* 1000ms | > "A"
* 1500ms | >
* 2000ms | >
* ... | >
*/
const scheduleOnce = useCallback(
(fn: VoidFn, delay: number, options: ScheduleOnceOptions) => {
const timeoutId = setTimeout(() => {
fn();
clearInstance({ tag: options.tag, timeoutId });
}, delay);
const instance = scheduled.current.find(({ tag }) => tag === options.tag);
if (!instance) {
scheduled.current.push({
tag: options.tag,
timeoutId,
});
}
},
[clearInstance]
);
useUnmount(() => {
scheduled.current.forEach((instance) => unschedule(instance));
});
return useMemo(
() => ({ schedule, scheduleOnce }) as const,
[schedule, scheduleOnce]
);
};
export { useSchedule };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment