A Qwik shell loads separately built Angular and React bundles as custom elements, with a message attribute down and a DOM event back. This demo proves the contract — not Module Federation, and not four production deploys.
Author:Omid Farhang
Published:May 11, 2024Last Updated:
Last Updated September 13, 2026Reading time:7 min
This is the companion for Why and How. It proves one thing: a host page can load separately built Angular and React bundles as custom elements, pass a message attribute down, and take a microfrontend:message event back.
It does not prove Module Federation, independent production deploys, or a shared design system. npm run dev builds the remotes, then starts one Qwik server that serves them from public/mfes/. Rust WebAssembly is a shell helper for CPU work, not a fourth micro frontend.
Qwik is the shell. Angular and React are separate apps, exposed as Web Components, and loaded at runtime. The shell owns shared state, passes it down through custom element attributes, and listens for messages through a small DOM event contract.
Each micro frontend builds into qwik-micro-frontend/public/mfes/. The optional Rust WASM package is emitted there too, so the shell can serve every independently built piece from one public asset folder. The root package.json orchestrates the build so Angular, React, Rust WASM, and the Qwik shell can be built with one command.
The Angular remote in this repo is still an NgModule app: ngDoBootstrap plus createCustomElement. That is the 2024 demo API, not a recommendation to stay on NgModule. Register the root component as a custom element:
The React remote wraps the UI in a custom element, observes the message attribute, and isolates its CSS in an open Shadow DOM. Host styles do not restyle this button; the Angular remote, in the light DOM, is not similarly isolated.
// qwik-micro-frontend/src/routes/index.tsx
import{$,component$,useSignal,useVisibleTask$}from'@builder.io/qwik';constassetBase=import.meta.env.BASE_URL;constassetUrl=(path: string)=>{constbase=assetBase.endsWith('/')?assetBase:`${assetBase}/`;return`${base}${path.replace(/^\//,'')}`;};constscripts=newMap<string,Promise<void>>();constloadScript=(src: string)=>{if(scripts.has(src)){returnscripts.get(src);}constpromise=newPromise<void>((resolve,reject)=>{constscript=document.createElement('script');script.src=src;script.type='module';script.onload=()=>resolve();script.onerror=()=>reject(newError(`Unable to load ${src}`));document.head.append(script);});scripts.set(src,promise);returnpromise;};exportdefaultcomponent$(()=>{constassetsReady=useSignal(false);constmessage=useSignal('Hello from the Qwik shell');useVisibleTask$(({cleanup})=>{consthandleMicroFrontendMessage=(event: Event)=>{const{source,message: nextMessage}=(eventasCustomEvent).detail;message.value=`${source}: ${nextMessage}`;};window.addEventListener('microfrontend:message',handleMicroFrontendMessage);Promise.all([loadScript(assetUrl('mfes/angular/polyfills.js')),loadScript(assetUrl('mfes/angular/main.js')),loadScript(assetUrl('mfes/react/react-microfrontend.js')),]).then(()=>{assetsReady.value=true;});cleanup(()=>{window.removeEventListener('microfrontend:message',handleMicroFrontendMessage);});});constupdateFromShell=$(()=>{message.value='Qwik updated the contract for every micro frontend';});return(<main><buttontype="button"onClick$={updateFromShell}>UpdatesharedmessagefromQwik</button>{assetsReady.value?(<><angular-microfrontendmessage={message.value}></angular-microfrontend><react-microfrontendmessage={message.value}></react-microfrontend></>):(<p>Loadingmicrofrontendbundles...</p>)}</main>);});
This keeps the integration simple:
Shell to micro frontend: pass data through custom element attributes
Micro frontend to shell: dispatch a microfrontend:message DOM event
Deployment paths: resolve bundles through import.meta.env.BASE_URL, so the same demo works locally and under /examples/qwik-angular-react-rust/
Repeated navigation: cache script-load promises and guard custom element registration, so the bundles are not registered twice
That is enough for a small demo and keeps each app loosely coupled.
npm install --prefix qwik-micro-frontend
npm install --prefix angular-app
npm install --prefix react-app
npm run dev
Visit http://localhost:5173. You should see the Qwik shell with Angular and React micro frontends on the same page. Click the shell button to push a new message to both micro frontends, then click a button inside Angular or React to send a message back to the shell.
The sample also includes a small Rust WASM helper. When wasm-pack is installed, the root build emits it into qwik-micro-frontend/public/mfes/rust-wasm, and the Qwik shell imports it dynamically:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
constimportBrowserModule=newFunction('src','return import(src)',)as<T>(src: string)=>Promise<T>;importBrowserModule<RustWasm>(assetUrl('mfes/rust-wasm/rust_wasm.js')).then(async(rust)=>{awaitrust.default();window.rustWasmApi=rust;rustStats.value=rust.analyze_message(message.value);}).catch(()=>{rustStats.value='Run `npm run build:rust` from the project root to enable Rust WASM.';});
The Rust side exposes two functions:
1
2
3
4
5
6
7
8
9
10
11
12
13
#[wasm_bindgen]pubfnanalyze_message(input: &str)-> String{letchars=input.chars().count();letwords=input.split_whitespace().count();letchecksum=input.bytes().fold(0u32,|acc,byte|acc.wrapping_add(byteasu32));format!("{chars} chars - {words} words - checksum {checksum}")}#[wasm_bindgen]pubfncount_primes(limit: u32)-> u32{// Prime sieve implementation used by the shell's benchmark button.
}
analyze_message updates whenever the shared message changes. count_primes powers the “Run prime sieve in Rust WASM” button, which gives the demo a small but real CPU-bound WebAssembly task. If wasm-pack is not installed, the Rust build is skipped by default so the JavaScript micro frontends still run.
The shell is Qwik. Angular and React land as custom elements. Rust WebAssembly stays a helper inside the host. Instead of a shared Redux store, the page talks through a message attribute and a microfrontend:message event.
That is enough to keep each app independently buildable on one cohesive page. Independent production deploys are a later infrastructure step — not what this repo ships. If you are still choosing between this pattern, a monorepo, and a shared Angular library, the standalone comparison is Micro Frontends vs Monorepo vs Shared Module.