Module Federation, and the singleton problem
Runtime remote module loading gives you shared dependencies across separately deployed builds — and the moment your component’s react and its renderer’s react resolve to different module instances, the hooks in your graft break in a way that reads as your bug, not a config one.
The host already builds with webpack 5 and already exposes two remotes, so for once the seam is documented and nobody has to be persuaded of anything. You publish the review gate as a remote, the host consumes it, it renders. Two commits later you add a useState to it and the page dies with Invalid hook call. Hooks can only be called inside of the body of a function component.
Nothing about that message points at a build config. It names your component, it appears in your file, and every suggestion attached to it is about how you wrote the component. You will spend an afternoon rewriting code that was already correct. This lesson exists so that you spend ten minutes reading a config instead.
What Module Federation actually gives you
In the project’s own words, it is a way to share “code and resources among multiple JavaScript applications (or micro-frontends)”. Two capabilities are bundled inside that sentence and they are worth separating, because only one of them is unusual.
- Runtime remote loading. The host fetches your module at run time from a URL you deploy to, rather than at build time from a package it installed. You ship, they get it, nobody bumps a version in a
package.json. - Dependency sharing. Host and remote negotiate, at run time, which copy of a shared package everybody uses. This is the part a script tag cannot do, and it is the entire reason to reach for this over the simpler mechanisms.
Independent deployment you can get from an ordinary bundle on a CDN. Dependency sharing is what you are actually buying, and the price is that both sides now build in a way that assumes the other exists.
The sentence that explains the crash
The shared-configuration page states the failure without hedging: without singleton, “if the shared dependencies between the producer and the consumer have different versions, each side will load its own dependencies.” Both halves matter. It is not that sharing fails loudly; it is that the negotiation quietly concludes that no shared copy satisfies both, and each side falls back to the copy it built with. Your remote runs against its React. The host runs against the host’s React. Nothing throws at load time.
The fix is one field, and this is what it looks like on the host side of a webpack or rspack config:
new ModuleFederationPlugin({
shared: { react: { singleton: true, requiredVersion: '^19.0.0' } }
})Mark react and react-dom as singletons on both sides. A singleton declared only by the consumer is not a negotiation, it is a preference, and the producer that never declared one is still free to load its own.
Where people get burned
Module Federation is the project’s own documentation of the project’s own product. It is the correct place to learn the mechanism and the wrong place to learn whether you should be using the mechanism. Nothing on that site is going to tell you that a plain custom-element bundle would have solved your problem without asking the host to change how they build.
Now the part almost every write-up gets wrong
The usual explanation is that React is a singleton, or has to be one, or breaks when it is not. React’s own documentation says the opposite.
“In general, React supports using multiple independent copies on one page (for example, if an app and a third-party widget both use it). It only breaks if
require('react')resolves differently between the component and thereact-domcopy it was rendered with.”
That is from react.dev’s own page on the invalid hook call warning, which is the page the error message links to. Read the condition precisely, because it is narrower than the folklore and the narrowness is what makes it debuggable:
- Two copies of React on a page: fine. Your remote renders its own tree with its own
react-dom, against its ownreact. The host renders its tree with its pair. Two self-consistent worlds, no error, at the cost of shipping React twice. - One render, two module instances: broken. A component whose
reactimport is instance A, rendered by areact-domholding instance B, calls a hook. The hook reads its dispatcher from instance A, which was never set, because the renderer set the dispatcher on instance B. It throws.
So the diagnostic question is not “how many copies of React are on this page.” Counting copies tells you almost nothing. The question is which react the react-dom that rendered this tree is holding, and whether it is the same object the component imported.
Where this course lands on it
Module Federation solves a real problem well and it is almost never available to a tenant. That is this course’s own verdict, not a finding from anybody’s documentation, and it follows from one fact: both sides have to configure it. A host that has not already adopted it has to change its build, its deploy and its boot sequence so that your one surface can arrive. You already know from the delivery-constraints lesson how that conversation goes.
When it is available, take it, because the host has already paid the cost and the seam is documented. Read their existing shared block before you write yours, match their singleton declarations exactly, and treat any package you both use as a negotiation rather than an install.
Check your recall
Answer from memory — no scrolling back.
Retrieval check
A colleague says “the whole problem is that React is not a singleton.” Correct them in two sentences, using React’s own documented condition.
Check your answer
React is explicit that multiple independent copies on one page are supported in general, and names an app plus a third-party widget as the ordinary case. What breaks is narrower: a single render whose component code and whose react-dom resolve to different react module instances, because the dispatcher a hook reads was installed on the other instance.
The distinction is not pedantry. “React must be a singleton” sends you to deduplicate a dependency graph across two teams. “This tree’s renderer is holding a different React than this component imported” sends you to one shared block, and it also tells you that a surface with its own matched pair behind a boundary was never at risk.
Hands on
Reproduce the singleton failure on purpose, once
Done when: ARTIFACT.md records the deliberate reproduction: the config that caused it, the exact error text, and one sentence naming which react-dom held which react. A screenshot of the error alone does not count — the sentence is the artefact.
- Stand up a minimal host and a minimal remote with
ModuleFederationPluginon both sides. The remote exposes one component that calls a singleuseState. Keep it small enough that nothing else can be blamed. - Share
reactandreact-domwithsingleton: trueon both sides, matching versions. Confirm it renders and the state updates. This is your control. - Now remove
singletonfrom the remote only, and give the two sides different React versions so the negotiation cannot resolve. Rebuild and load the host. Copy the error text verbatim intolearning/grafting-ui/ARTIFACT.md. - Before fixing it, prove the diagnosis rather than assuming it. In the browser console, get a handle on both React exports and compare them with
===, or log the module ids the two entries resolved. The goal is a moment where you have watched two objects that should be one object fail an identity check. - Write one sentence in your own words: which
react-domrendered the failing tree, and whichreactthe component imported. If the sentence contains the phrase “not a singleton” and nothing else, it is not yet the diagnosis. - Restore the singleton on both sides and confirm the error is gone. Then note in
ARTIFACT.mdwhether your real host has asharedblock at all, because if it does not, this whole mechanism is unavailable to you and the note is the finding.
What this does not cover
Loading a module and running an application are different problems, and conflating them is how a team ends up with a router of routers it did not need. The orchestration lesson takes single-spa on its own terms: what a root config actually does, why registering applications is not the same job as fetching modules, and what the mount and unmount lifecycle quietly makes you responsible for.
Getting a React tree into a host without any of this machinery — one bundle, its own root, no build coupling — is the Web Components module’s subject, and the lesson on putting React inside a custom element is where the second-copy trade-off gets priced properly. And whether a federated remote can be fetched at all in a given host is decided by a response header rather than by either build: the lesson on how CSP narrows the table closes this module with it.
Read this next — primary source
Shared configurationmodule-federation.io — fetched 2026-09-05. The Module Federation project documenting its own product; it is describing an architecture it wants you to adopt.
This lesson takes one sentence from it — what happens to a shared dependency when nobody marks it a singleton — and spends the rest of the time on what that sentence does not say. Read the whole page anyway, because the fields around that sentence are the ones a host team will ask you about in review: eager and what it costs an entry bundle, version and requiredVersion, and the difference between sharing a package and sharing a specific import path. It is the vendor writing about its own product, so read it for mechanism and take its enthusiasm as marketing.
Stuck, curious, or think this lesson is wrong? Ask your teaching agent. The lessons are the scaffold; the conversation is where the learning gets unstuck.