The React host
What we’re building
Section titled “What we’re building”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.
Pros & cons
Section titled “Pros & cons”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.
Set it up
Section titled “Set it up”1. apps/shell/package.json
Section titled “1. apps/shell/package.json”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.
2. apps/shell/vite.config.ts
Section titled “2. apps/shell/vite.config.ts”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, },});3. apps/shell/index.html
Section titled “3. apps/shell/index.html”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>4. apps/shell/src/main.tsx
Section titled “4. apps/shell/src/main.tsx”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>,);5. apps/shell/src/App.tsx
Section titled “5. apps/shell/src/App.tsx”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; }Verify
Section titled “Verify”Install and start the shell:
pnpm installpnpm --filter shell devExpected 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:
pnpm --filter shell buildExpected: 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:
- What does the shell own that a remote does not, and why does exactly one app need to own it?
- Why build the shell as a plain React app before adding Module Federation, rather than configuring federation from the start?
- 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?
- Why does the dev script use
--strictPorton 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 →