DEV Community

sanketmunot
sanketmunot

Posted on

Cache-Busting a React Micro-Frontend Inside an Angular Shell

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';
}
Enter fullscreen mode Exit fullscreen mode

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'
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Enabling the manifest was straightforward.

build: {
  manifest: true
}
Enter fullscreen mode Exit fullscreen mode

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"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

The generated file is intentionally minimal.

{
  "js": "assets/main-AbCdEfGh.js",
  "css": "assets/style-XyZ123.css"
}
Enter fullscreen mode Exit fullscreen mode

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}`;
Enter fullscreen mode Exit fullscreen mode

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 = '';
}
Enter fullscreen mode Exit fullscreen mode

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)