A few months ago, I built a React micro-frontend that could be mounted inside our legacy Angular application. Rather than introducing Module Federation or rewriting parts of the Angular codebase, I kept the integration intentionally lightweight. The Angular shell dynamically loads the React application's JavaScript and CSS files, and the React application mounts itself into a container.
The mounting logic looked roughly like this:
loadReactApplication() {
this.reactScript = '/assets/react-mf/assets/main.js';
this.reactCss = '/assets/react-mf/assets/style.css';
}
The approach was simple, easy to understand, and most importantly—it worked.
For months, there were no issues. Developers could build new React features while the rest of the application continued to run on Angular. Since most of our testing happened locally with fresh browser sessions, nothing appeared out of the ordinary.
The problem only surfaced after the feature reached our lower QA environment.
After deploying a new version, testers reported that some UI changes were missing. The React application loaded successfully, but parts of the interface still looked like the previous release. Strangely, a hard refresh immediately fixed the issue.
That immediately pointed towards a browser caching problem.
Why It Happened
My Vite build was generating static filenames.
build: {
rollupOptions: {
output: {
entryFileNames: 'assets/main.js',
assetFileNames: () => 'assets/style.css'
}
}
}
Since every deployment produced the exact same filenames, browsers continued serving cached copies of main.js and style.css.
The Angular shell also had those filenames hardcoded, so there was no opportunity to reference a newer version.
The First Solution That Came to Mind
Vite already generates a manifest describing every build artifact.
dist/
├── assets/
├── .vite/
│ └── manifest.json
Enabling the manifest was straightforward.
build: {
manifest: true
}
My first instinct was to make Angular fetch and parse this manifest.
After all, it already contained everything I needed.
{
"index.html": {
"file": "assets/main-AbCdEfGh.js",
"css": [
"assets/style-XyZ123.css"
]
}
}
While this would have worked, I wasn't happy with the idea of Angular depending on Vite's manifest structure.
The Angular application doesn't care about chunks, dynamic imports, or any other metadata inside the manifest.
It only needs two values:
- Which JavaScript file should I load?
- Which stylesheet should I load?
Building a Smaller Contract
Instead of exposing the complete manifest, I added a small post-build script.
{
"scripts": {
"build": "tsc -b && vite build && node scripts/write-entry-asset.mjs"
}
}
The script reads .vite/manifest.json, extracts only the entry assets, writes a tiny JSON file, and removes the original manifest.
const manifest = JSON.parse(
readFileSync(manifestPath, 'utf8')
);
const entry = manifest['index.html'];
writeFileSync(
entryAssetPath,
JSON.stringify({
js: entry.file,
css: entry.css[0]
}, null, 2)
);
rmSync(viteDir, {
recursive: true,
force: true
});
The generated file is intentionally minimal.
{
"js": "assets/main-AbCdEfGh.js",
"css": "assets/style-XyZ123.css"
}
Loading the Assets
Now Angular doesn't need to understand Vite at all.
It simply requests the contract and injects the returned assets.
const assetBase = '/assets/react-mf';
const { js, css } = await fetch(
`${assetBase}/entry-asset.json`
).then(res => res.json());
this.reactScript = `${assetBase}/${js}`;
this.reactCss = `${assetBase}/${css}`;
During development, nothing changes. Angular still points directly to the Vite development server.
if (useViteDevServer) {
this.reactScript = `http://${host}:5173/src/main.tsx`;
this.reactCss = '';
}
The Result
After this change, every deployment generates uniquely named assets, browsers automatically download the latest JavaScript and CSS, and the Angular shell never needs to know how Vite structures its build output.
The React micro-frontend architecture itself didn't change. The only thing that changed was the contract between the build system and the host application.
Sometimes solving production problems isn't about adding more infrastructure—it's about introducing a smaller, cleaner interface between two systems.
Connect Linkedin
Top comments (0)