Modern.js
Modern.js is a progressive web development framework based on React. Internally at ByteDance, Modern.js supports the development of thousands of web applications.
The Module Federation team works closely with the Modern.js team and provides the @module-federation/modern-js
plugin to help users better utilize Module Federation within Modern.js.
Supports
- modern.js ^2.56.1
- Includes Server-Side Rendering (SSR)
We highly recommend referencing these applications, which showcases the best practices for integrating Modern.js with Module Federation:
Quick Start
Installation
You can install the plugin using the following commands:
npm add @module-federation/modern-js --save
Apply Plugin
Apply this plugin in the plugins
section of modern.config.ts
:
modern.config.ts
import { appTools, defineConfig } from '@modern-js/app-tools';
import { moduleFederationPlugin } from '@module-federation/modern-js';
export default defineConfig({
dev: {
port: 3005,
},
runtime: {
router: true,
},
// moduleFederationPlugin is a plugin for modern.js that can make certain modifications to the build/runtime
plugins: [appTools(), moduleFederationPlugin()],
});
Then, create the module-federation.config.ts
file and add the required configuration:
module-federation.config.ts
import { createModuleFederationConfig } from '@module-federation/modern-js';
export default createModuleFederationConfig({
name: 'host',
remotes: {
remote: 'remote@http://localhost:3006/mf-manifest.json',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
});
Server-Side Rendering (SSR)
Note
For a better performance experience, Module Federation X Modern.js SSR only supports stream SSR.
There is no difference in using Module Federation in SSR scenarios compared to CSR scenarios; developers can continue with their existing development practices.
Component-Level Data Fetch
Introduction
In SSR scenarios, useEffect
does not execute, which normally prevents fetching data before rendering a component.
To support this functionality, mainstream frameworks typically prefetch data based on React Router's data loader
and inject it into route components. Route components then access and render this data using useLoaderData.
This approach heavily relies on routing functionality and cannot be used directly with Module Federation.
To address this issue, Module Federation provides component-level data fetching capabilities, enabling developers to fetch and render data in SSR scenarios.
What does component-level mean?
Module Federation usage can be broadly categorized into two parts: components (functions) and applications. The difference lies in whether they include routing functionality.
How to Use
Different actions are required depending on the role.
Provider
Note
Producers can use Rslib to generate SSR components.
However, it should be noted that because the data in "Data Fetching" is injected by the consumer. Therefore, if "Data Fetching" is used in Rslib, the exported non-MF components cannot be isomorphic with MF components.
Each exposed module can have a corresponding .data
file with the same name. These files can export a loader function, which we call a Data Loader. This function executes before the corresponding expose
component renders, providing data to the component. Here's an example:
.
└── src
├── List.tsx
└── List.data.ts
The List.data.ts
file needs to export a function named fetchData
. This function will execute before the List
component renders and inject its data. Here's an example:
List.data.ts
import type { DataFetchParams } from '@module-federation/modern-js/runtime';
export type Data = {
data: string;
};
export const fetchData = async (params: DataFetchParams): Promise<Data> => {
console.log('params: ', params);
return new Promise((resolve) => {
setTimeout(() => {
resolve({
data: `data: ${new Date()}`,
});
}, 1000);
});
};
The data from the loader function is injected into the producer's props with the key mfData
. Therefore, the producer needs to modify its code to consume this data, as shown in the example below:
List.tsx
import React from 'react';
import type { Data } from './index.data';
const List = (props: {
mfData?: Data;
}): JSX.Element => {
return (
<div>
{props.mfData?.data && props.mfData?.data.map((item,index)=><p key={index}>{item}</p>)}
</div>
);
};
export default List;
Producer Consuming Its Own Data
If Modern.js is used to develop a producer, and this producer page is also accessed independently, then Modern.js's Data Loader can be used to inject data.
Its usage is basically the same as Module Federation, except for the function name. Therefore, you can easily consume Data Loader in the producer, as shown in the example below:
- Create a
page.data.ts
file in the producer page and export a function named loader
:
page.data.ts
import { fetchData } from '../components/List.data';
import type { Data } from '../components/List.data';
export const loader = fetchData
export type {Data}
- Consume this data in the producer page :
page.tsx
import { useLoaderData } from '@modern-js/runtime/router';
import List from '../components/List';
import './index.css';
import type { Data } from './page.data';
const Index = () => {
const data = useLoaderData() as Data;
console.log('page data', data);
return (
<div className="container-box">
<List mfData={data} />
</div>
)};
export default Index;
Consumer
In the consumer, we need to use createRemoteComponent
to load the remote component and fetch data.
import { kit, ERROR_TYPE } from '@module-federation/modern-js/runtime';
const { createRemoteComponent } = kit;
const List = createRemoteComponent({
loader: () => {
return import('remote/List');
},
loading: 'loading...',
export: 'default',
fallback: ({error,errorType,dataFetchMapKey}) => {
console.error(error)
if(errorType === ERROR_TYPE.LOAD_REMOTE){
return <div>load remote failed</div>
}
if(errorType === ERROR_TYPE.DATA_FETCH){
return <div>data fetch failed, the dataFetchMapKey key is: {dataFetchMapKey}</div>
}
return <div>error type is unknown</div>;
},
});
const Index = (): JSX.Element => {
return (
<div>
<h1>Basic usage with data fetch</h1>
<List />
</div>
);
};
export default Index;
Loader Function
Parameters
By default, parameters are passed to the loader function. The type is DataFetchParams
, which includes the following field:
isDowngrade
(boolean): Indicates whether the current execution context is in downgrade mode. For example, if Server-Side Rendering (SSR) fails and falls back to Client-Side Rendering (CSR), a request will be re-initiated to the server to call the loader function, and this value will be true
.
In addition to the default parameters, you can also pass the dataFetchParams field in createRemoteComponent
. This field will be passed through to the loader function.
Return Value
The return value of the loader function can only be a serializable data object.
Using Data Loader in Different Environments
The loader function may execute on the server or in the browser. A loader function executed on the server is called a Server Loader, and one executed in the browser is called a Client Loader.
In CSR applications, the loader function executes in the browser, meaning they are all Client Loaders by default.
In SSR applications, the loader function only executes on the server, meaning they are all Server Loaders by default. In SSR, Module Federation directly calls the corresponding loader function on the server. When switching routes in the browser, Module Federation sends an HTTP request to the SSR service, which also triggers the loader function on the server.
NOTE
Having the loader function for SSR applications execute only on the server offers the following benefits:
- Simplified usage: Ensures that data fetching in SSR applications is isomorphic, so developers don't need to differentiate loader function execution code based on the environment.
- Reduced browser bundle size: Moves logic code and its dependencies from the browser to the server.
- Improved maintainability: Moves logic code to the server, reducing the direct impact of data logic on the frontend UI. Additionally, it prevents accidental inclusion of server-side dependencies in the browser bundle or browser-side dependencies in the server bundle.
Using Client Loader in SSR Applications
By default, in SSR applications, the loader function only executes on the server. However, in some scenarios, developers might want requests sent from the browser to bypass the SSR service and directly request the data source, for example:
- To reduce network consumption in the browser by directly requesting the data source.
- If the application has data caching in the browser and doesn't want to request data from the SSR service.
Module Federation supports adding an additional .data.client
file in SSR applications, which also exports a named loader. In this case, if the Data Loader in the SSR application encounters an error and downgrades, or when switching routes in the browser, it will execute this loader function in the browser like a CSR application, instead of sending a data request to the SSR service.
List.data.client.ts
import cache from 'my-cache';
export async function loader({ params }) {
if (cache.has(params.id)) {
return cache.get(params.id);
}
const res = await fetch('URL_ADDRESS?id={params.id}');
return {
message: res.message,
}
}
WARNING
To use a Client Loader, there must be a corresponding Server Loader, and the Server Loader must be defined using the .data
file convention, not the .loader
file convention.
FAQ
Application-Level Data Fetching?
For application-level modules, we prefer to use RSC (React Server Components) to achieve more complete functionality. This feature is currently being explored, so please stay tuned.
Support nested remote?
Not supported yet.
API
In addition to exporting MF Runtime, @module-federation/modern-js/runtime
also provides a series of APIs to help developers better use Module Federation.
To prevent conflicts with Shared modules, you need to import them as follows:
import { kit } from '@module-federation/modern-js/runtime';
const { loadRemote ,createRemoteComponent, createRemoteSSRComponent, wrapNoSSR } = kit;
createRemoteComponent
Type declaration
declare function createRemoteComponent(
props: CreateRemoteComponentOptions
): (props: ComponentType) => React.JSX.Element;
type CreateRemoteComponentOptions<T, E extends keyof T> = {
loader: () => Promise<T>;
loading: React.ReactNode;
fallback: ReactNode | ((errorInfo: ErrorInfo) => ReactNode);
export?: E;
dataFetchParams?: DataFetchParams;
};
type ComponentType = T[E] extends (...args: any) => any
? Parameters<T[E]>[0] extends undefined
? Record<string, never>
: Parameters<T[E]>[0]
: Record<string, never>;
type DataFetchParams = {
isDowngrade: boolean;
} & Record<string, unknown>;
type ErrorInfo = {
error: Error;
errorType: number;
dataFetchMapKey?: string;
};
This function supports loading components and also provides the following capabilities:
- In SSR mode, it injects the corresponding producer's style tags/script resources. This behavior helps avoid CSS flickering issues caused by stream rendering and accelerates PID (First Paint Interactive Time).
- If the producer has a data fetching function, this function will be automatically called and the data injected.
Example
import React, { FC, memo, useEffect } from 'react';
import { kit, ERROR_TYPE } from '@module-federation/modern-js/runtime';
const { createRemoteComponent } = kit;
const RemoteComponent = createRemoteComponent({
loader: () => import('remote/Image'),
loading: <div>loading...</div>,
fallback: ({error,errorType,dataFetchMapKey}) => {
console.error(error)
if(errorType === ERROR_TYPE.LOAD_REMOTE){
return <div>load remote failed</div>
}
if(errorType === ERROR_TYPE.DATA_FETCH){
return <div>data fetch failed, the dataFetchMapKey key is: {dataFetchMapKey}</div>
}
return <div>error type is unknown</div>;
},
});
const App: FC = () => {
return <>
<RemoteComponent />
</>;
};
export default App;
loader
- Type:
() => Promise<T>
- Required: Yes
- Default value:
undefined
A function to load the remote component, usually ()=>loadRemote(id)
or ()=>import(id)
.
loading
- Type:
React.ReactNode
- Required: Yes
- Default value:
undefined
Sets the module loading state.
fallback
- Type:
(({ error }: { error: ErrorInfo}) => React.ReactElement)
- Required: Yes
- Default value:
undefined
The fallback component rendered when the component fails to load or render.
export
- Type:
string
- Required: No
- Default value:
'default'
If the remote component is a named export, you can use this parameter to specify the name of the component to export. By default, it loads the default export.
dataFetchParams
- Type:
DataFetchParams
- Required: No
- Default value:
undefined
If the remote component has a data fetching function, setting this will pass it to the data fetching function.
createRemoteSSRComponent
wrapNoSSR
Type declaration
declare function wrapNoSSR<T, E extends keyof T>(
createComponentFn: typeof createRemoteComponent<T, E>,
) : (options: CreateRemoteComponentOptions<T, E>) => (props: ComponentType) => React.JSX.Element
Wraps a component so that it does not render in SSR scenarios.
Usage example:
import { kit } from '@module-federation/modern-js/runtime';
const { createRemoteComponent, wrapNoSSR } = kit;
const RemoteComponent = wrapNoSSR(createRemoteComponent)({
loader: () => {
return import('remote/Content');
},
loading: 'loading...',
export: 'default',
fallback: ({ error }) => {
if (error instanceof Error && error.message.includes('not exist')) {
return <div>fallback - not existed id</div>;
}
return <div>fallback</div>;
},
});
const Index = (): JSX.Element => {
return (
<div>
<h1>
The component will be render in csr.
</h1>
<RemoteComponent />
</div>
);
};
export default Index;
Configuration
ssr
- Type:
false
- Required: No
- Default value:
undefined
@module-federation/modern-js
will automatically add SSR-related build presets based on server.ssr
in the modern.js config.
If the current project only needs to load MF in CSR, you can set ssr: false
to help with progressive migration.
import { appTools, defineConfig } from '@modern-js/app-tools';
import { moduleFederationPlugin } from '@module-federation/modern-js';
// https://modernjs.dev/en/configure/app/usage
export default defineConfig({
dev: {
port: 3050,
},
runtime: {
router: true,
},
server: {
ssr: {
mode: 'stream',
},
},
plugins: [
appTools(),
moduleFederationPlugin({ ssr: false })
],
});
fetchServerQuery
- Type:
Record<string, unknown>
- Required: No
- Default:
undefined
If a downgrade occurs, an HTTP request will be sent to the server. This configuration can be used to add query parameters to that request.