\When we started to work on microfrontend migration on one of our projects, the architecture looked great on paper (like always): one host shell, several remote apps, and teams could deploy independently on their own timelines.But in practice it wasn't so clean. One part kept getting on my nerves: actually mounting remote React components inside the host. Each microfrontend came with the same glue code. Load the remote bundle, create a React root, render the component, keep track of the mounted instance, push updated props into it when the host re-renders and clean up listeners on unmount. And do not forget to handle load failures. It wasn't especially hard code. But it was just the kind of code nobody wants to repeat.Another problem is type safety which had a habit of disappearing exactly where I wanted it most. Inside the remote, TypeScript understood the component props perfectly. But at the host boundary, that often collapsed into unknown and as any. If a remote added a required prop or renamed an existing one, the host usually did not find out from the compiler. After doing this a few times across different projects, I decided the pattern deserved a real abstraction instead of one more copy-pasted wrapper.What I WantedIt should be part of my toollkit package and should't be really hard. Something much more practical. The goal was simple:remove repetitive host-side boilerplatekeep prop types across the host/remote boundarywork with separate bundles and separate React rootsavoid shared stores, global registries, and code generationfit into an existing Module Federation setup without changing how remotes are versioned or deployedThat idea transformed to @mf-toolkit/mf-bridgeThe BaseThe package has two parts: one wrapper on the remote side, and one host component that takes care of the integration.On the remote side, you define the entry once:import { createMFEntry } from '@mf-toolkit/mf-bridge/entry'import { CheckoutWidget } from './CheckoutWidget'export const register = createMFEntry(CheckoutWidget)On the host side, you render the bridge where the remote should appear: \n import { MFBridgeLazy } from '@mf-toolkit/mf-bridge' import('checkout/entry').then(m => m.register)} props={{ orderId, userId }}fallback={}/>That’s all. With MFBridgeLazy, the host doesn’t have to deal with all the hassle of loading things on demand, setting up the root, updating stuff, cleaning up, or handling event listeners — the tool does it all. Plus, because the register function has clear types, the host can automatically figure out what props the remote component needs.If the remote component suddenly needs a new prop, you’ll see a TypeScript error right away during development, not after the app is already live and causing problems.How Prop Updates TravelThis was the part I wanted to keep as boring and predictable as possible. \n Once a remote component is mounted, it lives in its own React root. That means the host cannot simply re-render it as if it were a normal local child. The host still needs a way to send updated props into that remote tree every time its own state changes. \n There are plenty of ways to solve this: shared stores, shared context, global event buses, custom registries. I wanted the smallest possible mechanism that stayed local to each mounted microfrontend. \n So `mf-bridge` uses the one thing both sides already share: the mount element.When the host re-renders with new props, the bridge dispatches a `CustomEvent` on that specific DOM element. The remote listens to events on that same element and re-renders with the new props. That is it. \n I like this approach for a few reasons. \n First, it is naturally isolated. If you have several microfrontend slots on the same page, each one has its own mount element, so updates do not bleed across instances. \n Second, it does not need a shared module graph or global state container just to move props around. \n Third, it keeps the contract very explicit: the host owns the mount point and the props, and the remote owns how it renders them. \n Internally, the package wraps this in a small typed DOM event bus, but consumers do not really need to think about those details.Why This Helped More Than Just Saving Lines of CodeThe obvious benefit is less boilerplate. If a page has five remote slots, I no longer end up with five slightly different wrappers all doing the same lifecycle work.But the bigger benefit is moving problems earlier in the process.Before this, the host/remote boundary was often exactly where type information got blurry. That made one of the most important contracts in the system feel surprisingly fragile. A remote could evolve, and the host would not always know it had fallen out of sync.With mf-bridge, prop inference flows from the remote entry to the host usage. That changes the feedback loop. A contract mismatch becomes a compile-time problem instead of an incident report.There is also a reliability benefit in the lifecycle handling. The package takes care of the repetitive, easy-to-forget parts:lazy loading with a fallback UIclean mount and unmount behaviorprop streaming on re-renderslistener cleanuperror handling when the remote fails to loadoptional preloading and retry behavioroptional hooks for setup and teardown on the remote side when you need DI or per-mount initializationNone of these features are individually groundbreaking. The value is that they come together in one small, reusable bridge instead of being re-implemented in every host wrapper.The Cases I Wanted to Be Sure AboutWhen the basic version start to work, I spent a bit more time on some of scenarios that usually make microfrontend wrappers fragile. One of that cases was multiple instances of the same remote on a single page - widget in the main content area, a compact version in a sidebar, or the same remote mounted in a few different places. I wanted to make sure what updates stayed local to the exact mount point instead of leaking. Using the DOM element itself as the transport turned out to be a very practical way to preserve that isolation.Another important case was failed loading. I did't want the host to end up with a blank hole in the UI just because a remote bundle failed on the first attempt. That is why the bridge supports fallbacks, preloading, and retry behavior. I think that kind of thing that makes an integration feel solid. \n And sure we should not forget about what happens when the problem is rendering. If a remote drops during render, I do not want that failure to destabilize the whole host page. So error handling became part of the design too: we keep the failure contained to the mount point, surface the error to the host, and make recovery possible when new props arrive. Then there is setup and unmount - that case covered tooWhere It Fits Compared to React.lazy or PortalsThis package is not a replacement for React.lazy, and it is not trying to be cleverer than React.If your component lives in the same bundle and the same React tree, React.lazy is still the natural tool. If you just want to render into a different DOM node inside the same tree, portals are great.mf-bridge is for the awkward case those tools do not cover well: a component living across a Module Federation boundary, loaded from a separate bundle, mounted into its own React root, but still expected to behave like a first-class part of the host page.That is the gap I wanted to close.A Small Package, Not a New PlatformI also cared quite a bit about keeping the package lightweight. It has zero production dependencies and uses the browser's native CustomEvent API for prop streaming. In practice, that means less surface area, fewer moving parts, and one less utility layer to debug when something goes wrong.The goal was never to build a microfrontend platform. It was simply to remove a recurring nuisance and make the host/remote boundary feel safer.Sometimes that is enough to justify a package. I published it as @mf-toolkit/mf-bridge Repository, docs, and examples: github.com/zvitaly7/mf-toolkitIf you are working with Module Federation and you already have a small pile of hand-written wrappers around remote React components, this may save you some time. And if you have solved the same problem in a completely different way, I would genuinely be curious to compare notes. \\