From 7fdd49798086520185284614f5a368e798015ddd Mon Sep 17 00:00:00 2001 From: idylls Date: Sun, 7 May 2023 14:45:30 -0400 Subject: [PATCH] Begin work on data fetching --- frontend/client.ts | 4 ++ frontend/components/Home/Home.ts | 16 +++++++- frontend/fetch.ts | 37 ----------------- frontend/useAsync.ts | 68 ++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 39 deletions(-) create mode 100644 frontend/client.ts delete mode 100644 frontend/fetch.ts create mode 100644 frontend/useAsync.ts diff --git a/frontend/client.ts b/frontend/client.ts new file mode 100644 index 0000000..b2d467f --- /dev/null +++ b/frontend/client.ts @@ -0,0 +1,4 @@ +import { MakeClient } from "../common/deps/yaypi.ts"; +import { Api } from "../common/api.ts"; + +export type Client = MakeClient; diff --git a/frontend/components/Home/Home.ts b/frontend/components/Home/Home.ts index fc2759c..4156162 100644 --- a/frontend/components/Home/Home.ts +++ b/frontend/components/Home/Home.ts @@ -1,7 +1,13 @@ import { h } from "preact"; import { Dispatch } from "../../utils.ts"; import { Action } from "./state.ts"; -import { Client } from "../../fetch.ts"; +import { assert } from "../../../common/deps/yaypi.ts"; +import { Loader, useAsync } from "../../useAsync.ts"; +import { Client } from "../../client.ts"; + +export const Schemas = (p: unknown) => { + return h("div", {}, "schemas loaded"); +}; export const Home = (p: { dispatch: Dispatch; client: Client }) => { const button = h( @@ -12,6 +18,12 @@ export const Home = (p: { dispatch: Dispatch; client: Client }) => { "New schema", ); + const schemas = h(Loader, { + fn: async () => assert(await p.client.v1.listSchemas(null)), + args: [], + renderLoaded: Schemas, + }); + const b = h( "button", { @@ -20,5 +32,5 @@ export const Home = (p: { dispatch: Dispatch; client: Client }) => { "Say hello", ); - return h("div", {}, button, b); + return h("div", {}, schemas, button, b); }; diff --git a/frontend/fetch.ts b/frontend/fetch.ts deleted file mode 100644 index f77740d..0000000 --- a/frontend/fetch.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Reducer, useMemo, useReducer } from "preact/hooks"; -import { MakeClient, assert, makeClient } from "../common/deps/yaypi.ts"; -import { Api } from "../common/api.ts"; - -export type Client = MakeClient; - -export type Loadable = - | null - | { loading: true } - | { loading: boolean; data: T } - | { loading: boolean; error: string }; -export const isLoading = (loadable: Loadable) => loadable?.loading; - -export type Data = { - schemas: Loadable>; -}; -export type Action = never; - -const reduce: Reducer = (data, action) => { - return data; -}; - -const emptyData: Data = { - schemas: null, -}; - -// NOTE: this should only be used in App.ts -export const useFetch = () => { - const client = useMemo(() => makeClient(Api, "/api"), []); - const [data, reducer] = useReducer(reduce, emptyData); - - const listSchemas = async ( - ...args: Parameters - ) => { - const schemas = assert(await client.v1.listSchemas(...args)); - }; -}; diff --git a/frontend/useAsync.ts b/frontend/useAsync.ts new file mode 100644 index 0000000..2232ae7 --- /dev/null +++ b/frontend/useAsync.ts @@ -0,0 +1,68 @@ +import { Fragment, VNode, h } from "preact"; +import { useState, useEffect } from "preact/hooks"; + +export type AsyncState = + | { loading: true } + | { loading: boolean; data: T } + | { loading: boolean; error: E }; +const asyncState = (state: AsyncState) => { + 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 = Promise, E = any>( + fn: Fn, + args: Parameters, +): AsyncState>, E> => { + const [state, setState] = useState>>>({ + 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 = Promise, E = any>(p: { + renderLoading?: () => VNode; + renderError?: (p: E, loading: boolean) => VNode; + renderLoaded: (p: Awaited>, loading: boolean) => VNode; + fn: Fn; + args: Parameters; + 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(); + } +};