Skip to content

Commit f1a4d4b

Browse files
authored
Publisher pages lazy imports, pt1 (#5460)
* feature: lazy Suspense wrapper component * feature: lazy import AccountDetails * fix: linting * feature: importComponent test
1 parent 418d49f commit f1a4d4b

5 files changed

Lines changed: 90 additions & 1 deletion

File tree

static/js/publisher/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { createRoot } from "react-dom/client";
44
import { QueryClient, QueryClientProvider } from "react-query";
55
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
66

7+
import { importComponent } from "./utils/importComponent";
78
import BrandStoreRoute from "./components/BrandStoreRoute/BrandStoreRoute";
89
import PublisherLayout from "./layouts/PublisherLayout";
9-
import AccountDetails from "./pages/AccountDetails";
1010
import AccountSnaps from "./pages/AccountSnaps";
1111
import BrandStoreSettings from "./pages/BrandStoreSettings";
1212
import Build from "./pages/Build";
@@ -29,6 +29,8 @@ import ValidationSet from "./pages/ValidationSet";
2929
import ValidationSets from "./pages/ValidationSets";
3030
import AccountKeys from "./pages/AccountKeys";
3131

32+
const AccountDetails = importComponent(() => import("./pages/AccountDetails"));
33+
3234
Sentry.init({
3335
dsn: window.SENTRY_DSN,
3436
integrations: [Sentry.browserTracingIntegration()],

static/js/publisher/pages/AccountDetails/AccountDetails.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from "@canonical/react-components";
1212

1313
import { publisherState } from "../../state/publisherState";
14+
import { setPageTitle } from "../../utils";
1415

1516
function AccountDetails(): React.JSX.Element {
1617
const [subscriptionPreferencesChanged, setSubscriptionPreferencesChanged] =
@@ -21,6 +22,10 @@ function AccountDetails(): React.JSX.Element {
2122
const [isSaving, setIsSaving] = useState(false);
2223
const publisher = useAtomValue(publisherState);
2324

25+
useEffect(() => {
26+
setPageTitle(`Account details`);
27+
}, []);
28+
2429
useEffect(() => {
2530
setSubscribeToNewsletter(
2631
publisher?.subscriptions ? publisher?.subscriptions?.newsletter : false,
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {
2+
render,
3+
screen,
4+
act,
5+
waitForElementToBeRemoved,
6+
} from "@testing-library/react";
7+
import "@testing-library/jest-dom";
8+
9+
import { importComponent } from "../importComponent";
10+
11+
function renderComponent() {
12+
const sleep = (n: number) => new Promise<void>((r) => setTimeout(r, n));
13+
14+
function MockComponent() {
15+
return <h1>Mock Component!</h1>;
16+
}
17+
18+
const LazyComponent = importComponent(async () => {
19+
await sleep(100);
20+
21+
return {
22+
default: MockComponent,
23+
};
24+
});
25+
26+
return render(<LazyComponent />);
27+
}
28+
29+
describe("importComponent", () => {
30+
test("renders loader", () => {
31+
act(() => {
32+
renderComponent();
33+
});
34+
35+
expect(screen.getByText(/Loading/)).toBeInTheDocument();
36+
});
37+
38+
test("renders component", async () => {
39+
await act(async () => {
40+
renderComponent();
41+
});
42+
43+
await waitForElementToBeRemoved(() => screen.getByText(/Loading/));
44+
45+
expect(screen.getByText(/Mock Component!/)).toBeInTheDocument();
46+
});
47+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { forwardRef, lazy, Suspense } from "react";
2+
import { Spinner } from "@canonical/react-components";
3+
4+
type LoaderFn<TComponent extends React.ComponentType> = () => Promise<{
5+
default: TComponent;
6+
}>;
7+
8+
/**
9+
* Helper function that uses `React.lazy` to load a component and wrap it in a `React.Suspense` boundary
10+
*
11+
* @param {LoaderFn<TComponent>} loader function that executes the lazy import for `Component`
12+
* @param {React.ReactNode} fallback element that will be displayed while waiting for `Component` to load, optional
13+
* @return a `Suspense` boundary that wraps `Component`
14+
*/
15+
export function importComponent<TComponent extends React.ComponentType>(
16+
loader: LoaderFn<TComponent>,
17+
fallback?: React.ReactNode,
18+
) {
19+
const Component = lazy(loader);
20+
21+
return forwardRef<unknown, React.ComponentPropsWithRef<TComponent>>(
22+
(props, ref) => (
23+
<Suspense fallback={fallback ? fallback : <Spinner text="Loading..." />}>
24+
{/*
25+
Something is wrong with the types for Component and its props,
26+
but it does actually work at runtime and types are inferred properly
27+
outisde of this file, so...
28+
*/}
29+
{/* @ts-expect-error: let's just ignore this error for the moment */}
30+
<Component {...props} ref={ref} />
31+
</Suspense>
32+
),
33+
);
34+
}

vite.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export default defineConfig({
9191
},
9292
],
9393
},
94+
base: "./", // use the script's URL path as base when loading assets in dynamic imports
9495
build: {
9596
manifest: true,
9697
modulePreload: false,

0 commit comments

Comments
 (0)