Skip to content

The React host

The shell — the React app that owns everything shared across Mosaic: the layout, the top-nav, and (soon) the session and top-level routing. In this lesson we build the shell as a plain React + Vite app with a real layout and a slot where remotes will mount. It runs on port 5000. No federation yet — that’s the next lesson. First we need a host worth hosting into.

In a micro-frontend system, one app has to own the frame: the header the user sees on every page, the navigation between areas, the session, the routing. That’s the shell (or “host”). Everything else — catalog, cart, content — is a remote that mounts inside the shell’s layout.

Making the shell a normal React app first, before adding Module Federation, is deliberate. Federation is a runtime concern layered on top of an ordinary Vite build; if the shell doesn’t run and render on its own, no amount of federation config will help. Establishing a clean host — layout, nav, a mount slot — gives every later remote a stable place to land, and gives us a working app to verify against at each step.

The shell is React because React’s ecosystem is the series’ backbone and because sharing React as a singleton with the React remotes (the catalog) is the simplest interop path. Non-React remotes (the Svelte cart) mount through a Web Component boundary instead — but that’s later.

A dedicated host app vs. no host (peer remotes)

  • Pros: One place owns layout, nav, session, and routing, so the user sees one coherent app; remotes stay focused on their slice; there’s a single, obvious entry point to load.
  • Cons: The host is a shared dependency every team relies on — a change to the frame affects everyone, so it needs careful ownership. It can also become a bottleneck if it grows beyond “the frame.” Keeping it thin is the discipline.

Building the host as plain React first vs. adding federation immediately

  • Pros: The app runs and renders from step one; you can verify the layout in isolation; federation becomes an additive change, not a prerequisite for anything working.
  • Cons: You don’t see a remote load until the next lesson — the “wow” is deferred in exchange for a foundation you can actually debug.

The shell is a React + Vite app. It runs on port 5000 and, for now, depends only on React and the Vite React plugin.

{
"name": "shell",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --port 5000 --strictPort",
"build": "vite build",
"preview": "vite preview --port 5000 --strictPort",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"react": "^18.3.0",
"react-dom": "^18.3.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.0",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0"
}
}

--strictPort makes Vite fail loudly if 5000 is taken, instead of silently picking another port — important once the remotes expect the shell at a fixed origin.

For now just the React plugin and the port. The Module Federation plugin, server.origin, and build.target are added in the next lesson — this file grows into the host config.

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 5000,
},
});

Vite’s entry HTML mounts the React root.

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mosaic</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

Standard React 18 client entry.

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);

The frame. A header with the brand and top-nav, and a <main> that is the mount slot for remotes. Right now the slot holds a placeholder; in the next lesson a real remote renders there.

export function App() {
return (
<div className="shell">
<header className="shell__header">
<a className="shell__brand" href="/">
🧩 Mosaic
</a>
<nav className="shell__nav">
<a href="/">Catalog</a>
<a href="/cart">Cart</a>
<a href="/about">About</a>
</nav>
</header>
<main className="shell__main">
{/* Remotes mount here. For now, a placeholder. */}
<section className="shell__slot">
<h1>Welcome to Mosaic</h1>
<p>The shell is running. Remotes will load into this slot at runtime.</p>
</section>
</main>
</div>
);
}

6. apps/shell/src/index.css (optional, imported from main.tsx)

Section titled “6. apps/shell/src/index.css (optional, imported from main.tsx)”

Just enough to make the frame legible while we work. Real styling comes with the design system.

.shell__header {
display: flex;
align-items: center;
gap: 2rem;
padding: 1rem 1.5rem;
border-bottom: 1px solid #e5e7eb;
}
.shell__brand { font-weight: 700; text-decoration: none; color: #8b5cf6; }
.shell__nav { display: flex; gap: 1rem; }
.shell__main { padding: 1.5rem; }

Install and start the shell:

Terminal window
pnpm install
pnpm --filter shell dev

Expected output:

VITE ready
➜ Local: http://localhost:5000/

Open http://localhost:5000/. You should see the 🧩 Mosaic header, the three nav links, and the “The shell is running” placeholder in the main slot. Then confirm it builds clean — the check every lesson ends on:

Terminal window
pnpm --filter shell build

Expected: a successful build writing dist/ with no type or bundle errors. A running dev server and a clean build means the host is solid enough to federate into.

Check your understanding:

  1. What does the shell own that a remote does not, and why does exactly one app need to own it?
  2. Why build the shell as a plain React app before adding Module Federation, rather than configuring federation from the start?
  3. The shell is React and shares React with the catalog remote, but the Svelte cart mounts differently. What’s the boundary the non-React remote uses instead?
  4. Why does the dev script use --strictPort on port 5000?

The shell runs: a React + Vite app on port 5000 with a header, top-nav, and a <main> mount slot waiting for remotes. It’s an ordinary app right now — which is the point. Federation is the next layer, not the foundation.

Now let’s turn this host into an actual host by adding the Module Federation config and loading a first remote.

Next → Host config →