5 min readWeb Development
Microfrontends in Simple Terms: Vue + React, Deployed on Vercel
Build two tiny apps, one in Vue and one in React, join them in the browser with Module Federation, and put both live on Vercel.
- #microfrontends
- #module-federation
- #vue
- #react
- #vite
- #vercel
Microfrontends in Simple Terms: Vue + React, Deployed on Vercel
A microfrontend is a piece of a website that is built and deployed as its own app. The page loads it in the browser, at runtime, like a widget from another website.
It's the frontend version of microservices. One team can ship its part without waiting for the others, and each team can even use a different framework.
Live demo: microfronts-host.vercel.app · Code: github.com/iamsomraj/microfronts
What we're building
Two apps:
- Remote (React): owns a
Headercomponent and shares it. - Host (Vue): the main page. It loads the
Headerfrom the remote and shows it.
The Vue page has a counter. It passes the count to the React component, and React can update it back. The two apps are deployed separately, on two different URLs.
The glue is Module Federation. One app exposes code, another app consumes it, and it all happens in the browser.
When should you use this?
- Use it when several teams work on one big site, or when you're moving an old app to a new framework bit by bit.
- Skip it for a small app built by one team. It adds moving parts. A normal component library is simpler.
Step 1: Create the two apps
One folder, two Vite apps, joined by a pnpm workspace:
mkdir microfronts && cd microfronts
pnpm create vite remote --template react
pnpm create vite host --template vue
# pnpm-workspace.yaml
packages:
- host
- remote
Add the Module Federation plugin to both:
pnpm --filter host --filter remote add -D @module-federation/vite
Step 2: The remote shares a component
The React component is a normal component. It takes props:
// remote/src/components/Header.jsx
const Header = ({ count = 0, onAdd }) => (
<div>
<h2>Hello from React!</h2>
<p>Count from Vue: {count}</p>
{onAdd && <button onClick={onAdd}>Add 1 from React</button>}
</div>
);
export default Header;
Then the remote exposes it:
// remote/vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { federation } from '@module-federation/vite';
export default defineConfig({
plugins: [
// Vue has no React Fast Refresh, so skip it for shared components.
react({ exclude: [/node_modules/, /src\/components\//] }),
federation({
name: 'remote_app',
filename: 'remoteEntry.js', // the "menu" other apps read
exposes: { './Header': './src/components/Header.jsx' },
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
}),
],
build: { target: 'esnext' },
});
When it builds, the remote creates a remoteEntry.js file. Think of it as a menu that says "I have a Header, and here's where to get it."
Step 3: The host loads it
The host tells the plugin where the remote lives. The URL comes from an env var, so it works locally and in production:
// host/vite.config.js
import { defineConfig, loadEnv } from 'vite';
import vue from '@vitejs/plugin-vue';
import { federation } from '@module-federation/vite';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd());
const remoteUrl = env.VITE_REMOTE_URL ?? 'http://localhost:3001';
return {
plugins: [
vue(),
federation({
name: 'host_app',
remotes: {
remote_app: {
type: 'module',
name: 'remote_app',
entry: `${remoteUrl}/remoteEntry.js`,
},
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
}),
],
build: { target: 'esnext' },
};
});
Vue can't render a React component directly. So we give React an empty <div> and let React render into it:
<!-- host/src/App.vue -->
<script setup>
import { onMounted, ref, watch } from 'vue';
const count = ref(0);
const status = ref('loading');
const target = ref(null);
let root;
const render = async () => {
const React = await import('react');
const { default: Header } = await import('remote_app/Header');
const props = { count: count.value, onAdd: () => count.value++ };
root.render(React.createElement(Header, props));
};
onMounted(async () => {
try {
const { createRoot } = await import('react-dom/client');
root = createRoot(target.value);
await render();
status.value = 'ready';
} catch {
status.value = 'error';
}
});
// When the Vue count changes, render React again with the new props.
watch(count, () => root && render());
</script>
<template>
<button @click="count++">Count in Vue: {{ count }}</button>
<p v-if="status === 'error'">The React remote is down.</p>
<div ref="target"></div>
</template>
Data goes down as props (count) and comes back up through a callback (onAdd). That's all you need for the two apps to talk.
Step 4: Run it locally
Give each app its own port in its package.json:
"dev": "vite --port 3000" // host
"dev": "vite --port 3001" // remote
Then add a root script that starts both:
{ "scripts": { "dev": "pnpm --parallel -r dev" } }
pnpm install
pnpm dev
Open http://localhost:3000. You'll see the Vue page with the React box inside it.
Step 5: Deploy both on Vercel
Each app is its own Vercel project, both from the same GitHub repo:
| Project | Root Directory | Setting |
|---|---|---|
microfronts-remote | remote | CORS headers (below) |
microfronts-host | host | Env var VITE_REMOTE_URL=https://microfronts-remote.vercel.app |
The host's page loads files from the remote's domain. Browsers block that unless the remote allows it, so add a CORS header:
// remote/vercel.json
{
"headers": [
{
"source": "/(.*)",
"headers": [{ "key": "Access-Control-Allow-Origin", "value": "*" }]
}
]
}
Deploy the remote first, then the host. From then on, every push to main redeploys both.
Here's the best part: change the React Header and redeploy only the remote. The host picks up the new version on the next page load, without being rebuilt.
Things to watch out for
- Load React once.
singleton: truemakes both apps share one copy of React. Two copies break hooks. - CORS. If the remote's files are blocked, check the header above.
- The remote can go down. Always wrap the import in
try/catchand show a fallback, so the host keeps working.
That's it
Two apps, two frameworks, two deployments, and one page. Try the live demo and click both buttons. Vue and React are keeping the same count.