import type { Dispatch, StateUpdater } from "../dependencies/types"; import { isPromise, timeout, useEffect, useMemo, useState, } from "../dependencies.js"; import type { PromiseState } from "../types.js"; /** * Returns a promise state object to track the provided `promise`. * Ignores outdated promises or ones that resolve when the component got unmounted. * Non-promise values are immediately resolved, avoiding a second render. * * @param promise The promise to track. * @returns A promise state object. */ export function usePromise(promise?: Promise | T) { const { 0: state, 1: onChangeState } = useState>({ status: "idle", }); const observer = useMemo( () => attachPromise(onChangeState, promise), [promise], ); useEffect(() => observer.dispose, [observer.dispose]); if (observer.state.promise !== state.promise) { return observer.state; } return state; } function attachPromise( onChangeState: Dispatch>>, promise?: Promise | T, ): { dispose?: () => void; state: PromiseState } { let mounted = true; if (promise === undefined) { return { dispose: undefined, state: { promise: Promise.resolve(undefined), reason: undefined, status: "idle", value: undefined, }, }; } if (!isPromise(promise)) { const state = { promise: Promise.resolve(promise), reason: undefined, status: "fulfilled", value: promise, } as const; return { dispose: undefined, state, }; } const state: PromiseState = { status: "pending", promise, value: undefined, reason: undefined, }; timeout(0, () => { onChangeState(state); promise.then( (value) => { if (!mounted) { return; } onChangeState((state) => state.promise !== promise ? state : { status: "fulfilled", promise, value, reason: undefined }, ); return value; }, (reason) => { if (!mounted) { return; } onChangeState((state) => state.promise !== promise ? state : { status: "rejected", promise, value: undefined, reason }, ); }, ); }); return { dispose: () => { mounted = false; }, state, }; }