1
Fork 0
mirror of https://github.com/Steffo99/festa.git synced 2024-10-16 15:07:27 +00:00
festa/components/generic/loading/promise.tsx
2022-07-16 16:06:07 +02:00

145 lines
4.8 KiB
TypeScript

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<D> = { 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<E = Error> = { type: "reject", error: E }
/**
* Actions that can be performed on the {@link useReducer} hook used inside {@link usePromise}.
*/
type UsePromiseAction<D, E = Error> = UsePromiseActionRun | UsePromiseActionFulfill<D> | UsePromiseActionReject<E>;
/**
* The internal state of the {@link useReducer} hook used inside {@link usePromise}.
*/
type UsePromiseState<D, E = Error> = {
status: UsePromiseStatus,
result: D | undefined,
error: E | undefined,
}
/**
* The initial {@link UsePromiseState} of the {@link usePromise} hook.
*/
const initialUsePromise: UsePromiseState<any, any> = {
status: UsePromiseStatus.READY,
result: undefined,
error: undefined,
}
/**
* The reducer used by {@link usePromise}.
*/
function reducerUsePromise<D, E = Error>(prev: UsePromiseState<D, E>, action: UsePromiseAction<D, E>): UsePromiseState<D, E> {
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<D, P> = (params: P) => Promise<D>
/**
* Values returned by the {@link usePromise} hook.
*/
export type UsePromise<D, P, E = Error> = UsePromiseState<D, E> & { run: (params: P) => Promise<void> }
/**
* Hook executing an asyncronous function in a way that can be handled by React components.
*/
export function usePromise<D, P, E = Error>(func: UsePromiseFunction<D, P>): UsePromise<D, P, E> {
const [state, dispatch] = useReducer<Reducer<UsePromiseState<D, E>, UsePromiseAction<D, E>>>(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<D, P, E = Error> = {
run: UsePromise<D, P, E>["run"],
}
export type PromiseMultiplexerPendingParams<D, P, E = Error> = {
}
export type PromiseMultiplexerFulfilledParams<D, P, E = Error> = {
run: UsePromise<D, P, E>["run"],
result: D,
}
export type PromiseMultiplexerRejectedParams<D, P, E = Error> = {
run: UsePromise<D, P, E>["run"],
error: E,
}
export type PromiseMultiplexerConfig<D, P, E = Error> = {
hook: UsePromise<D, P, E>,
ready: (params: PromiseMultiplexerReadyParams<D, P, E>) => JSX.Element,
pending: (params: PromiseMultiplexerPendingParams<D, P, E>) => JSX.Element,
fulfilled: (params: PromiseMultiplexerFulfilledParams<D, P, E>) => JSX.Element,
rejected: (error: PromiseMultiplexerRejectedParams<D, P, E>) => 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<D, P, E = Error>(config: PromiseMultiplexerConfig<D, P, E>): 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! })
}
}