SSR vs CSR in Next.js Module Federation

Published: April 22, 2026 · 15 min read

SSR vs CSR in Next.js Module Federation Explained

You deploy a Next.js micro frontend host that loads remote components via NextFederationPlugin. The page renders on the server, sends HTML to the browser, and then React hydrates it. But the remote component attached to window — which does not exist on the server. The build crashes: ReferenceError: window is not defined. Understanding SSR vs CSR in Next.js Module Federation is the difference between a working production deployment and a wall of hydration errors. In the previous article, you configured remote MFEs with basePath and assetPrefix. Now it is time to understand what happens under the hood when those remotes render on the server and in the browser.

In this guide, you will:

  • Understand why Next.js runs webpack twice and what the isServer flag does
  • Learn why NextFederationPlugin generates two remoteEntry.js files (SSR + CSR)
  • See the complete rendering timeline from server HTML to remote component mount
  • Understand why ssr: false is mandatory for all remote component imports
  • Learn why eager: false prevents hydration mismatch errors
  • See real production patterns for hybrid SSR + CSR pages with Module Federation

SSR vs CSR in Next.js Module Federation architecture diagram showing dual webpack builds, two remoteEntry files, and the hydration timeline

The Dual Build — Why Next.js Runs Webpack Twice

When you run next build, Next.js executes webpack two separate times — once for the server and once for the client. Each build produces a completely different output with different module formats, different available APIs, and different entry points.

How Next.js dual build works
# Next.js runs webpack TWICE for every build

# Build 1 — Server (Node.js)
#   isServer = true
#   Output: .next/server/
#   Purpose: render HTML on the server before sending to browser
#   Environment: Node.js — has fs, http, process — NO window, NO document

# Build 2 — Client (Browser)
#   isServer = false
#   Output: .next/static/chunks/
#   Purpose: hydrate server HTML and handle interactivity
#   Environment: Browser — has window, document, localStorage — NO fs, NO process

# Module Federation must work in BOTH builds.
# That means two separate remoteEntry.js files:
#
#   .next/static/ssr/remoteEntry.js       ← Node.js compatible (Build 1)
#   .next/static/chunks/remoteEntry.js    ← Browser compatible (Build 2)
#
# React MFEs (ModuleFederationPlugin) only produce ONE remoteEntry.js
# because they run exclusively in the browser — no server build exists.

This dual build is the reason NextFederationPlugin exists. ModuleFederationPlugin (opens in a new tab) generates a single remoteEntry.js for the browser. NextFederationPlugin hooks into both webpack runs and generates two separate remote entries — one compatible with Node.js (for SSR) and one compatible with the browser (for CSR).

The isServer flag is NOT a runtime check. It is a build-time constant provided by Next.js to the webpack function in next.config.js. During the server build, isServer is true. During the client build, isServer is false. The ternary in the remotes config produces two different URLs at build time — not one URL that switches at runtime.

Two remoteEntry.js Files — SSR vs CSR

NextFederationPlugin generates two remote entry files for every Next.js remote. The host uses the isServer flag to load the correct one during each webpack build.

_next/static/ssr/remoteEntry.js
# SSR remoteEntry.js — runs in Node.js on the server
# Location: /content/_next/static/ssr/remoteEntry.js

# What it contains:
#   - CommonJS module format (require/module.exports)
#   - No references to window, document, localStorage, or DOM APIs
#   - Can access Node.js APIs (fs, http, process)
#   - Exports the same federated modules as the client entry
#   - Used during server-side rendering to resolve remote components

# When it runs:
#   1. User requests a page (GET /faq)
#   2. Next.js server starts rendering the page
#   3. The page imports a remote component (Content/FAQ)
#   4. Module Federation fetches ssr/remoteEntry.js from the Content remote
#   5. The remote module is resolved and its component rendered to HTML
#   6. Server sends the complete HTML to the browser

# Who generates it:
#   NextFederationPlugin automatically creates this file during next build.
#   ModuleFederationPlugin (React) does NOT generate an SSR entry —
#   this is a NextFederationPlugin-only feature.

The host configuration uses isServer to select the correct entry for each build:

apps/Main/next.config.js
// apps/Main/next.config.js — isServer selects the correct remoteEntry
webpack(config, { isServer }) {
  config.plugins.push(
    new NextFederationPlugin({
      name: 'Main',
      filename: 'static/chunks/remoteEntry.js',
      remotes: {
        // React remotes — ONE remoteEntry.js (browser only)
        // No isServer check needed — loaded with ssr: false
        Auth: 'Auth@https://dev.myapp.com/auth/remoteEntry.js',
        Cart: 'Cart@https://dev.myapp.com/cart/remoteEntry.js',
        Account: 'Account@https://dev.myapp.com/account/remoteEntry.js',
        Support: 'Support@https://dev.myapp.com/support/remoteEntry.js',

        // Next.js remotes — TWO remoteEntry.js files (server + browser)
        // isServer picks the correct one for each webpack build
        Content: `Content@https://dev.myapp.com/content/_next/static/${
          isServer ? 'ssr' : 'chunks'
        }/remoteEntry.js`,

        Products: `Products@https://dev.myapp.com/products/_next/static/${
          isServer ? 'ssr' : 'chunks'
        }/remoteEntry.js`,
      },
      shared: {
        react: { singleton: true, requiredVersion: false, eager: false },
        'react-dom': { singleton: true, requiredVersion: false, eager: false },
        'react-redux': { singleton: true, requiredVersion: false, eager: false },
        '@reduxjs/toolkit': { singleton: true, requiredVersion: false, eager: false },
        '@myapp/store': { singleton: true, strictVersion: true, requiredVersion: '1.0.0' },
        '@myapp/api': { singleton: true, strictVersion: true, requiredVersion: '1.0.0' },
        '@myapp/seo': { singleton: true, strictVersion: true, requiredVersion: '1.0.0' },
      },
      extraOptions: {
        exposePages: true,
        enableImageLoaderFix: true,
        enableUrlLoaderFix: true,
        automaticAsyncBoundary: true,
      },
    })
  );

  return config;
}

React Remotes vs Next.js Remotes — Entry File Count

Remote TyperemoteEntry FilesisServer Needed?Why
React (ModuleFederationPlugin)1 — remoteEntry.js at rootNoBrowser-only — no SSR build exists
Next.js (NextFederationPlugin)2 — ssr/ + chunks/YesDual build — server needs Node.js compatible entry

React remotes built with ModuleFederationPlugin produce a single remoteEntry.js that only works in the browser. The host loads them with ssr: false via next/dynamic, so the server build never tries to fetch their remote entry. For a detailed comparison, see the NextFederationPlugin vs ModuleFederationPlugin article.

SSR vs CSR remoteEntry.js comparison showing two separate files generated by NextFederationPlugin for server and browser builds

SSR vs CSR Comparison

AspectSSR (Server-Side Rendering)CSR (Client-Side Rendering)
Where it runsNode.js on the serverBrowser on the client
remoteEntry path_next/static/ssr/remoteEntry.js_next/static/chunks/remoteEntry.js
Module formatCommonJS (require/exports)ES modules / webpack runtime
DOM accessNo (no window, document)Yes (full DOM API)
Node.js accessYes (fs, http, process)No
When it executesOn every page requestAfter hydration completes
Data fetchinggetStaticProps / getServerSidePropsuseEffect + API calls
Redux storeNot available (client-only)Shared singleton via MF
localStorageNot availableAvailable
HTML outputComplete HTML with dataLoading placeholder
First Contentful PaintFast (HTML has content)Slower (JS must load first)
Time to InteractiveSlower (hydration needed)Faster (no hydration)
SEOExcellent (content in HTML)Poor (empty HTML, needs JS)
Generated byNextFederationPlugin onlyBoth plugins

Why ssr: false Is Mandatory for Remote Components

Every remote component — whether it comes from a React remote or a Next.js remote — must be loaded with ssr: false in next/dynamic. This is not optional. Without it, the server build attempts to render the remote component and crashes because Module Federation containers attach to window.

pages/faq/index.tsx
// pages/faq/index.tsx — Loading a Next.js remote with ssr: false
import dynamic from 'next/dynamic';
import { Suspense } from 'react';

const RemoteFAQ = dynamic(
  () => import('Content/FAQ'),
  {
    loading: () => <div className="shimmer" />,
    ssr: false,
  }
);

export default function FAQPage() {
  return (
    <Suspense fallback={<div className="shimmer" />}>
      <RemoteFAQ />
    </Suspense>
  );
}

// What happens at each phase:
//
// SERVER PHASE (Node.js):
//   1. Next.js receives GET /faq
//   2. It renders FAQPage on the server
//   3. RemoteFAQ has ssr: false → server SKIPS it entirely
//   4. Server renders the loading fallback (<div className="shimmer" />)
//   5. Server sends HTML with the shimmer placeholder to the browser
//
// CLIENT PHASE (Browser):
//   1. Browser receives HTML with shimmer placeholder
//   2. React hydrates the page — shimmer is already in the DOM
//   3. After hydration, next/dynamic triggers the Module Federation import
//   4. Browser fetches /content/_next/static/chunks/remoteEntry.js
//   5. remoteEntry.js resolves the FAQ component
//   6. React replaces the shimmer with the actual FAQ component
//   7. FAQ component calls useEffect → fetches data from API → renders

The Host _app.tsx — CSR-First Architecture

In production Next.js micro frontend architectures, the host's _app.tsx wraps every component in ssr: false dynamic imports. The Redux store, authentication, layout, and route protection all run exclusively on the client.

apps/Main/pages/_app.tsx
// apps/Main/pages/_app.tsx — Every wrapper uses ssr: false
import React from 'react';
import type { AppProps } from 'next/app';
import dynamic from 'next/dynamic';
import '../styles/globals.css';

// Layout — header, footer, sidebar (uses window for responsive breakpoints)
const Layout = dynamic(() => import('../components/Layout'), {
  ssr: false,
  loading: () => <div className="shimmer" />,
});

// Redux Provider — creates the singleton store (client-only)
const ClientReduxProvider = dynamic(
  () => import('../components/ClientReduxProvider'),
  { ssr: false, loading: () => null }
);

// Auth — checks refresh token, restores session (needs localStorage)
const InitiateAuth = dynamic(() => import('../components/InitiateAuth'), {
  ssr: false,
  loading: () => null,
});

// Route guard — blocks unauthenticated access (reads Redux auth state)
const ProtectedRoute = dynamic(
  () => import('../components/Security/ProtectedRoute'),
  { ssr: false, loading: () => null }
);

// Analytics — tracks page views (needs window.gtag)
const GoogleAnalytics = dynamic(
  () => import('../components/GoogleAnalytics'),
  { ssr: false, loading: () => null }
);

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <ClientReduxProvider>
      <InitiateAuth />
      <GoogleAnalytics />
      <ProtectedRoute>
        <Layout>
          <Component {...pageProps} />
        </Layout>
      </ProtectedRoute>
    </ClientReduxProvider>
  );
}

export default MyApp;

// Why ssr: false on EVERYTHING?
//
// 1. ClientReduxProvider uses window to check auth state on mount
// 2. InitiateAuth reads localStorage for refresh tokens
// 3. ProtectedRoute reads Redux store (populated by InitiateAuth)
// 4. Layout uses window.innerWidth for responsive breakpoints
// 5. GoogleAnalytics calls window.gtag
//
// None of these can run on the server. If ssr were true,
// the server build would crash with "window is not defined".
⚠️

This creates a CSR-first architecture inside an SSR-capable framework. The server sends minimal HTML (loading placeholders), and the browser renders everything after hydration. The trade-off: you lose SSR's SEO and First Contentful Paint benefits for interactive sections, but you avoid hydration mismatches and keep Module Federation's singleton state management working correctly.

The Complete Rendering Timeline

Here is exactly what happens from the moment a user requests a page to the moment the remote component finishes rendering:

Rendering timeline: Server → Hydration → Remote Load
# Complete rendering timeline: SSR + CSR with Module Federation

# Phase 1 — Server-Side Rendering (Node.js)
# ──────────────────────────────────────────
# 1. Browser requests GET /faq
# 2. Next.js server receives the request
# 3. Server runs getStaticProps (if any) — fetches static data
# 4. Server starts rendering the React component tree
# 5. It reaches <RemoteFAQ /> with ssr: false
# 6. Server renders the LOADING FALLBACK instead of the component
#    Output: <div class="shimmer"></div>
# 7. Server sends complete HTML to browser
#    → Fast First Contentful Paint (user sees shimmer + layout)

# Phase 2 — Hydration (Browser)
# ──────────────────────────────────────────
# 8.  Browser receives HTML and paints it immediately
# 9.  Browser downloads the client JavaScript bundle
# 10. React hydration starts — attaches event listeners to existing HTML
# 11. Hydration matches server HTML node-by-node
# 12. RemoteFAQ (ssr: false) was not in server HTML — React skips it
# 13. Hydration completes — page is now interactive

# Phase 3 — Remote Component Loading (Browser)
# ──────────────────────────────────────────
# 14. next/dynamic triggers the Module Federation import
# 15. Browser fetches /content/_next/static/chunks/remoteEntry.js
# 16. remoteEntry.js registers the Content container
# 17. Module Federation negotiates shared dependencies (React, Redux)
# 18. Browser fetches the FAQ component chunk
# 19. FAQ component mounts and runs useEffect
# 20. useEffect calls the API → fetches FAQ data
# 21. FAQ data arrives → component re-renders with content
# 22. Shimmer is replaced with the fully rendered FAQ

# Total user experience:
#   0ms:    White screen (HTML downloading)
#   ~200ms: Layout + shimmer visible (server HTML painted)
#   ~500ms: Page interactive (hydration complete)
#   ~800ms: Remote component appears (remoteEntry fetched)
#   ~1200ms: FAQ data displayed (API response rendered)

Error Handling for Remote Components

Remote components can fail at three different points: network fetch, component initialization, or runtime rendering. A production setup layers three error handling mechanisms.

pages/beautycare/index.tsx
// pages/beautycare/index.tsx — Remote loading with error boundary
import dynamic from 'next/dynamic';
import { Suspense } from 'react';
import ErrorBoundary from '../../components/ErrorBoundary';

const RemoteBeautyCare = dynamic(
  () => import('Products/BeautyCare').catch(() => {
    return { default: () => <div>Products service is temporarily unavailable.</div> };
  }),
  {
    loading: () => <div className="shimmer" />,
    ssr: false,
  }
);

export default function BeautyCarePage() {
  return (
    <ErrorBoundary>
      <Suspense fallback={<div className="shimmer" />}>
        <RemoteBeautyCare />
      </Suspense>
    </ErrorBoundary>
  );
}

// Error handling layers:
//
// Layer 1 — .catch() on import:
//   If remoteEntry.js fails to load (404, network error, timeout),
//   the catch returns a fallback component instead of crashing.
//
// Layer 2 — ErrorBoundary:
//   If the remote component throws during rendering (null reference,
//   missing data, API error), the ErrorBoundary catches it and
//   displays a recovery UI with a "Reload" button.
//
// Layer 3 — Suspense fallback:
//   While the remote is loading, the shimmer placeholder displays.
//   If the remote loads but suspends (e.g., lazy subcomponents),
//   Suspense shows the fallback until everything resolves.

Hydration and Module Federation — No Mismatch

A common concern: "If the server renders a shimmer but the client renders the full component, won't React throw a hydration mismatch error?" The answer is no — and here is why.

Why ssr: false prevents hydration mismatch
// PROBLEM: Hydration mismatch when remote renders different content
// on server vs client

// Server renders:
<div class="shimmer"></div>   <!-- ssr: false  loading fallback -->

// Client hydrates and then replaces with:
<div class="faq-container">
  <h2>Frequently Asked Questions</h2>
  <div class="faq-item">...</div>
</div>

// This is NOT a hydration mismatch because ssr: false tells React
// "this component does not exist in the server HTML."
// React knows to mount it fresh on the client — no comparison needed.

// REAL hydration mismatch happens when:
// 1. A component renders on the server AND the client
// 2. The server output differs from the client output
//
// Example of what WOULD cause a mismatch:
//   Server: <p>Current time: 10:30:00 AM</p>
//   Client: <p>Current time: 10:30:01 AM</p>
//
// The fix: use ssr: false for any component whose output depends on
// browser-only state (time, localStorage, window size, auth tokens).

// With Module Federation remotes, ssr: false is ALWAYS the correct choice
// because remote components:
//   - Attach to window (which doesn't exist on the server)
//   - Read from localStorage/sessionStorage for auth state
//   - Use browser APIs (IntersectionObserver, ResizeObserver, etc.)
//   - Fetch data client-side via useEffect + API calls

Why eager: false Prevents Hydration Errors

The eager setting in shared dependency configuration controls when shared modules load. In Next.js, eager: true breaks hydration by creating duplicate React instances.

Shared dependencies — eager: false vs eager: true
// Why eager: false is critical for SSR in Next.js

// WRONG — eager: true breaks hydration
shared: {
  react: { singleton: true, requiredVersion: false, eager: true },
}
// What happens:
// 1. Server renders HTML using its own React instance
// 2. Browser starts hydration with the SAME React instance
// 3. eager: true loads a SECOND React instance synchronously
//    during webpack chunk evaluation (before hydration finishes)
// 4. Two React instances exist simultaneously
// 5. Hydration fails: "Cannot read properties of null"
//    or "Rendered fewer hooks than expected"

// CORRECT — eager: false defers loading until needed
shared: {
  react: { singleton: true, requiredVersion: false, eager: false },
}
// What happens:
// 1. Server renders HTML using its own React instance
// 2. Browser starts hydration with the server's React
// 3. Module Federation's async negotiation runs AFTER hydration
// 4. The singleton check finds React already loaded — reuses it
// 5. One React instance throughout the entire lifecycle
// 6. No hydration mismatch, no duplicate instances

// This is why EVERY shared dependency in Next.js must use eager: false.
// React MFEs can sometimes use eager: true because they have no
// server-rendered HTML to hydrate against — there is no SSR phase.

For a deep dive into shared dependency configuration, see Shared Dependencies and Singleton Pattern.

Hybrid Pages — SSR Data + CSR Remotes

The most powerful pattern in Next.js Module Federation is combining server-rendered static content with client-rendered remote components on the same page. The host uses getStaticProps (or getServerSideProps) for data that must be in the initial HTML, while remote components load after hydration.

pages/index.tsx
// pages/index.tsx — SSR data fetching on the HOST (not remotes)
import { GetStaticProps } from 'next';
import dynamic from 'next/dynamic';

// Remote MFE — loaded client-side only
const RemoteProducts = dynamic(
  () => import('Products/CategoryPage'),
  { ssr: false, loading: () => <div className="shimmer" /> }
);

interface HomePageProps {
  bannerData: { title: string; image: string }[];
  timestamp: number;
}

export default function HomePage({ bannerData, timestamp }: HomePageProps) {
  return (
    <div>
      {/* SSR content — rendered on the server as static HTML */}
      <section className="hero-banner">
        {bannerData.map((banner, i) => (
          <div key={i}>
            <h2>{banner.title}</h2>
            <img src={banner.image} alt={banner.title} />
          </div>
        ))}
      </section>

      {/* CSR content — remote loads after hydration */}
      <section className="product-grid">
        <RemoteProducts />
      </section>
    </div>
  );
}

// getStaticProps runs at BUILD TIME on the server.
// It fetches data that becomes part of the static HTML.
// Remote components are NOT involved — they load client-side.
export const getStaticProps: GetStaticProps<HomePageProps> = async () => {
  const response = await fetch('https://api.myapp.com/banners');
  const bannerData = await response.json();

  return {
    props: {
      bannerData: bannerData.data,
      timestamp: Date.now(),
    },
    revalidate: 300, // ISR: regenerate every 5 minutes
  };
};

// This creates a HYBRID page:
//   - Banner section: SSR (pre-rendered HTML, fast First Contentful Paint)
//   - Products section: CSR (loads after hydration, shows shimmer first)
//
// The remote component NEVER runs during getStaticProps.
// Module Federation imports only execute in the browser (ssr: false).

This hybrid approach gives you:

  • Fast First Contentful Paint — the server sends real content (banners, headings, structured data) in the initial HTML
  • SEO-friendly markup — search engines see the pre-rendered content without executing JavaScript
  • Independent deployability — remote MFEs update independently without rebuilding the host
  • Resilient architecture — if a remote fails to load, the SSR content still displays

How Remote Components Fetch Data

Remote components exposed via Module Federation are regular React components — not Next.js page components. They cannot use getServerSideProps or getStaticProps. All data fetching happens client-side via useEffect.

apps/Content/components/FAQ.tsx
// Remote component: apps/Content/components/FAQ.tsx
// This runs INSIDE the Content remote MFE — loaded by the host
import { useState, useEffect } from 'react';
import { getFAQData } from '@myapp/api';

export default function FAQ() {
  const [faqData, setFaqData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  // Data fetching happens client-side via useEffect
  // NOT via getServerSideProps or getStaticProps
  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await getFAQData('active');
        setFaqData(response.data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, []);

  if (loading) return <div className="shimmer" />;
  if (error) return <div className="error">{error}</div>;

  return (
    <div className="faq-container">
      {faqData.map((item) => (
        <details key={item.id}>
          <summary>{item.question}</summary>
          <p>{item.answer}</p>
        </details>
      ))}
    </div>
  );
}

// Why useEffect and not getServerSideProps?
//
// 1. This component is EXPOSED via Module Federation (exposes: { './FAQ': ... })
// 2. The host loads it with ssr: false — it never runs on the server
// 3. getServerSideProps only works for PAGE components in the pages/ directory
// 4. Exposed components are regular React components, not Next.js pages
// 5. They cannot use Next.js data fetching methods (getStaticProps, etc.)
//
// This is the standard pattern for ALL remote MFE components:
//   Mount → useEffect → fetch API → render data

The Client Redux Provider — Why Redux Is Client-Only

The shared Redux store is the backbone of state management across remote MFEs. It must be a singleton — one store instance shared by every component. This singleton lives on the client, not the server.

components/ClientReduxProvider.tsx
// components/ClientReduxProvider.tsx — Client-only Redux wrapper
'use client';
import { ReactNode } from 'react';
import { Provider, store } from '@myapp/store';

interface ClientReduxProviderProps {
  children: ReactNode;
}

export default function ClientReduxProvider({ children }: ClientReduxProviderProps) {
  return (
    <Provider store={store}>
      {children}
    </Provider>
  );
}

// 'use client' marks this as a Client Component.
// The Redux store is a singleton shared via Module Federation.
// Every remote MFE that imports @myapp/store gets the SAME instance.
//
// Why this CANNOT run on the server:
//   1. The store reads localStorage for persisted auth tokens
//   2. Redux devtools extension attaches to window.__REDUX_DEVTOOLS__
//   3. The store singleton must be created ONCE in the browser
//   4. If created on the server, each request would share state
//      between different users — a critical security vulnerability

Browser API Guards — Defensive Coding

Even with ssr: false, browser API guards (typeof window !== 'undefined') serve as a safety net. The module code is bundled into both builds — the guard prevents crashes if the code path is reached during the server build.

Browser API guards in remote components
// Remote component with browser API guards
// apps/Products/pages/products/[productNumericId].tsx

const handleBuyNow = () => {
  handleAddToCart();
  // Guard: typeof window check prevents server crash
  if (typeof window !== 'undefined') {
    return handleNavigate('/bag');
  }
};

// Guest cart service — 100% client-side storage
const isLocalStorageAvailable = () => {
  try {
    const test = '__storage_test__';
    localStorage.setItem(test, test);
    localStorage.removeItem(test);
    return true;
  } catch (e) {
    return false;
  }
};

// Why typeof window checks still matter with ssr: false:
//
// 1. The MODULE is bundled into both server and client builds
//    even if the COMPONENT is loaded with ssr: false
// 2. Tree-shaking might not eliminate all browser-only code paths
// 3. If the module is accidentally imported without ssr: false,
//    the typeof window guard prevents a server crash
// 4. It's defensive coding — the guard costs nothing but prevents
//    hard-to-debug "window is not defined" errors in production

automaticAsyncBoundary — Replacing bootstrap.js

React MFEs require a manual bootstrap.js file to create an async entry point for shared dependency negotiation. Next.js MFEs using NextFederationPlugin replace this with automaticAsyncBoundary: true.

automaticAsyncBoundary vs bootstrap.js
// Next.js remote — automaticAsyncBoundary replaces bootstrap.js

// React MFE approach (manual bootstrap.js):
// index.js
import('./bootstrap');

// bootstrap.js — async entry point
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));

// The separate bootstrap.js file forces webpack to load shared
// dependencies asynchronously before the app mounts.
// Without it, shared dependency negotiation fails.


// Next.js MFE approach (automaticAsyncBoundary: true):
// next.config.js
new NextFederationPlugin({
  name: 'Content',
  filename: 'static/chunks/remoteEntry.js',
  exposes: {
    './FAQ': './components/FAQ',
  },
  extraOptions: {
    automaticAsyncBoundary: true,
  },
});

// NextFederationPlugin wraps EVERY exposed module in an async boundary
// automatically — no bootstrap.js file needed.
//
// What it generates internally:
//   export default () => import('./components/FAQ');
//
// This ensures shared dependencies are negotiated asynchronously
// before the component code executes.
//
// Benefits over manual bootstrap.js:
// 1. No extra file to maintain
// 2. Works with Next.js's own code splitting
// 3. Compatible with both SSR and CSR builds
// 4. Each exposed module gets its own boundary (not one for the entire app)

Common SSR + CSR Mistakes

  1. Forgetting ssr: false on remote imports — The server tries to render the remote component, encounters window, and crashes. Every import('RemoteName/Component') must be wrapped in next/dynamic with ssr: false.

  2. Using eager: true for shared dependencies — Creates duplicate React instances during hydration. Use eager: false for all shared deps in Next.js. See the host app setup guide for the complete shared config.

  3. Expecting getServerSideProps in remote components — Remote components are regular React components, not Next.js pages. They fetch data via useEffect, not Next.js data fetching methods.

  4. Using React-style remote paths for Next.js remotes — Writing Content@/content/remoteEntry.js instead of the Next.js path with isServer ternary. The SSR build needs ssr/remoteEntry.js, not the browser entry. See the basePath and assetPrefix guide for path details.

  5. Assuming SSR content for remote sections — Remote MFE sections render as loading placeholders in the server HTML. If SEO requires content in those sections, move the critical data to the host's getStaticProps and pass it as props.

  6. Not using automaticAsyncBoundary — Without it, shared dependency negotiation can fail silently. NextFederationPlugin's automaticAsyncBoundary: true replaces the manual bootstrap.js pattern from React MFEs.

What's Next

You now understand how SSR and CSR work together in Next.js Module Federation — the dual webpack build, two remoteEntry files, isServer flag, ssr: false requirement, and the complete rendering timeline from server HTML to client-rendered remote. The next article covers mixing React and Next.js micro frontends together — how to load React remotes (ModuleFederationPlugin) and Next.js remotes (NextFederationPlugin) in the same host application, handle routing differences, and manage shared state across both frameworks.

← Back to Next.js Remote MFE with basePath and assetPrefix

Continue to Mixing React and Next.js Micro Frontends →


Frequently Asked Questions

Why does NextFederationPlugin generate two remoteEntry.js files?

NextFederationPlugin generates two remoteEntry.js files because Next.js runs webpack twice — once for the server (Node.js) and once for the client (browser). The SSR entry at _next/static/ssr/remoteEntry.js is a CommonJS module that runs in Node.js with no window or document access. The CSR entry at _next/static/chunks/remoteEntry.js is a browser script that attaches to window and uses DOM APIs. The host uses the isServer flag to pick the correct file for each build. React MFEs built with ModuleFederationPlugin only generate one remoteEntry.js because they run exclusively in the browser.

Why must all remote components use ssr: false in next/dynamic?

Remote components must use ssr: false because Module Federation remotes attach their module container to the window object, which does not exist on the server. Without ssr: false, the server attempts to import the remote during rendering and crashes with ReferenceError: window is not defined. Setting ssr: false tells Next.js to skip the component during server rendering and render the loading fallback instead. The remote loads only after hydration completes in the browser.

What is the isServer flag in Next.js Module Federation?

The isServer flag is a boolean passed by Next.js to the webpack function in next.config.js. It is true during the server build (Node.js) and false during the client build (browser). In Module Federation, it is used to select the correct remoteEntry.js path for Next.js remotes — ssr/remoteEntry.js for the server build and chunks/remoteEntry.js for the client build. React remotes do not need this check because they use a single remoteEntry.js that only runs in the browser.

How does hydration work with Module Federation remote components?

When a remote component has ssr: false, the server renders the loading fallback (e.g., a shimmer placeholder) instead of the component. The browser receives this HTML and React hydrates it — attaching event listeners to the existing shimmer div. After hydration completes, next/dynamic triggers the Module Federation import, fetches the remote's remoteEntry.js, resolves the component, and React replaces the shimmer with the actual remote component. This is not a hydration mismatch because React knows the component was excluded from SSR.

Why is eager: false required for shared dependencies in Next.js?

Setting eager: false ensures shared dependencies load asynchronously after hydration, not synchronously during chunk evaluation. With eager: true, webpack loads shared modules (like React) immediately when the chunk evaluates — creating a second React instance before hydration finishes. Two React instances cause hydration mismatch errors like Cannot read properties of null or Rendered fewer hooks than expected. With eager: false, Module Federation's async negotiation runs after hydration, finds React already loaded, and reuses the existing instance.

Can remote MFE components use getServerSideProps or getStaticProps?

No. Remote MFE components exposed via Module Federation are regular React components, not Next.js page components. getServerSideProps and getStaticProps only work for files inside the pages/ directory of the app that owns them. Exposed components cannot use these data fetching methods because they are imported dynamically at runtime, not routed to by Next.js. Remote components fetch data client-side using useEffect hooks and API calls instead.