React Bridge

React Bridge

@module-federation/bridge-react provides bridge utility functions for React applications:

  • createBridgeComponent: Used for exporting application-level modules, suitable for producers to wrap modules exported as application types.
  • createRemoteComponent: Used for loading application-level modules, suitable for consumers to load modules as application types.

View Demo

Installation

npm
yarn
pnpm
npm install @module-federation/bridge-react@latest

Examples

Exporting Application Type Modules

DANGER

Note: After using @module-federation/bridge-react, you cannot set react-router-dom as shared, otherwise the build tool will throw an exception. This is because @module-federation/bridge-react implements route control by proxying react-router-dom to ensure that inter-application routing works correctly.

In the producer project, assuming we need to export the application as an application type module using @module-federation/bridge-react, with App.tsx as the application entry point.

  • Step 1: First, create a new file export-app.tsx, which will be the file exported as an application type module. We need to use createBridgeComponent to wrap the root component of the application.
// ./src/export-app.tsx
import App from './src/App.tsx';
import { createBridgeComponent } from '@module-federation/bridge-react';

export default createBridgeComponent({
  rootComponent: App
});
  • Step 2: In the rsbuild.config.ts configuration file, we need to export export-app.tsx as an application type module
// rsbuild.config.ts
export default defineConfig({
  tools: {
    rspack: (config, { appendPlugins }) => {
      appendPlugins([
        new ModuleFederationPlugin({
          name: 'remote1',
          exposes: {
            './export-app': './src/export-app.tsx',
          },
          shared: ['react', 'react-dom'],
        }),
      ]);
    },
  },
});

At this point, we have completed the export of the application type module.

INFO

Why do application type modules need to be wrapped with createBridgeComponent? There are three main reasons:

  1. Support for cross-framework rendering. Components wrapped with createBridgeComponent will conform to the loading protocol of the application type consumer, making cross-framework rendering possible.
  2. Automatic injection of basename. Components wrapped with createBridgeComponent will automatically inject basename, ensuring that the producer application works correctly under the consumer project.
  3. Wrapping ErrorBoundary. Components wrapped with createBridgeComponent will be wrapped with ErrorBoundary to ensure that fallback logic is automatically entered when remote loading fails or rendering errors occur.

Loading Application Type Modules

Host

  • Step 1: In the rsbuild.config.ts configuration, we need to register remote modules, which is no different from other Module Federation configurations.
// rsbuild.config.ts
export default defineConfig({
  tools: {
    rspack: (config, { appendPlugins }) => {
      config.output!.uniqueName = 'host';
      appendPlugins([
        new ModuleFederationPlugin({
          name: 'host',
          remotes: {
            remote1: 'remote1@http://localhost:2001/mf-manifest.json',
          },
        }),
      ]);
    },
  },
});
  • Step 2: In the consumer project, we need to load the application type module. We use createRemoteComponent to load the application type module
// ./src/App.tsx
import React from 'react';
import { createRemoteComponent } from '@module-federation/bridge-react';
import { loadRemote } from '@module-federation/enhanced/runtime'
import styles from './index.module.less';

// define FallbackErrorComp Component
const FallbackErrorComp = (info: any) => {
  return (
    <div>
      <h2>This is ErrorBoundary Component</h2>
      <p>Something went wrong:</p>
      <pre style={{ color: 'red' }}>{info?.error.message}</pre>
      <button onClick={() => info.resetErrorBoundary()}>
        resetErrorBoundary(try again)
      </button>
    </div>
  );
};

// define FallbackLoading Component
const FallbackComp = <div data-test-id="loading">loading...</div>;

// use createRemoteComponent to export remote component
const Remote1App = createRemoteComponent({
  // loader is for loading remote module, for example: loadRemote('remote1/export-app')、import('remote1/export-app')
  loader: () => loadRemote('remote1/export-app'),
  // fallback 用于在加载远程模块失败时展示的组件
  // fallback is for error handling
  fallback: FallbackErrorComp,
  // loading is for loading state
  loading: FallbackComp,
});

const App = () => {
  return (<BrowserRouter basename="/">
      <Routes>
        <Route path="/" Component={Home} />
         <Route
          path="/remote1/*"
          // use Remote1App component, will be lazy loaded
          Component={() => (
            <Remote1App
              // can set className and style, will be injected to component
              className={styles.remote1}
              style={{ color: 'red' }}
              // name and age are remote component props, will be passed to remote component
              name={'Ming'}
              age={12}
              // can set ref, will be forwarded to remote component, can get ref object to operate dom
              ref={ref}
            />
          )}
        />
      </Routes>
    </BrowserRouter>)
};

At this point, we have completed loading the application type module.

:::info

  1. The remote module exported by createRemoteComponent will automatically use the react-bridge loading protocol to load the module, making cross-framework rendering of applications possible.

  2. Additionally, createRemoteComponent will automatically handle module loading, module destruction, error handling, loading, routing, and other logic, allowing developers to focus solely on how to use the remote component.

  3. For remote modules exported through createRemoteComponent, you can use them like regular React components: passing className, style, props, ref, and other attributes will be automatically passed through to the remote component, making the user experience almost equivalent to using local components.

Methods

createBridgeComponent

export declare function createBridgeComponent<T>(bridgeInfo: ProviderFnParams<T>): () => {
    render(info: RenderFnParams): Promise<void>;
    destroy(info: {
        dom: HTMLElement;
    }): Promise<void>;
};

type ProviderFnParams<T> = {
  rootComponent: React.ComponentType<T>;
  render?: (
    App: React.ReactElement,
    id?: HTMLElement | string,
  ) => RootType | Promise<RootType>;
};

export declare interface RenderFnParams extends ProviderParams {
    dom: HTMLElement;
}

export declare interface ProviderParams {
    moduleName?: string;
    basename?: string;
    memoryRoute?: {
        entryPath: string;
    };
    style?: React.CSSProperties;
    className?: string;
}
  • bridgeInfo
    • type:
type ProviderFnParams<T> = {
  rootComponent: React.ComponentType<T>;
  render?: (
    App: React.ReactElement,
    id?: HTMLElement | string,
  ) => RootType | Promise<RootType>;
};
  • Purpose: Used to pass the root component
  • ReturnType
    • type:

      () => {
        render(info: {
          moduleName?: string;
          basename?: string;
          memoryRoute?: {
            entryPath: string;
          };
          style?: React.CSSProperties;
          className?: string;
          dom?: HTMLElement;
      }): Promise<void>;
        destroy(info: { dom: HTMLElement}): Promise<void>;
      }

createRemoteComponent

import { createRemoteComponent } from '@module-federation/bridge-react';
import type { ProviderParams } from '@module-federation/bridge-react';

function createRemoteComponent<T, `E extends keyof T`>(
  options: {
    // loader is for loading remote module, for example: loadRemote('remote1/export-app')、import('remote1/export-app')
    loader: () => Promise<T>,
    // default is default, used to specify the export of the module
    export?: E;
    // loading is for loading state
    loading: React.ReactNode;
    // fallback is for error handling
    fallback: ComponentType<{ error: any; }>;
  }
): (props: {
    basename?: ProviderParams['basename'];
    memoryRoute?: { entryPath: string };
} & RawComponentType) => React.JSX.Element;
  • options
    • loader
      • type: () => Promise<Module>
      • Purpose: Used to load remote modules, for example: loadRemote('remote1/export-app'), import('remote1/export-app')
const Remote1App = createRemoteComponent({
  // loader is for loading remote module, for example: loadRemote('remote1/export-app')、import('remote1/export-app')
  loader: () => loadRemote('remote1/export-app'),
  // fallback is for error handling
  fallback: FallbackErrorComp,
  // loading is for loading state
  loading: FallbackComp,
});

const Remote2App = createRemoteComponent({
  // loader is for loading remote module, for example: loadRemote('remote2/export-app')、import('remote2/export-app')
  loader: () => import('remote2/export-app'),
  // fallback is for error handling
  fallback: FallbackErrorComp,
  // loading is for loading state
  loading: FallbackComp,
});
  • export
    • type: string
    • Purpose: Can specify the export of the module
// remote
export const provider = createBridgeComponent({
  rootComponent: App
});

// host
const Remote1App = createRemoteComponent({
  loader: () => loadRemote('remote1/export-app'),
  export: 'provider'
});
  • loading

    • type: React.ReactNode
    • Purpose: Component displayed when loading remote modules
  • fallback

    • type: ComponentType<{ error: any; }>
    • Purpose: Component displayed when loading, rendering remote modules
  • ReturnType

    • type: (props: PropsInfo)=> React.JSX.Element
    • Purpose: Used to render remote module components
const Remote1App = createRemoteComponent({
  // loader is for loading remote module, for example: loadRemote('remote1/export-app')、import('remote1/export-app')
  loader: () => loadRemote('remote1/export-app'),
  // fallback is for error handling
  fallback: FallbackErrorComp,
  // loading is for loading state
  loading: FallbackComp,
});


function App() {
  return (<BrowserRouter basename="/">
    <Routes>
     <Route
          path="/remote1/*"
          Component={() => (
            <Remote1App
              className={styles.remote1}
              props1={'props_value'}
              props2={'another_props_value'}
              ref={ref}
              {/* Use memoryRoute to control the child application's routing as memoryRouter, which will not directly display the URL in the browser address */}
              memoryRoute={{ entryPath: '/detail' }}
            />
          )}
        />
    </Routes>
  </BrowserRouter>)
}