Data Fetching

Remote modules typically want to handle data fetching on their own: the consumer is only responsible for loading and rendering, without having to know what data the remote depends on, and without each consumer reimplementing the same fetching logic.

In CSR scenarios, a remote can simply call useEffect inside the component to fire the request. In SSR scenarios, however, useEffect does not run on the server, so the remote cannot fetch data on the server before rendering.

Mainstream SSR frameworks (Next.js, Remix, Modern.js, etc.) usually provide route-level data prefetching: when a route matches, a data-loading function runs first, and its data is injected into the matching route component. But this mechanism depends on each framework's own router, and modules exposed by Module Federation do not live in the host's route tree, so this cannot be reused directly.

To solve this, Module Federation provides component-level data fetching capabilities, so developers can fetch data and render components in SSR scenarios.

What is component-level?

Modules exposed via Module Federation fall into two broad categories:

  • Components: standalone UI components, utility functions, etc., without their own router.
  • Applications: sub-applications with a complete router, typically exposed and consumed via Bridge.

The capability described in this document targets the former — "component-level" data fetching. See the demo for a detailed walkthrough.

How to Use

Data fetching supports both SSR and CSR scenarios. The sections below cover producer and consumer separately:

Producer

Tip

Producers can use Rslib and Modern.js.

Data fetching file convention

Data fetching uses a file convention:

  • Each exposed module can be paired with a .data file of the same name.
  • A function exported from a .data file is called a Data Loader, and the build plugin recognizes this convention.
  • When an exposed component is loaded, the runtime first calls the Data Loader to retrieve the data, injects it into the component, and then renders.

Example file layout:

.
└── src
    ├── List.tsx
    └── List.data.ts

The convention file must export a function named fetchData, which runs before the remote component renders:

List.data.ts
import type { DataFetchParams } from '@module-federation/bridge-react/data-fetch';
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 function is called before the component renders, and the runtime injects the returned data object into the component's props. The default key is mfData:

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;

Default loader function parameters

By default, parameters are passed to the loader function. The type is DataFetchParams, which includes the following field:

  • isDowngrade: Indicates whether the current execution context is in a fallback mode. For example, if Server-Side Rendering fails, a new request is sent from the browser to the server to call the loader function, in which case the value is true.

Using Data Loader in Different Environments

The loader function can be executed 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 is executed in the browser, so they are all Client Loaders by default.

In SSR applications, the loader function is only executed on the server, so 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.

Benefits of running the SSR loader only on the server
  • Simplified Usage: It ensures that the data fetching method in SSR applications is isomorphic, so developers don't need to differentiate code execution based on the environment.
  • Reduced Browser Bundle Size: Logic code and its dependencies are moved from the browser to the server.
  • Improved Maintainability: Moving logic code to the server reduces the direct impact of data logic on the front-end UI. It also prevents accidentally including server-side dependencies in the browser bundle or vice versa.

Using Client Loader in SSR Applications

By default, in SSR applications, the loader function is only executed on the server. However, in some scenarios, developers may want requests from the browser to go directly to the data source without passing through the SSR service. For example:

  1. To reduce network consumption in the browser by directly requesting the data source.
  2. The application has a data cache in the browser and does not want to request data from the SSR service.

Module Federation supports adding an additional .data.client file in SSR applications, which exports the named function in the same way. In this case, if the Data Loader on the server fails and falls back, or when switching routes in the browser, the application will execute this function in the browser like a CSR application, instead of sending another 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 in a .data file.

Producer Consuming Its Own Application Data

A producer may also be visited as a standalone page. In that case, it needs to be able to fetch data in both scenarios:

  • When loaded by a consumer, data is injected via the Data Loader.
  • When visited directly, data is fetched by the producer's own framework mechanism.

For example, if the producer uses Modern.js, it needs to support both being loaded by a consumer and being visited directly as a producer page. This can be done as follows:

  • Create a page.data.ts file in the producer's page directory and export a function named loader:
Info

Modern.js convention is to export a loader function from page.data.ts to fetch data.

page.data.ts
// Reuse fetchData from List.data.ts as the loader implementation
import { fetchData } from '../components/List.data';
import type { Data } from '../components/List.data';

export const loader = fetchData

export type {Data}
  • Consume this data on the producer's 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

Note

Currently, only Modern.js supports data fetching in SSR environments.

In the consumer, we need to use the createLazyComponent API to load the remote component and fetch its data.

import { getInstance } from '@module-federation/enhanced/runtime';
import { ERROR_TYPE } from '@module-federation/bridge-react/data-fetch';
import { lazyLoadComponentPlugin } from '@module-federation/bridge-react/data-fetch';

const instance = getInstance();
instance.registerPlugins([lazyLoadComponentPlugin()]);

const List = instance.createLazyComponent({
  loader: () => 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: {dataFetchMapKey}</div>;
    }
    return <div>error type is unknown</div>;
  },
});

export default function Index() {
  return <List />;
}

Passing custom parameters

In addition to the data structure the runtime injects by default, the consumer can pass custom parameters through to the loader function via the dataFetchParams field on createLazyComponent — for example, the host-side context such as user identifier, language, or tenant.

This field is merged with the default parameters and passed to the loader.

FAQ

Application-Level Data Fetching?

For application-level modules, we prefer to use RSC (React Server Components) to make the functionality more complete. This feature is currently under exploration, so please stay tuned.

Is Nested Producer Supported?

No, it is not supported.

Are there other plugins for producers besides the Rslib plugin and the Modern.js plugin?

Currently, only the Rslib and Modern.js plugins can create a Data Loader.