PluginRouteLoader is the host's plugin orchestrator: it loads the manifest, filters it, mounts each
plugin's init, gates render on a loader, evaluates onboarding steps and records lifecycle
transitions.
import { PluginRouteLoader } from "@z0devs/mf-shell-kit";Mount it as a route element inside your dynamic route tree — it renders an <Outlet/> (optionally
behind <OnboardingHost/>) once every plugin is ready.
Props#
| Prop | Required | Type | Notes |
|---|---|---|---|
shell | ✅ | DefinedSharedStore | The defineSharedStore result. Supplies the Provider and the useInitProps hook. |
auth | ✅ | AuthAdapter | { user, isLoading, login(returnTo?), logout? } — adapt Auth0 / Clerk / Cognito / Keycloak / custom. |
manifest | ✅ | ManifestLoader | () => Promise<MFEManifestEntry[]>. Cached through React Query. |
loader | ✅ | LoaderComponent | Your branded loading component — any ComponentType<{ size?: number }>. |
submodules | — | SubmoduleComponent[] | Shell-local init components rendered once during startup, alongside remote inits. |
onPluginError | — | (record: PluginErrorRecord) => void | Central error sink. Mounts a PluginErrorProvider for the subtree. |
config | — | PluginRouteLoaderConfig | See below. |
<PluginRouteLoader
shell={sharedStore}
auth={{ user, isLoading, login, logout }}
manifest={loadManifest}
loader={AppLoader}
submodules={[BpmInit, NotificationsInit]}
onPluginError={reportToTelemetry}
config={{ versionPolicy: "block" }}
/>config#
| Field | Type | Default | Notes |
|---|---|---|---|
manifestCacheKey | ReadonlyArray<unknown> | ["mfes-config-json"] | React Query cache key. Include the tenant id for per-tenant manifests. |
buildEnvConfigUrl | (entry) => string | null | ${origin}/env-config.js from service.remoteEntry | Return null to skip env-config for an entry. |
loaderSize | number | 300 | Forwarded to your loader as size. |
settleMs | number | 1000 | Debounce before declaring loading done after dynamic router changes settle. |
versionPolicy | "block" | "warn" | "off" | "block" | Behaviour when shellApiVersion doesn't satisfy shell.apiVersion. |
onboardingRegistryKey | string | null | "onboardingSteps" when present | Which shell.registries key holds onboarding steps. null disables the auto-mounted OnboardingHost. |
loadPolicy | RemoteLoadPolicy | 3 attempts / 500 ms·n / 60 s | Retry + timeout, forwarded to every InitRemote → BaseRemote. |
RemoteLoadPolicy#
type RemoteLoadPolicy = {
maxAttempts?: number; // >= 1; 1 disables retries. Default 3
backoffMs?: (attempt: number) => number; // default (n) => 500 * n
isTransient?: (error: Error) => boolean; // default: ScriptNetworkError, timeouts, RUNTIME-008, ChunkLoadError
scriptTimeoutMs?: number; // default 60_000
};<PluginRouteLoader
shell={sharedStore}
config={{
loadPolicy: {
maxAttempts: 5,
backoffMs: (n) => Math.min(2000, 250 * 2 ** n),
scriptTimeoutMs: 30_000,
},
}}
/>What it does, in order#
Fetch
Calls manifest(), cached under manifestCacheKey.
Validate
validateManifestEntry on every record; failures are skipped and reported as manifest-invalid.
Filter
Drops enabled: false (→ disabled), then applies versionPolicy (→ version-blocked), then
resolves inter-plugin dependencies (→ dependency-unmet).
Mount
One <InitRemote> per surviving entry, inside an InitPropsProvider wired to
shell.useInitProps — integrity check, registerRemotes, loadRemote("${service.id}/init"), with
loadPolicy retries. service.type is threaded through so ESM containers load correctly.
Gate
loader stays mounted until every plugin reports ready (plus settleMs), so the nav never renders
half-built.
Render
<Outlet/> — wrapped in <OnboardingHost/> when an onboarding registry is present.
Adapter types#
type AuthAdapter<TUser = unknown> = {
user: TUser | null;
isLoading: boolean;
login: (returnTo?: string) => void;
logout?: () => void;
};
type ManifestLoader = () => Promise<MFEManifestEntry[]>;
type LoaderComponent = ComponentType<{ size?: number }>;
type SubmoduleComponent = ComponentType<Record<string, unknown>>;AuthAdapter is intentionally minimal — it's the shape the loader needs to decide "wait, redirect to
login, or proceed", not a general auth abstraction. Anything richer belongs in the store's values.
What it mounts for you#
shell.Provider— the shared-store contextInitPropsProvider— wired toshell.useInitProps(remoteName)PluginErrorProvider— whenonPluginErroris setOnboardingHost— when an onboarding registry key resolves
Which means you do not mount those yourself. You do still mount RouterContextProvider,
MFEnvProvider and your query client at the app root — or let createShellApp do it.
createShellApp#
import { createShellApp } from "@z0devs/mf-shell-kit";
const App = createShellApp({
initialRoutes, // with <PluginRouteLoader/> wired inside
defaultErrorElement: <ErrorPage />,
mfEnv: { mfeEnvNamespace: "__MFE_ENV__", shellConfigLoader: () => getShellConfig() },
AppShell: ({ children }) => <ThemeProvider>{children}</ThemeProvider>,
});Mounts RouterContextProvider → MFEnvProvider → Router in one call. You still decide where
PluginRouteLoader sits in the tree, because that position determines which routes are gated by it.
Related host exports#
| Export | Purpose |
|---|---|
defineSharedStore | Declare the host↔plugin contract |
OnboardingHost | Evaluate onboarding steps (auto-mounted, or mount manually for custom UI) |
PluginDebugPanel | Table of every plugin's status, timing and last error |
useLifecycle(events, key, handler) | Subscribe to a store event with cleanup |
defineSubmodule | Type helper for shell-local init components |
SharedInitProps, PluginInitProps, DefinedSharedStore, StoreFeatures | Contract types |