From c4e4b16300accd8afc37937c4ed79ae00ed6c10f Mon Sep 17 00:00:00 2001 From: gearnode Date: Tue, 25 Mar 2025 16:16:11 +0100 Subject: [PATCH] Use @probo/react-lazy instead of native React.lazy close #58 Signed-off-by: gearnode --- apps/console/package.json | 3 +- apps/console/src/App.tsx | 3 +- .../organizations/CreateOrganizationPage.tsx | 3 +- .../src/pages/organizations/SettingsPage.tsx | 3 +- .../frameworks/CreateFrameworkPage.tsx | 3 +- .../frameworks/FrameworkListPage.tsx | 5 +- .../frameworks/FrameworkPage.tsx | 3 +- .../frameworks/UpdateFrameworkPage.tsx | 3 +- .../frameworks/controls/ControlPage.tsx | 3 +- .../frameworks/controls/CreateControlPage.tsx | 3 +- .../frameworks/controls/UpdateControlPage.tsx | 3 +- .../organizations/people/CreatePeoplePage.tsx | 3 +- .../organizations/people/PeopleListPage.tsx | 3 +- .../pages/organizations/people/PeoplePage.tsx | 3 +- .../policies/CreatePolicyPage.tsx | 3 +- .../organizations/policies/PolicyListPage.tsx | 3 +- .../organizations/policies/PolicyPage.tsx | 3 +- .../policies/UpdatePolicyPage.tsx | 3 +- .../organizations/vendors/VendorListPage.tsx | 3 +- .../organizations/vendors/VendorPage.tsx | 3 +- package-lock.json | 12 ++ packages/react-lazy/package.json | 14 ++ packages/react-lazy/src/index.ts | 167 ++++++++++++++++++ packages/react-lazy/tsconfig.json | 5 + 24 files changed, 239 insertions(+), 21 deletions(-) create mode 100644 packages/react-lazy/package.json create mode 100644 packages/react-lazy/src/index.ts create mode 100644 packages/react-lazy/tsconfig.json diff --git a/apps/console/package.json b/apps/console/package.json index f95c08818..57863d037 100644 --- a/apps/console/package.json +++ b/apps/console/package.json @@ -44,7 +44,8 @@ "react-router": "^7.1.5", "relay-runtime": "^18.2.0", "tailwind-merge": "^3.0.2", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "@probo/react-lazy": "workspace:*" }, "devDependencies": { "@babel/core": "^7.26.7", diff --git a/apps/console/src/App.tsx b/apps/console/src/App.tsx index 158a8542b..e1b8bf231 100644 --- a/apps/console/src/App.tsx +++ b/apps/console/src/App.tsx @@ -1,6 +1,7 @@ import posthog from "posthog-js"; import { PostHogProvider } from "posthog-js/react"; -import { lazy, StrictMode, Suspense } from "react"; +import { lazy } from "@probo/react-lazy"; +import { StrictMode, Suspense } from "react"; import { createRoot } from "react-dom/client"; import { HelmetProvider } from "react-helmet-async"; import { RelayEnvironmentProvider } from "react-relay"; diff --git a/apps/console/src/pages/organizations/CreateOrganizationPage.tsx b/apps/console/src/pages/organizations/CreateOrganizationPage.tsx index e80f75974..b0f5df7f0 100644 --- a/apps/console/src/pages/organizations/CreateOrganizationPage.tsx +++ b/apps/console/src/pages/organizations/CreateOrganizationPage.tsx @@ -1,5 +1,6 @@ import { PageTemplateSkeleton } from "@/components/PageTemplate"; -import { lazy, Suspense } from "react"; +import { Suspense } from "react"; +import { lazy } from "@probo/react-lazy"; import { useLocation } from "react-router"; import { ErrorBoundaryWithLocation } from "./ErrorBoundary"; diff --git a/apps/console/src/pages/organizations/SettingsPage.tsx b/apps/console/src/pages/organizations/SettingsPage.tsx index 6b69432a9..02798fbf7 100644 --- a/apps/console/src/pages/organizations/SettingsPage.tsx +++ b/apps/console/src/pages/organizations/SettingsPage.tsx @@ -1,5 +1,6 @@ import { PageTemplateSkeleton } from "@/components/PageTemplate"; -import { lazy, Suspense } from "react"; +import { Suspense } from "react"; +import { lazy } from "@probo/react-lazy"; import { useLocation } from "react-router"; import { ErrorBoundaryWithLocation } from "./ErrorBoundary"; diff --git a/apps/console/src/pages/organizations/frameworks/CreateFrameworkPage.tsx b/apps/console/src/pages/organizations/frameworks/CreateFrameworkPage.tsx index 53302fbf6..a462447f8 100644 --- a/apps/console/src/pages/organizations/frameworks/CreateFrameworkPage.tsx +++ b/apps/console/src/pages/organizations/frameworks/CreateFrameworkPage.tsx @@ -1,5 +1,6 @@ import { PageTemplateSkeleton } from "@/components/PageTemplate"; -import { lazy, Suspense } from "react"; +import { Suspense } from "react"; +import { lazy } from "@probo/react-lazy"; import { useLocation } from "react-router"; import { ErrorBoundaryWithLocation } from "../ErrorBoundary"; diff --git a/apps/console/src/pages/organizations/frameworks/FrameworkListPage.tsx b/apps/console/src/pages/organizations/frameworks/FrameworkListPage.tsx index 8f83caa82..70f8d1002 100644 --- a/apps/console/src/pages/organizations/frameworks/FrameworkListPage.tsx +++ b/apps/console/src/pages/organizations/frameworks/FrameworkListPage.tsx @@ -1,7 +1,8 @@ -import { lazy, Suspense } from "react"; +import { PageTemplateSkeleton } from "@/components/PageTemplate"; +import { Suspense } from "react"; +import { lazy } from "@probo/react-lazy"; import { ErrorBoundaryWithLocation } from "../ErrorBoundary"; import { useLocation } from "react-router"; -import { PageTemplateSkeleton } from "@/components/PageTemplate"; import { Card, CardContent } from "@/components/ui/card"; const FrameworkListView = lazy(() => import("./FrameworkListView")); diff --git a/apps/console/src/pages/organizations/frameworks/FrameworkPage.tsx b/apps/console/src/pages/organizations/frameworks/FrameworkPage.tsx index 81c36f765..df350a23c 100644 --- a/apps/console/src/pages/organizations/frameworks/FrameworkPage.tsx +++ b/apps/console/src/pages/organizations/frameworks/FrameworkPage.tsx @@ -1,8 +1,9 @@ import { PageTemplateSkeleton } from "@/components/PageTemplate"; import { Card, CardContent } from "@/components/ui/card"; -import { lazy, Suspense } from "react"; +import { Suspense } from "react"; import { useLocation } from "react-router"; import { ErrorBoundaryWithLocation } from "../ErrorBoundary"; +import { lazy } from "@probo/react-lazy"; const FrameworkView = lazy(() => import("./FrameworkView")); diff --git a/apps/console/src/pages/organizations/frameworks/UpdateFrameworkPage.tsx b/apps/console/src/pages/organizations/frameworks/UpdateFrameworkPage.tsx index 9d14e21fb..3c1802227 100644 --- a/apps/console/src/pages/organizations/frameworks/UpdateFrameworkPage.tsx +++ b/apps/console/src/pages/organizations/frameworks/UpdateFrameworkPage.tsx @@ -1,5 +1,6 @@ import { PageTemplateSkeleton } from "@/components/PageTemplate"; -import { lazy, Suspense } from "react"; +import { Suspense } from "react"; +import { lazy } from "@probo/react-lazy"; import { useLocation } from "react-router"; import { ErrorBoundaryWithLocation } from "../ErrorBoundary"; diff --git a/apps/console/src/pages/organizations/frameworks/controls/ControlPage.tsx b/apps/console/src/pages/organizations/frameworks/controls/ControlPage.tsx index 37ccc7e8d..a237f6922 100644 --- a/apps/console/src/pages/organizations/frameworks/controls/ControlPage.tsx +++ b/apps/console/src/pages/organizations/frameworks/controls/ControlPage.tsx @@ -1,6 +1,7 @@ import { PageTemplateSkeleton } from "@/components/PageTemplate"; import { Card, CardContent } from "@/components/ui/card"; -import { lazy, Suspense } from "react"; +import { Suspense } from "react"; +import { lazy } from "@probo/react-lazy"; import { useLocation } from "react-router"; import { ErrorBoundaryWithLocation } from "../../ErrorBoundary"; diff --git a/apps/console/src/pages/organizations/frameworks/controls/CreateControlPage.tsx b/apps/console/src/pages/organizations/frameworks/controls/CreateControlPage.tsx index ace40c5fd..dc4139dae 100644 --- a/apps/console/src/pages/organizations/frameworks/controls/CreateControlPage.tsx +++ b/apps/console/src/pages/organizations/frameworks/controls/CreateControlPage.tsx @@ -1,7 +1,8 @@ import { PageTemplateSkeleton } from "@/components/PageTemplate"; import { ErrorBoundaryWithLocation } from "../../ErrorBoundary"; -import { lazy, Suspense } from "react"; +import { Suspense } from "react"; import { useLocation } from "react-router"; +import { lazy } from "@probo/react-lazy"; const CreateControlView = lazy(() => import("./CreateControlView")); diff --git a/apps/console/src/pages/organizations/frameworks/controls/UpdateControlPage.tsx b/apps/console/src/pages/organizations/frameworks/controls/UpdateControlPage.tsx index f5c1a5830..f3e38b29d 100644 --- a/apps/console/src/pages/organizations/frameworks/controls/UpdateControlPage.tsx +++ b/apps/console/src/pages/organizations/frameworks/controls/UpdateControlPage.tsx @@ -1,5 +1,6 @@ import { PageTemplateSkeleton } from "@/components/PageTemplate"; -import { lazy, Suspense } from "react"; +import { Suspense } from "react"; +import { lazy } from "@probo/react-lazy"; import { useLocation } from "react-router"; import { ErrorBoundaryWithLocation } from "../../ErrorBoundary"; diff --git a/apps/console/src/pages/organizations/people/CreatePeoplePage.tsx b/apps/console/src/pages/organizations/people/CreatePeoplePage.tsx index 712cb8e83..9fb26545f 100644 --- a/apps/console/src/pages/organizations/people/CreatePeoplePage.tsx +++ b/apps/console/src/pages/organizations/people/CreatePeoplePage.tsx @@ -1,5 +1,6 @@ import { PageTemplateSkeleton } from "@/components/PageTemplate"; -import { lazy, Suspense } from "react"; +import { Suspense } from "react"; +import { lazy } from "@probo/react-lazy"; import { useLocation } from "react-router"; import { ErrorBoundaryWithLocation } from "../ErrorBoundary"; diff --git a/apps/console/src/pages/organizations/people/PeopleListPage.tsx b/apps/console/src/pages/organizations/people/PeopleListPage.tsx index 00db19e61..bb2ce7430 100644 --- a/apps/console/src/pages/organizations/people/PeopleListPage.tsx +++ b/apps/console/src/pages/organizations/people/PeopleListPage.tsx @@ -1,7 +1,8 @@ import { PageTemplateSkeleton } from "@/components/PageTemplate"; -import { lazy, Suspense } from "react"; +import { Suspense } from "react"; import { useLocation } from "react-router"; import { ErrorBoundaryWithLocation } from "../ErrorBoundary"; +import { lazy } from "@probo/react-lazy"; const PeopleListView = lazy(() => import("./PeopleListView")); diff --git a/apps/console/src/pages/organizations/people/PeoplePage.tsx b/apps/console/src/pages/organizations/people/PeoplePage.tsx index 03e1dcd13..7f836b40a 100644 --- a/apps/console/src/pages/organizations/people/PeoplePage.tsx +++ b/apps/console/src/pages/organizations/people/PeoplePage.tsx @@ -1,5 +1,6 @@ import { PageTemplateSkeleton } from "@/components/PageTemplate"; -import { lazy, Suspense } from "react"; +import { Suspense } from "react"; +import { lazy } from "@probo/react-lazy"; import { useLocation } from "react-router"; import { ErrorBoundaryWithLocation } from "../ErrorBoundary"; diff --git a/apps/console/src/pages/organizations/policies/CreatePolicyPage.tsx b/apps/console/src/pages/organizations/policies/CreatePolicyPage.tsx index 751f7a205..e500e4db4 100644 --- a/apps/console/src/pages/organizations/policies/CreatePolicyPage.tsx +++ b/apps/console/src/pages/organizations/policies/CreatePolicyPage.tsx @@ -1,7 +1,8 @@ import { PageTemplateSkeleton } from "@/components/PageTemplate"; -import { lazy, Suspense } from "react"; +import { Suspense } from "react"; import { useLocation } from "react-router"; import { ErrorBoundaryWithLocation } from "../ErrorBoundary"; +import { lazy } from "@probo/react-lazy"; const CreatePolicyView = lazy(() => import("./CreatePolicyView")); diff --git a/apps/console/src/pages/organizations/policies/PolicyListPage.tsx b/apps/console/src/pages/organizations/policies/PolicyListPage.tsx index 5456e46d1..b1a8fb7e7 100644 --- a/apps/console/src/pages/organizations/policies/PolicyListPage.tsx +++ b/apps/console/src/pages/organizations/policies/PolicyListPage.tsx @@ -1,8 +1,9 @@ import { PageTemplateSkeleton } from "@/components/PageTemplate"; import { Card, CardContent, CardFooter } from "@/components/ui/card"; -import { lazy, Suspense } from "react"; +import { Suspense } from "react"; import { useLocation } from "react-router"; import { ErrorBoundaryWithLocation } from "../ErrorBoundary"; +import { lazy } from "@probo/react-lazy"; const PolicyListView = lazy(() => import("./PolicyListView")); diff --git a/apps/console/src/pages/organizations/policies/PolicyPage.tsx b/apps/console/src/pages/organizations/policies/PolicyPage.tsx index 0f840ecd5..a4fbf5a99 100644 --- a/apps/console/src/pages/organizations/policies/PolicyPage.tsx +++ b/apps/console/src/pages/organizations/policies/PolicyPage.tsx @@ -1,8 +1,9 @@ import { PageTemplateSkeleton } from "@/components/PageTemplate"; import { Card, CardContent } from "@/components/ui/card"; -import { lazy, Suspense } from "react"; +import { Suspense } from "react"; import { useLocation } from "react-router"; import { ErrorBoundaryWithLocation } from "../ErrorBoundary"; +import { lazy } from "@probo/react-lazy"; const PolicyView = lazy(() => import("./PolicyView")); diff --git a/apps/console/src/pages/organizations/policies/UpdatePolicyPage.tsx b/apps/console/src/pages/organizations/policies/UpdatePolicyPage.tsx index 0108b5a72..b6160335f 100644 --- a/apps/console/src/pages/organizations/policies/UpdatePolicyPage.tsx +++ b/apps/console/src/pages/organizations/policies/UpdatePolicyPage.tsx @@ -1,5 +1,6 @@ import { PageTemplateSkeleton } from "@/components/PageTemplate"; -import { lazy, Suspense } from "react"; +import { Suspense } from "react"; +import { lazy } from "@probo/react-lazy"; import { useLocation } from "react-router"; import { ErrorBoundaryWithLocation } from "../ErrorBoundary"; diff --git a/apps/console/src/pages/organizations/vendors/VendorListPage.tsx b/apps/console/src/pages/organizations/vendors/VendorListPage.tsx index bb18bfb22..501882bc1 100644 --- a/apps/console/src/pages/organizations/vendors/VendorListPage.tsx +++ b/apps/console/src/pages/organizations/vendors/VendorListPage.tsx @@ -1,5 +1,6 @@ import { PageTemplateSkeleton } from "@/components/PageTemplate"; -import { lazy, Suspense } from "react"; +import { Suspense } from "react"; +import { lazy } from "@probo/react-lazy"; import { useLocation } from "react-router"; import { ErrorBoundaryWithLocation } from "../ErrorBoundary"; diff --git a/apps/console/src/pages/organizations/vendors/VendorPage.tsx b/apps/console/src/pages/organizations/vendors/VendorPage.tsx index fcd0f5ed9..aa8726e90 100644 --- a/apps/console/src/pages/organizations/vendors/VendorPage.tsx +++ b/apps/console/src/pages/organizations/vendors/VendorPage.tsx @@ -1,5 +1,6 @@ import { PageTemplateSkeleton } from "@/components/PageTemplate"; -import { lazy, Suspense } from "react"; +import { Suspense } from "react"; +import { lazy } from "@probo/react-lazy"; import { useLocation } from "react-router"; import { ErrorBoundaryWithLocation } from "../ErrorBoundary"; diff --git a/package-lock.json b/package-lock.json index b84041380..6a292948e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2807,6 +2807,10 @@ "resolved": "packages/esbuild-plugin-postcss", "link": true }, + "node_modules/@probo/react-lazy": { + "resolved": "packages/react-lazy", + "link": true + }, "node_modules/@probo/tsconfig": { "resolved": "packages/tsconfig", "link": true @@ -10384,6 +10388,14 @@ "esbuild": "^0.25.0" } }, + "packages/react-lazy": { + "name": "@probo/react-lazy", + "version": "0.1.0", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, "packages/tsconfig": { "name": "@probo/tsconfig", "version": "0.0.1", diff --git a/packages/react-lazy/package.json b/packages/react-lazy/package.json new file mode 100644 index 000000000..292234f41 --- /dev/null +++ b/packages/react-lazy/package.json @@ -0,0 +1,14 @@ +{ + "name": "@probo/react-lazy", + "version": "0.1.0", + "description": "Enhanced React lazy loading with retries and error handling", + "private": true, + "main": "src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } +} diff --git a/packages/react-lazy/src/index.ts b/packages/react-lazy/src/index.ts new file mode 100644 index 000000000..c6972f8e7 --- /dev/null +++ b/packages/react-lazy/src/index.ts @@ -0,0 +1,167 @@ +import { + lazy as reactLazy, + type LazyExoticComponent, + type ComponentType, + type FC, +} from "react"; + +export const DEFAULT_STORAGE_KEY = "reactLazyReloadedFunctions"; + +export class ReloadStorage { + constructor(public readonly storageKey = DEFAULT_STORAGE_KEY) {} + + getMap(): Map { + const stored = sessionStorage.getItem(this.storageKey); + + try { + const parsed: unknown = stored ? JSON.parse(stored) : []; + return new Map( + Array.isArray(parsed) + ? parsed.filter( + (value) => + Array.isArray(value) && + typeof value[0] === "string" && + typeof value[1] === "number" + ) + : [] + ); + } catch (error) { + console.error("Error parsing react-lazy storage:", error); + return new Map(); + } + } + + addFunction(functionName: string): void { + const stored = this.getMap(); + stored.set(functionName, (stored.get(functionName) ?? 0) + 1); + this.save(stored); + } + + removeFunction(functionName: string): void { + const stored = this.getMap(); + stored.delete(functionName); + this.save(stored); + } + + protected save(stored: Map): void { + sessionStorage.setItem( + this.storageKey, + JSON.stringify([...stored.entries()]) + ); + } +} + +export interface ForceReloadConfig { + maxRetries: number; + storageKey?: string; +} + +export interface LazyConfigInit { + forceReload?: false | Partial; + importRetries?: number; + retryDelay?: number; + onImportError?: (error: unknown) => void; +} + +export interface LazyConfig { + forceReload: ForceReloadConfig; + importRetries: number; + retryDelay: number; + onImportError?: (error: unknown) => void; +} + +const createConfig = (init: LazyConfigInit = {}): LazyConfig => ({ + forceReload: { + maxRetries: 1, + storageKey: DEFAULT_STORAGE_KEY, + ...(typeof init.forceReload === "object" + ? init.forceReload + : init.forceReload === false + ? { maxRetries: 0 } + : {}), + }, + importRetries: + typeof init.importRetries === "number" + ? Math.max(0, init.importRetries) + : 0, + retryDelay: + typeof init.retryDelay === "number" ? Math.max(0, init.retryDelay) : 300, + onImportError: init.onImportError, +}); + +export function createLazy

(config: LazyConfigInit = {}) { + const { forceReload, importRetries, retryDelay, onImportError } = + createConfig(config); + const reloadStorage = new ReloadStorage(forceReload.storageKey); + + function enhancedLazy( + importFunction: () => Promise<{ default: ComponentType }> + ): LazyExoticComponent> { + const functionString = importFunction.toString(); + + const importWithRetry = async () => { + let retryCount = 0; + + const tryImport = async (): Promise<{ default: ComponentType }> => { + try { + return await importFunction(); + } catch (error) { + if (onImportError) { + onImportError(error); + } else { + console.error("React-lazy import error:", error); + } + + if (retryCount < importRetries) { + retryCount++; + + if (retryDelay > 0) { + await new Promise((resolve) => setTimeout(resolve, retryDelay)); + } + + return tryImport(); + } + + throw error; + } + }; + + return tryImport(); + }; + + return reactLazy(async () => { + try { + const component = await importWithRetry(); + if (forceReload.maxRetries > 0) { + reloadStorage.removeFunction(functionString); + } + return component; + } catch (error) { + if ( + forceReload.maxRetries > 0 && + (reloadStorage.getMap().get(functionString) ?? 0) < + forceReload.maxRetries + ) { + reloadStorage.addFunction(functionString); + window.location.reload(); + + const EmptyComponent: FC = () => null; + return { default: EmptyComponent }; + } + throw error; + } + }); + } + + return enhancedLazy; +} + +export const lazy = createLazy(); + +export function createRetryLazy(retries = 3, delay = 300) { + return createLazy({ + forceReload: false, + importRetries: retries, + retryDelay: delay, + }); +} diff --git a/packages/react-lazy/tsconfig.json b/packages/react-lazy/tsconfig.json new file mode 100644 index 000000000..7d6dc724b --- /dev/null +++ b/packages/react-lazy/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../tsconfig/library.json", + "include": ["src"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file