You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-11-20 12:02:22 +03:00
frontend: Migrate to jotai and urql
This cuts the bundle size by 50% and makes it easier to reason about state. It removes the usage of react-router-dom and replaces it with a simple router atom based on jotai-location. Since the screens will be quite simple, I don't expect that we'll need the advanced caching features of react-relay, hence the switch to urql.
This commit is contained in:
@@ -12,52 +12,153 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { lazy, Suspense } from "react";
|
||||
import { createBrowserRouter, Outlet, RouterProvider } from "react-router-dom";
|
||||
import { lazy, Suspense, useTransition } from "react";
|
||||
import { atomWithLocation } from "jotai-location";
|
||||
import { atom, useAtomValue, useSetAtom } from "jotai";
|
||||
|
||||
import Layout from "./components/Layout";
|
||||
import LoadingSpinner from "./components/LoadingSpinner";
|
||||
|
||||
type Location = {
|
||||
pathname?: string;
|
||||
searchParams?: URLSearchParams;
|
||||
};
|
||||
|
||||
type HomeRoute = { type: "home" };
|
||||
type DumbRoute = { type: "dumb" };
|
||||
type OAuth2ClientRoute = { type: "client"; id: string };
|
||||
type BrowserSessionRoute = { type: "session"; id: string };
|
||||
type UnknownRoute = { type: "unknown"; segments: string[] };
|
||||
|
||||
export type Route =
|
||||
| HomeRoute
|
||||
| DumbRoute
|
||||
| OAuth2ClientRoute
|
||||
| BrowserSessionRoute
|
||||
| UnknownRoute;
|
||||
|
||||
const routeToSegments = (route: Route): string[] => {
|
||||
switch (route.type) {
|
||||
case "home":
|
||||
return [];
|
||||
case "dumb":
|
||||
return ["dumb"];
|
||||
case "client":
|
||||
return ["client", route.id];
|
||||
case "session":
|
||||
return ["session", route.id];
|
||||
case "unknown":
|
||||
return route.segments;
|
||||
}
|
||||
};
|
||||
|
||||
const segmentsToRoute = (segments: string[]): Route => {
|
||||
if (segments.length === 0 || (segments.length === 1 && segments[0] === "")) {
|
||||
return { type: "home" };
|
||||
}
|
||||
|
||||
if (segments.length === 1 && segments[0] === "dumb") {
|
||||
return { type: "dumb" };
|
||||
}
|
||||
|
||||
if (segments.length === 2 && segments[0] === "client") {
|
||||
return { type: "client", id: segments[1] };
|
||||
}
|
||||
|
||||
if (segments.length === 2 && segments[0] === "session") {
|
||||
return { type: "session", id: segments[1] };
|
||||
}
|
||||
|
||||
return { type: "unknown", segments };
|
||||
};
|
||||
|
||||
const routeToPath = (route: Route): string =>
|
||||
routeToSegments(route)
|
||||
.map((part) => encodeURIComponent(part))
|
||||
.join("/");
|
||||
|
||||
const pathToRoute = (path: string): Route => {
|
||||
const segments = path.split("/").map(decodeURIComponent);
|
||||
return segmentsToRoute(segments);
|
||||
};
|
||||
|
||||
const locationToRoute = (location: Location): Route => {
|
||||
if (
|
||||
!location.pathname ||
|
||||
!location.pathname.startsWith(window.APP_CONFIG.root)
|
||||
) {
|
||||
throw new Error("Invalid location");
|
||||
}
|
||||
|
||||
const path = location.pathname.slice(window.APP_CONFIG.root.length);
|
||||
return pathToRoute(path);
|
||||
};
|
||||
|
||||
const locationAtom = atomWithLocation();
|
||||
export const routeAtom = atom(
|
||||
(get) => locationToRoute(get(locationAtom)),
|
||||
(_get, set, value: Route) => {
|
||||
set(locationAtom, {
|
||||
pathname: window.APP_CONFIG.root + routeToPath(value),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const Home = lazy(() => import("./pages/Home"));
|
||||
const OAuth2Client = lazy(() => import("./pages/OAuth2Client"));
|
||||
const BrowserSession = lazy(() => import("./pages/BrowserSession"));
|
||||
|
||||
export const router = createBrowserRouter(
|
||||
[
|
||||
{
|
||||
path: "/",
|
||||
element: (
|
||||
<Layout>
|
||||
<Suspense fallback={<LoadingSpinner />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</Layout>
|
||||
),
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Home />,
|
||||
},
|
||||
{
|
||||
path: "dumb",
|
||||
element: <>Hello from another dumb page.</>,
|
||||
},
|
||||
{
|
||||
path: "client/:id",
|
||||
element: <OAuth2Client />,
|
||||
},
|
||||
{
|
||||
path: "session/:id",
|
||||
element: <BrowserSession />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
{
|
||||
basename: window.APP_CONFIG.root,
|
||||
const InnerRouter: React.FC = () => {
|
||||
const route = useAtomValue(routeAtom);
|
||||
|
||||
switch (route.type) {
|
||||
case "home":
|
||||
return <Home />;
|
||||
case "client":
|
||||
return <OAuth2Client id={route.id} />;
|
||||
case "session":
|
||||
return <BrowserSession id={route.id} />;
|
||||
case "dumb":
|
||||
return <>Dumb route.</>;
|
||||
case "unknown":
|
||||
return <>Unknown route {JSON.stringify(route.segments)}</>;
|
||||
}
|
||||
};
|
||||
|
||||
const Router = () => (
|
||||
<Layout>
|
||||
<Suspense fallback={<LoadingSpinner />}>
|
||||
<InnerRouter />
|
||||
</Suspense>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
const Router = () => <RouterProvider router={router} />;
|
||||
export const Link: React.FC<
|
||||
{
|
||||
route: Route;
|
||||
children: React.ReactNode;
|
||||
} & React.HTMLProps<HTMLAnchorElement>
|
||||
> = ({ route, children, ...props }) => {
|
||||
const path = routeToPath(route);
|
||||
const setRoute = useSetAtom(routeAtom);
|
||||
|
||||
// TODO: we should probably have more user control over this
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
return (
|
||||
<a
|
||||
href={path}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
startTransition(() => {
|
||||
setRoute(route);
|
||||
});
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{isPending ? "Loading..." : children}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default Router;
|
||||
|
||||
Reference in New Issue
Block a user