You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

69 lines
1.8 KiB
TypeScript

import { Fragment, VNode, h } from "preact";
import { useState, useEffect } from "preact/hooks";
export type AsyncState<T, E = any> =
| { loading: true }
| { loading: boolean; data: T }
| { loading: boolean; error: E };
const asyncState = <T, E = any>(state: AsyncState<T, E>) => {
if ("data" in state) {
return state.loading ? ("stale" as const) : ("loaded" as const);
} else if ("error" in state) {
return state.loading ? ("staleError" as const) : ("error" as const);
}
return "loading";
};
export const useAsync = <Fn extends (...args: any) => Promise<any>, E = any>(
fn: Fn,
args: Parameters<Fn>,
): AsyncState<Awaited<ReturnType<Fn>>, E> => {
const [state, setState] = useState<AsyncState<Awaited<ReturnType<Fn>>>>({
loading: true,
});
useEffect(() => {
(async () => {
setState((state) => ({ ...state, loading: true }));
try {
const data = await fn(args);
setState((state) => ({ ...state, loading: false, data }));
} catch (error) {
setState((state) => ({ ...state, loading: false, error }));
}
})();
}, args);
return state;
};
export const Loader = <Fn extends (...args: any) => Promise<any>, E = any>(p: {
renderLoading?: () => VNode;
renderError?: (p: E, loading: boolean) => VNode;
renderLoaded: (p: Awaited<ReturnType<Fn>>, loading: boolean) => VNode;
fn: Fn;
args: Parameters<Fn>;
preferLoading?: boolean;
}) => {
const {
fn,
args,
preferLoading = true,
renderLoaded,
renderLoading = () => h(Fragment, {}, "Loading..."),
renderError = (e) => h(Fragment, {}, `${e}`),
} = p;
const res = useAsync(fn, args);
if (res.loading && preferLoading) {
return renderLoading();
} else if ("data" in res) {
return renderLoaded(res.data, res.loading);
} else if ("error" in res) {
return renderError(res.error, res.loading);
} else {
return renderLoading();
}
};