@module-federation/tanstack
v0.1.1
Published
TanStack Start integration for Module Federation
Readme
@module-federation/tanstack
TanStack Start integration for Module Federation on Vite and Rsbuild. The adapter packages are optional peers, so importing one adapter does not resolve the other.
Support status
| Host | Remote | Browser federation | Federated SSR |
| ------- | ------- | ------------------ | ---------------------------------- |
| Vite | Vite | Supported | Supported |
| Vite | Rsbuild | Supported | Supported (remote: server: true) |
| Rsbuild | Vite | Supported | Supported (host: server: true) |
| Rsbuild | Rsbuild | Supported | Supported (both: server: true) |
"Federated SSR" means the remote component is rendered into the host's initial HTML. Every row is covered by browser tests against both development servers and production builds: the remote hydrates, shares the host's React and context, responds to clicks, logs no errors, and the host falls back and recovers when the remote goes offline.
Vite
import { tanstackStartModuleFederation } from "@module-federation/tanstack/vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
tanstackStartModuleFederation({
name: "host",
remotes: {
remote: {
type: "module",
name: "remote",
entry: "https://example.test/remoteEntry.js",
},
},
}),
tanstackStart(),
react(),
],
});Place the federation wrapper before tanstackStart(), followed by the React
plugin. The root import (@module-federation/tanstack) is the same Vite
adapter.
Defaults:
filename: "remoteEntry.js",manifest: true, andhostInitInjectLocation: "entry".react,react-dom,@tanstack/react-router, and@tanstack/router-coreare shared singletons. When the config declaresremotes, they are alsoeager, so the host's own React and Router runtimes stay in the share scope and a remote built by another bundler cannot replace them. Remote-only builds stay lazy.- Any entry you pass in
sharedreplaces the default for that package.
A Vite host renders before lazy shared modules resolve, so mark any other shared
package the host imports from its entry or root route as eager: true, as the
wrapper does for React. Remotes can keep their shares lazy.
For a Vite host, configure SSR entry revalidation directly with the Vite option:
tanstackStartModuleFederation({
name: "host",
ssrEntryLoader: { maxAgeMs: 30_000 },
// ...remotes and shared options
});Vite's maxAgeMs revalidation is version-aware when the remote is resolved
from an mf-manifest.json. Convention-resolved or direct server-entry URLs do
not provide a remote version; use a manifest URL or call the Vite loader's
explicit revalidate() when those URLs must be refreshed.
The wrapper omits the global Module Federation target option. TanStack
Start's client and SSR environments need @module-federation/vite to select
web and node independently.
The Vite adapter requires Vite 8, like TanStack Start's own examples and
@vitejs/plugin-react 6. It stops with an error on older versions, where
@module-federation/vite cannot load remotes on the server.
A Vite host renders Rsbuild remotes on the server when they are built with
server: true (see below). Declare such a remote by its manifest, so the
server can find the Node container:
remotes: {
rsbuild_remote: {
type: "global",
name: "rsbuild_remote",
entry: "https://example.test/mf-manifest.json",
},
},Rsbuild
import { defineConfig } from "@rsbuild/core";
import { pluginReact } from "@rsbuild/plugin-react";
import { tanstackStart } from "@tanstack/react-start/plugin/rsbuild";
import { tanstackStartModuleFederation } from "@module-federation/tanstack/rsbuild";
export default defineConfig({
plugins: [
pluginReact(),
tanstackStart(),
...tanstackStartModuleFederation({
federation: {
name: "remote",
exposes: { "./StatusCard": "./src/StatusCard.tsx" },
},
}),
],
});The adapter returns an array of Rsbuild plugins; spread it into plugins, after
tanstackStart(). It emits script-compatible browser output, so the manifest
can be consumed by Vite and Rsbuild hosts.
Defaults:
react,react-dom,@tanstack/react-router, and@tanstack/router-coreare shared singletons,eagerfor hosts (configs withremotes) and lazy for remote-only builds.experiments.asyncStartup: true, so entries wait for shared modules before running. Passexperiments: { asyncStartup: false }to opt out.- A remote's browser build uses
publicPath: "auto", so hosts on other origins load its chunks from the remote. TanStack Start otherwise derives/fromserver.base. An absolute asset prefix, such as a CDN URL, is kept.
For hosts, set shareStrategy: "loaded-first". With the default
version-first strategy, the host fetches every remote's manifest during
startup, and a single offline remote fails the whole page.
SSR federation
Pass server: true (or an options object) to federate on the server:
- Remotes (
exposes) build a Node container,remoteEntry.ssr.cjs, next toremoteEntry.jsindist/client(its chunks go indist/client/ssr/), and advertise it inmf-manifest.jsonasssrRemoteEntry. Deploydist/clientas usual; the container holds only the exposed modules, never the app's server code. The remote's own server output is unchanged. Vite and Rsbuild hosts can both render it. - Hosts (
remotes) load remotes on the server through the Module Federation Node runtime. TanStack Start's server output becomes async-node CommonJS (dist/server/index.cjs), withdist/server/index.jsre-exporting it, sorsbuild previewand deployments that importindex.jskeep working. The dev server writes its server bundle tonode_modules/.cache/tanstack-start-federation/instead, so a production build indist/serversurvives development sessions. Rsbuild remotes needserver: truetoo. Vite remotes need nothing extra on their side, but the host must have@module-federation/vite(and itsvitepeer) installed: their server entries are ES modules, loaded by that package's SSR loader.
When a remote's server entry cannot load, the host's runtime plugin throws a
RemoteEntryError whose phase is fetch, evaluate, or loader and whose
cause is the original error.
Server entries are cached for the lifetime of the Node process by default. To pick up a rebuilt remote while keeping the process running, configure the revalidation window on an Rsbuild host:
tanstackStartModuleFederation({
federation: {
name: "host",
remotes: {
remote: "remote@https://example.test/mf-manifest.json",
},
},
server: {
ssrEntryLoader: {
maxAgeMs: 30_000,
},
},
});For an Rsbuild host, when maxAgeMs expires, the adapter invalidates the
Module Federation remote container cache. Vite remotes also invalidate the Vite
SSR entry-loader cache; the option is forwarded to that loader. strategy and
fetchMaxBytes apply to Vite SSR entries, while fetchTimeoutMs applies to
both Vite and CommonJS server-entry requests. Set either limit to 0 to
disable it. Concurrent requests for an expired remote share one revalidation
request on this adapter path.
Revalidation makes subsequent loads use the new remote entry and exposed modules, but does not safely replace already loaded process-level shared singletons such as React. Restart the host process after changing a shared singleton's version or implementation. For Rsbuild/CommonJS remotes, expiration also re-evaluates the downloaded server entry with a forced container re-registration. Module-scope side effects in a remote (for example timers or process listeners) are not automatically disposed, so use revalidation only for remotes whose evaluation is safe to repeat.
Rendering remotes on the server
Hosts on either bundler render remotes the same way: load the remote in the
route loader, render it with lazyRemote inside a Suspense and error
boundary, and link its stylesheets from the route's head:
import { getRemoteStylesheets, lazyRemote } from "@module-federation/tanstack/runtime";
import { createFileRoute } from "@tanstack/react-router";
const StatusCard = lazyRemote(() => import("remote/StatusCard"));
export const Route = createFileRoute("/")({
// Loading the remote before rendering puts its markup in the server response.
// Catch route-loader failures so an unavailable remote does not fail the route.
loader: async () => {
const [stylesheets] = await Promise.all([
getRemoteStylesheets("https://remote.test/mf-manifest.json", "./StatusCard").catch(() => []),
import("remote/StatusCard").catch(() => null),
]);
return { stylesheets };
},
head: ({ loaderData }) => ({
links: loaderData?.stylesheets.map((href) => ({ href, rel: "stylesheet" })),
}),
component: () => (
<RemoteBoundary fallback="Loading…">
<StatusCard />
</RemoteBoundary>
),
});RemoteBoundary stands for any error boundary around a Suspense; the
examples in apps/ include one. The route loader must catch remote failures.
During SSR, React does not guarantee that an error boundary switches to its
fallback for a rejected lazy module; the fallback is handled during client
rendering or hydration recovery instead.
lazyRemote(load, { retryAfterMs })isReact.lazyfor remotes. AReact.lazycomponent that fails once stays rejected until its lazy instance is recreated.lazyRemoterecreates it on the first render afterretryAfterMs(5 seconds by default), so a later request can retry a remote that recovered. This retry policy does not make an SSR error boundary render its fallback. The import itself loads again after a failure too:@module-federation/vite1.23.3+ retries failed server loads, and the Rsbuild adapter removes a failed remote module from Rspack's module cache.getRemoteStylesheets(manifestUrl, expose, { maxAgeMs })lists the stylesheets the remote's manifest declares for an expose, as absolute URLs. A remote's CSS otherwise loads with its JavaScript, after the HTML, so the server-rendered markup would be unstyled until then. It works in the browser and on the server, reuses a fetched manifest for 30 seconds (maxAgeMs), and rejects when the manifest is unreachable or does not list the expose.
Known limitations
- In development, a Vite remote's server modules import React from the files its dev server resolves, so host and remote must resolve React to the same files, as they do in a monorepo. Production builds take React from the host's share scope.
- A Vite dev server injects CSS from JavaScript, so its manifest lists no
stylesheets and
getRemoteStylesheetsreturns none for it in development.
Requirements
- Node 22.18+, 24.11+, or 26+.
- TanStack Start 1.167.43+ (the first release with the Rsbuild integration).
- React 19, which the tests use. TanStack Start also accepts React 18, but as
of 1.168 its Rsbuild server build fails with it: TanStack Router imports
React's
use, which React 18 lacks. React 18 with Vite builds but is untested. - Vite:
@module-federation/vite1.23.3+ andvite8. - Rsbuild:
@module-federation/rsbuild-pluginand@rsbuild/core. An Rsbuild host that renders Vite remotes on the server also needs@module-federation/vite1.23.3+ andvite.
