Vinext

Vinext runs Next.js apps on Vite, so a Vinext app uses @module-federation/vite instead of @module-federation/nextjs-mf. This works with the App Router: a Vinext host renders remote components on the server and hydrates them in the browser.

Info

Server rendering of remotes comes from @module-federation/vite, not from Vinext. Vinext's own Module Federation bridge is browser-only and only supplies the React hooks that remote client components use, as described in Write remote components.

Supports

  • Vinext hosts using the Next.js App Router with server-side rendering
  • Remotes built with Vite that share React with the host
  • Isolated React islands for remotes that bring their own React version

Installation

npm
yarn
pnpm
bun
npm add @module-federation/vite

Configure the remote

The remote is a Vite React app. Build both a client and an SSR environment so the host can render exposed components on the server:

remote/vite.config.ts
import { federation } from '@module-federation/vite';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';

export default defineConfig({
  builder: {},
  plugins: [
    federation({
      name: 'remote',
      filename: 'remoteEntry.js',
      exposes: {
        './Widget': './src/widget.tsx',
      },
      shared: {
        react: { singleton: true },
        'react/': { singleton: true },
        'react-dom': { singleton: true },
        'react-dom/': { singleton: true },
      },
    }),
    react(),
  ],
  environments: {
    client: {
      build: { outDir: 'dist/client' },
    },
    ssr: {
      build: {
        outDir: 'dist/server',
        ssr: './src/widget.tsx',
      },
    },
  },
  build: {
    target: 'chrome89',
  },
  server: {
    cors: true,
    origin: 'http://localhost:4174',
  },
  preview: {
    cors: true,
    port: 4174,
  },
});

The react/ and react-dom/ keys share subpath imports such as react/jsx-runtime and react-dom/client between the host and the remote.

Write remote components

Sharing the React packages is not enough for hooks. A remote client component must read its hooks through getVinextReact() from vinext/client, so they come from the host's React instance. Add vinext to the remote's dependencies, then:

remote/src/widget.tsx
'use client';

import * as React from 'react';
import { getVinextReact } from 'vinext/client';

const { useState } = getVinextReact(React);

export default function Widget() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount((value) => value + 1)}>{count}</button>;
}

Without it, a component that imports useState or useContext directly can run against a different React instance and fail with invalid hook calls. getVinextReact() does not rewrite React imports inside third-party packages the remote bundles.

Configure the Vinext host

Add federation() before vinext():

host/vite.config.ts
import { federation } from '@module-federation/vite';
import vinext from 'vinext';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [
    federation({
      name: 'host',
      hostInitInjectLocation: 'entry',
      remotes: {
        remote: {
          type: 'module',
          name: 'remote',
          entry: 'http://localhost:4174/remoteEntry.js',
          entryGlobalName: 'remote',
          shareScope: 'default',
        },
      },
      shared: {
        react: { singleton: true },
        'react/': { singleton: true },
        'react-dom': { singleton: true },
        'react-dom/': { singleton: true },
      },
    }),
    vinext(),
  ],
});

hostInitInjectLocation: 'entry' injects the host initialization into the entry script, since an App Router app has no index.html for the plugin to update.

Load remote components

Load remotes from a client component with React.lazy and Suspense. Use React.lazy rather than next/dynamic, which avoids hydration errors with federated components:

host/app/remote-widget.tsx
'use client';

import { lazy, Suspense } from 'react';

const LazyRemoteWidget = lazy(() => import('remote/Widget'));

export function RemoteWidget() {
  return (
    <Suspense fallback={<div>Loading remote widget...</div>}>
      <LazyRemoteWidget />
    </Suspense>
  );
}

Then render it from any page or layout:

host/app/page.tsx
import { RemoteWidget } from './remote-widget';

export default function Home() {
  return <RemoteWidget />;
}

Isolated React islands

A remote that cannot share React with the host, for example a React 18 remote inside a React 19 host, can opt into island mode. Configure its client and SSR builds like the remote above, leave react out of shared, and enable experiments.ssrMode:

island/vite.config.ts
federation({
  name: 'island',
  filename: 'remoteEntry.js',
  exposes: {
    './Counter': './src/counter.tsx',
  },
  experiments: { ssrMode: 'ISLAND' },
  shared: {},
});

The host imports the expose with the ?mf-island query to use the plugin's React adapter. It renders the island HTML on the server and hydrates it with the remote's own React, outside the host React tree:

host/app/page.tsx
import RemoteIsland from 'island/Counter?mf-island';

export default function Home() {
  return <RemoteIsland />;
}

Importing island/Counter without the query still returns the original component.

Examples