import { memo, Reducer, useCallback, useEffect, useMemo, useReducer } from "react"; /** * The possible states of a {@link Promise}, plus an additional one that represents that it has not been started yet. * * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise */ export enum UsePromiseStatus { READY, PENDING, REJECTED, FULFILLED, } /** * Action to start running the {@link Promise} contained in the {@link usePromise} hook, putting it in the {@link UsePromiseStatus.PENDING} state. */ type UsePromiseActionRun = { type: "start" } /** * Action to fulfill the {@link Promise} contained in the {@link usePromise} hook, putting it in the {@link UsePromiseStatus.FULFILLED} state. */ type UsePromiseActionFulfill = { type: "fulfill", result: D } /** * Action to reject the {@link Promise} contained in the {@link usePromise} hook, putting it in the {@link UsePromiseStatus.REJECTED} state. */ type UsePromiseActionReject = { type: "reject", error: E } /** * Actions that can be performed on the {@link useReducer} hook used inside {@link usePromise}. */ type UsePromiseAction = UsePromiseActionRun | UsePromiseActionFulfill | UsePromiseActionReject; /** * The internal state of the {@link useReducer} hook used inside {@link usePromise}. */ type UsePromiseState = { status: UsePromiseStatus, result: D | undefined, error: E | undefined, } /** * The initial {@link UsePromiseState} of the {@link usePromise} hook. */ const initialUsePromise: UsePromiseState = { status: UsePromiseStatus.READY, result: undefined, error: undefined, } /** * The reducer used by {@link usePromise}. */ function reducerUsePromise(prev: UsePromiseState, action: UsePromiseAction): UsePromiseState { switch (action.type) { case "start": return { ...prev, status: UsePromiseStatus.PENDING } case "fulfill": return { status: UsePromiseStatus.FULFILLED, result: action.result, error: undefined } case "reject": return { status: UsePromiseStatus.REJECTED, error: action.error, result: undefined } } } /** * Async function that can be ran using {@link usePromise}. */ type UsePromiseFunction = (params: P) => Promise /** * Values returned by the {@link usePromise} hook. */ export type UsePromise = UsePromiseState & { run: (params: P) => Promise } /** * Hook executing an asyncronous function in a way that can be handled by React components. */ export function usePromise(func: UsePromiseFunction): UsePromise { const [state, dispatch] = useReducer, UsePromiseAction>>(reducerUsePromise, initialUsePromise) const run = useCallback( async (params: P) => { dispatch({ type: "start" }) try { var result = await func(params) } catch (error) { dispatch({ type: "reject", error: error as E }) return } dispatch({ type: "fulfill", result }) return }, [func] ) return { ...state, run } } export type PromiseMultiplexerReadyParams = { run: UsePromise["run"], } export type PromiseMultiplexerPendingParams = { } export type PromiseMultiplexerFulfilledParams = { run: UsePromise["run"], result: D, } export type PromiseMultiplexerRejectedParams = { run: UsePromise["run"], error: E, } export type PromiseMultiplexerConfig = { hook: UsePromise, ready: (params: PromiseMultiplexerReadyParams) => JSX.Element, pending: (params: PromiseMultiplexerPendingParams) => JSX.Element, fulfilled: (params: PromiseMultiplexerFulfilledParams) => JSX.Element, rejected: (error: PromiseMultiplexerRejectedParams) => JSX.Element, } /** * Function which selects and memoizes an output based on the {@link UsePromiseStatus} of a {@link usePromise} hook. * * It would be nice for it to be a component, but TSX does not support that, since it's a generic function, and that would make all its types `unknown`. */ export function promiseMultiplexer(config: PromiseMultiplexerConfig): JSX.Element { switch (config.hook.status) { case UsePromiseStatus.READY: return config.ready({ run: config.hook.run }) case UsePromiseStatus.PENDING: return config.pending({}) case UsePromiseStatus.FULFILLED: return config.fulfilled({ run: config.hook.run, result: config.hook.result! }) case UsePromiseStatus.REJECTED: return config.rejected({ run: config.hook.run, error: config.hook.error! }) } }