useFetch hook (with optional view transition)
Had to use a .d.ts and .js file to get all the types to align and make all the tools happy, IDE/eslint/TypeScript.
This commit is contained in:
parent
6542cee415
commit
f06a7381e7
frontend/src
@ -1,57 +1,21 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import "./App.css";
|
||||
import { useBackend } from "./hooks/backend.ts";
|
||||
import { paths } from "./lib/api";
|
||||
import suLogoLandscape from "./assets/SU_logo_optimized.svg";
|
||||
import { ProfileContext } from "./hooks/profile.ts";
|
||||
import Studentportalen from "./Studentportalen.tsx";
|
||||
|
||||
type Profile =
|
||||
paths["/profile"]["get"]["responses"]["200"]["content"]["application/json"];
|
||||
import { useViewTransitioningFetch } from "./hooks/fetch";
|
||||
|
||||
function App() {
|
||||
const { client } = useBackend();
|
||||
const [state, setState] = useState<
|
||||
"initializing" | "authenticating" | "error" | Profile
|
||||
>("initializing");
|
||||
const { data: profile, error } = useViewTransitioningFetch("/profile");
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
withViewTransition(() => {
|
||||
return client
|
||||
.GET("/profile", { signal: controller.signal })
|
||||
.then(({ data, response }) => {
|
||||
if (data) {
|
||||
setState(data);
|
||||
}
|
||||
if (response.status === 401) {
|
||||
setState("authenticating");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.name !== "AbortError") {
|
||||
setState("error");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
controller.abort();
|
||||
};
|
||||
}, [client]);
|
||||
|
||||
if (state === "initializing") {
|
||||
if (!profile) {
|
||||
return splashScreen("Loading...");
|
||||
}
|
||||
if (state === "authenticating") {
|
||||
return splashScreen("Logging in...");
|
||||
}
|
||||
if (state === "error") {
|
||||
if (error) {
|
||||
return splashScreen("Application failed to start");
|
||||
}
|
||||
|
||||
return (
|
||||
<ProfileContext value={state}>
|
||||
<ProfileContext value={profile}>
|
||||
<Studentportalen />
|
||||
</ProfileContext>
|
||||
);
|
||||
@ -70,9 +34,4 @@ function splashScreen(extraContent: string) {
|
||||
);
|
||||
}
|
||||
|
||||
function withViewTransition(f: () => Promise<unknown>): void {
|
||||
if (document.startViewTransition) void document.startViewTransition(f);
|
||||
else void f();
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
39
frontend/src/hooks/fetch.d.ts
vendored
Normal file
39
frontend/src/hooks/fetch.d.ts
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
// Using a .js & .d.ts file to make the types work well at use sites
|
||||
// and make TypeScript, IDE, and eslint happy.
|
||||
//
|
||||
// Important to use `useMemo` on any options object passed in
|
||||
// since it is used as a dependency to re-fetch
|
||||
|
||||
import {
|
||||
PathsWithMethod,
|
||||
type RequiredKeysOf,
|
||||
} from "openapi-typescript-helpers";
|
||||
import { paths } from "../lib/api";
|
||||
import { FetchResponse, MaybeOptionalInit } from "openapi-fetch";
|
||||
|
||||
type InitParam<Init> =
|
||||
RequiredKeysOf<Init> extends never
|
||||
? [(Init & { [key: string]: unknown })?]
|
||||
: [Init & { [key: string]: unknown }];
|
||||
|
||||
export function useViewTransitioningFetch<
|
||||
Path extends PathsWithMethod<paths, "get">,
|
||||
Init extends MaybeOptionalInit<paths[Path], "get">,
|
||||
>(
|
||||
path: Path,
|
||||
...init: InitParam<Init>
|
||||
): Omit<
|
||||
FetchResponse<paths[Path]["get"], Init, "application/json">,
|
||||
"response"
|
||||
>;
|
||||
|
||||
export function useFetch<
|
||||
Path extends PathsWithMethod<paths, "get">,
|
||||
Init extends MaybeOptionalInit<paths[Path], "get">,
|
||||
>(
|
||||
path: Path,
|
||||
...init: InitParam<Init>
|
||||
): Omit<
|
||||
FetchResponse<paths[Path]["get"], Init, "application/json">,
|
||||
"response"
|
||||
>;
|
65
frontend/src/hooks/fetch.js
Normal file
65
frontend/src/hooks/fetch.js
Normal file
@ -0,0 +1,65 @@
|
||||
// Using a .js & .d.ts file to make the types work well at use sites
|
||||
// and make TypeScript, IDE, and eslint happy.
|
||||
//
|
||||
// Important to use `useMemo` on any options object passed in
|
||||
// since it is used as a dependency to re-fetch
|
||||
|
||||
import { useBackend } from "./backend.ts";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function useFetch(path, options) {
|
||||
const { client } = useBackend();
|
||||
const [response, setResponse] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
const abortController = new AbortController();
|
||||
|
||||
void doFetch(client, path, options, abortController, setResponse);
|
||||
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
}, [client, path, options]);
|
||||
|
||||
return { data: response.data, error: response.error };
|
||||
}
|
||||
|
||||
export function useViewTransitioningFetch(path, options) {
|
||||
const { client } = useBackend();
|
||||
const [response, setResponse] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
const abortController = new AbortController();
|
||||
|
||||
withViewTransition(function () {
|
||||
return doFetch(client, path, options, abortController, setResponse);
|
||||
});
|
||||
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
}, [client, path, options]);
|
||||
|
||||
return { data: response.data, error: response.error };
|
||||
}
|
||||
|
||||
async function doFetch(client, path, options, abortController, setResponse) {
|
||||
try {
|
||||
const response = await client.GET(path, {
|
||||
...options,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
setResponse(response);
|
||||
} catch (error) {
|
||||
if (abortController.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
setResponse({ error });
|
||||
}
|
||||
}
|
||||
|
||||
function withViewTransition(f) {
|
||||
if (document.startViewTransition) void document.startViewTransition(f);
|
||||
else void f();
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user