|
// This PortalLoader is a bundle |
|
import React, { lazy, Suspense, useEffect, useState } from "react"; |
|
import { createPortal } from "react-dom"; |
|
import { createRoot } from "react-dom/client"; |
|
|
|
// Include these in the bundle since they are on every page |
|
import NewsLetterSubscribe from "./portals/NewsLetterSubscribe"; |
|
import UserMenu from "./portals/UserMenu"; |
|
|
|
import ErrorBoundaryComponent from "./utils/ErrorBoundaryComponent"; |
|
|
|
const portals = { |
|
NewsLetterSubscribe, // not lazy |
|
UserMenu, // not lazy |
|
|
|
NavBarSearch: lazy(() => import("./portals/NavBarSearch")), |
|
AdminReact: lazy(() => import("./portals/AdminReact")), |
|
AffiliatesPage: lazy(() => import("./portals/AffiliatesPage")), |
|
// etc. no limit |
|
}; |
|
|
|
type Portal = { |
|
name: string; |
|
component: React.LazyExoticComponent<React.ComponentType<any>>; |
|
container: Element; |
|
props: Record<string, string | number | null>; |
|
}; |
|
type Portals = Record<string, Portal>; |
|
|
|
/** |
|
* PortalLoader is a component that will lazy load components and render them as a portal |
|
* for each element with the attribute data-react-portal |
|
* |
|
* It does this when this is first mounted on the page. |
|
*/ |
|
function PortalLoader() { |
|
// on mount detect any data-react-portal, |
|
// lazy load those components |
|
// and render them in portals |
|
const [components, setComponents] = useState<Portals>({}); |
|
|
|
useEffect(() => { |
|
document.querySelectorAll("[data-react-portal]").forEach((el) => { |
|
const name = el.getAttribute("data-react-portal"); |
|
const props = el.getAttribute("data-props"); |
|
const page = portals[name]; |
|
if (!page) { |
|
if (process.env.NODE_ENV === "development") { |
|
throw Error(`portal not registered: ${name}`); |
|
} else { |
|
console.error("portal not registered", name, portals); |
|
return; |
|
} |
|
} |
|
setComponents((prev) => ({ |
|
...prev, |
|
[name]: { |
|
name, |
|
component: page, |
|
container: el, |
|
props: props ? JSON.parse(props) : {}, |
|
}, |
|
})); |
|
}); |
|
}, []); |
|
|
|
return ( |
|
<div id="portal-loader"> |
|
{Object.values(components).map((portal) => |
|
createPortal( |
|
<ErrorBoundaryComponent silent={true}> |
|
<Suspense fallback={null} key={portal.name}> |
|
<portal.component {...portal.props} /> |
|
</Suspense> |
|
</ErrorBoundaryComponent>, |
|
portal.container, |
|
), |
|
)} |
|
</div> |
|
); |
|
} |
|
|
|
const container = document.getElementById("portal-loader"); |
|
// Probably due to the hot loader this is called multiple times |
|
const didMount = !!container.getAttribute("data-did-mount"); |
|
|
|
if (!didMount) { |
|
const root = createRoot(container); |
|
root.render(<PortalLoader />); |
|
container.setAttribute("data-did-mount", "true"); |
|
} |