Last active
March 8, 2025 05:03
-
-
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.
This file contains hidden or 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, 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