diff --git a/src/Async.tsx b/src/Async.tsx new file mode 100644 index 0000000..3c35729 --- /dev/null +++ b/src/Async.tsx @@ -0,0 +1,111 @@ +import React, {useMemo, useState} from "react"; + +/** + * Asynchronous data state. + */ +interface AsyncState +{ + /** + * Determine if we are waiting for the promise result or not. + */ + pending: boolean; + /** + * The promise which is retrieved (or has retrieved) data. + */ + promise: Promise; + /** + * Error thrown by the promise. + */ + error: Error; + /** + * The promise result. + */ + data: T; +} + +/** + * A promise production function. + */ +export type PromiseFn = () => Promise; + +/** + * React hook for promise result retrieval. + * @param promise The promise or a function that produces a promise. + * @param deps When one of the `deps` change, it will wait for the promise again. + */ +export function useAsync(promise: Promise|PromiseFn, deps: any[] = []): AsyncState +{ + // Get the actual promise from the function if there is one. + if ((promise as PromiseFn)?.call) + promise = (promise as PromiseFn)(); + else + promise = Promise.race([promise as Promise]); + + // The async state. + const [state, setState] = useState>({ + pending: true, + promise: promise, + error: undefined, + data: undefined, + }); + + /** + * Partial update of an async state. + * @param stateUpdate A partial update object. + */ + const updateState = (stateUpdate: Partial>) => { + // Copy the original state and apply the partial state update. + setState(Object.assign({}, state, stateUpdate)); + }; + + // Reconfigure the promise when any deps have changed. + useMemo(() => { + (promise as Promise).then((result) => { + // When there is a result, disable pending state and set retrieved data, without error. + updateState({ + pending: false, + error: undefined, + data: result, + }) + }).catch((error) => { + // An error happened, disable pending state, reset data, and set the error. + updateState({ + pending: false, + error: error, + data: undefined, + }) + }); + + // Promise is ready: reset the state to pending with the configured promise, without data and error. + updateState({ + pending: true, + promise: promise as Promise, + error: undefined, + data: undefined, + }); + }, deps); + + return state; // Return the current async state. +} + +/** + * Wait for the promise to be fulfilled to render the children. + * @param async The async state. + * @param children Renderer function of the children, takes promised data as argument. + * @param fallback Content shown when the promise is not fulfilled yet. + * @constructor + */ +export function Await({ async, children, fallback }: { + async: AsyncState; + children: (async: T) => React.ReactElement; + fallback?: React.ReactElement; +}) +{ + // Still waiting for the promised data, showing fallback content. + if (async.pending) return fallback ?? <>; + // An error happened, throwing it. + if (async.error) throw async.error; + + // Promise is fulfilled, rendering the children with result data. + return children(async.data); +}