Skip to content

Commit

Permalink
feat: Adding loading for internal and federated apps
Browse files Browse the repository at this point in the history
This adds code to load internal (‘direct’) and federated (module federation) apps based on code in the site config file.  It reads the file and translates that into component loading in the shell.

This needs to be married to a routing strategy that only loads the components when their route has been visited - at the moment, I just want to test that this works at all and so am loading everything into the ‘homepage’, so to speak.
  • Loading branch information
davidjoy committed Aug 21, 2024
1 parent 2f9b284 commit 999dd6a
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 4 deletions.
60 changes: 56 additions & 4 deletions shell/bootstrap.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,76 @@
import { createElement } from 'react';
import { init } from '@module-federation/runtime';
import ReactDOM from 'react-dom';

import {
APP_INIT_ERROR, APP_READY,
AppProvider,
getConfig,
initialize,
subscribe,
subscribe
} from '../runtime';

import { ExternalAppConfig, FederatedAppConfig, InternalAppConfig } from '../types';
import Footer from './footer';
import Header from './header';
import useFederatedComponent from './utils';
import { Suspense } from 'react';

const messages = [];

function getFederatedApps() {
const { apps } = getConfig();

return apps.filter((app: InternalAppConfig | ExternalAppConfig | FederatedAppConfig) => 'remoteUrl' in app && 'moduleId' in app);
}

function getFederationRemotes(apps) {
return apps.map(app => ({
name: app.moduleId,
entry: app.remoteUrl
}));
}

function getInternalApps(): Array<InternalAppConfig> {
const { apps } = getConfig();

return apps.filter((app: InternalAppConfig | ExternalAppConfig | FederatedAppConfig) => 'component' in app);
}

subscribe(APP_READY, () => {
const federatedApps = getFederatedApps();
const remotes = getFederationRemotes(federatedApps);

init({
name: 'shell',
remotes,
});

const internalApps = getInternalApps();

ReactDOM.render(
<AppProvider>
<AppProvider wrapWithRouter>
<Header />
{getConfig().app ? createElement(getConfig().app, {}) : null}
<div>Okay what?</div>
{internalApps.map((internalApp: InternalAppConfig) => {
const AppComponent = internalApp.component;
return (
<AppComponent key={internalApp.appId} />
);
})}
{federatedApps.map((federatedApp: FederatedAppConfig) => {
const { Component: FederatedComponent, errorLoading } = useFederatedComponent(
federatedApp.remoteUrl,
federatedApp.appId,
federatedApp.moduleId
);
return (
<Suspense fallback="Loading...">
{errorLoading
? `Error loading module "${module}"`
: FederatedComponent && <FederatedComponent />}
</Suspense>
);
})}
<Footer />
</AppProvider>,
document.getElementById('root'),
Expand Down
80 changes: 80 additions & 0 deletions shell/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { loadRemote } from '@module-federation/runtime';
import {
ComponentType, lazy, LazyExoticComponent, useEffect, useState
} from 'react';

function loadComponent(scope, module) {
return async (): Promise<{ default: ComponentType<any> }> => {
// Initializes the share scope. This fills it with known provided modules from this build and all remotes
const Module = await loadRemote<{ default: ComponentType<any> }>(`${scope}/${module.slice(2)}`);
if (Module === null) {
throw new Error('Unable to load module.');
}
return Module;
};
}

const useDynamicScript = url => {
const [ready, setReady] = useState(false);
const [errorLoading, setErrorLoading] = useState(false);

useEffect(() => {
if (!url) {
return () => {};
}

setReady(false);
setErrorLoading(false);

const element = document.createElement('script');

element.src = url;
element.type = 'text/javascript';
element.async = true;

element.onload = () => {
setReady(true);
};

element.onerror = () => {
setReady(false);
setErrorLoading(true);
};

document.head.appendChild(element);

return () => {
document.head.removeChild(element);
};
}, [url]);

return {
errorLoading,
ready,
};
};

const useFederatedComponent = (remoteUrl, scope, module) => {
// const key = `${remoteUrl}-${scope}-${module}`;
const [Component, setComponent] = useState<LazyExoticComponent<ComponentType<any>> | null>(null);

const { ready, errorLoading } = useDynamicScript(remoteUrl);
useEffect(() => {
// if (Component) {
setComponent(null);
// }
// Only recalculate when key changes
}, [remoteUrl, scope, module]);

useEffect(() => {
if (ready && !Component) {
const loadedComponent = lazy(loadComponent(scope, module));
setComponent(loadedComponent);
}
// key includes all dependencies (scope/module)
}, [Component, ready, remoteUrl, scope, module]);

return { errorLoading, Component };
};

export default useFederatedComponent;

0 comments on commit 999dd6a

Please sign in to comment.