What you'll learn: By the end of this lesson you will be able to say what "independently deployable" demands of a micro-frontend system, name the three kinds of version skew that threaten it, read a semver range and say what it does and does not promise, explain why some libraries (React is the standard example) must exist once per page, configure sharing in Module Federation (
singleton,requiredVersion,strictVersion,eager) and predict what happens on a mismatch, compare that with import maps, and design a release process in which a shared-library upgrade does not force every team to ship on the same day.
Picture a block of flats where every tenant may redecorate, rewire their sockets and replace their oven whenever they like, with no meeting. That freedom works only because the building supplies one water main, one electrical supply and one set of pipe fittings, and every tenant's appliances fit them. If one tenant fits an appliance that needs a different voltage, the problem is not in their flat. It is in the shared supply, and every neighbour feels it.
Micro-frontends have the same shape. Several teams ship separate pieces of one page, each on its own schedule. The promise is independence. The shared supply is the set of libraries, versions and contracts the pieces have in common, and it is where the independence is usually lost. If you have not read it yet, Monolith vs Micro-Frontends covers whether to split at all, and Micro-Frontend Composition Approaches covers the ways to join the pieces. This lesson takes the pieces as given and asks what keeps them independent after they are joined.
The running example is a storefront page (a described design, not a real site). A shell app owns the header, the router and the page frame. Three team-owned pieces are loaded into it at run time: catalog, basket and checkout. All of them are React apps, each deployed from its own pipeline to a CDN. Every number in the lesson is labelled illustrative.
The core idea
A micro-frontend system is independently deployable only if every release of one piece works with every release of the others that can be live at the same moment. The words "can be live at the same moment" carry the weight. Teams release at different times, browsers keep old pages open for days, and caches serve old files after you ship new ones. So at any minute, several combinations of versions are running.
Three statements follow from that:
- Shared code is shared risk. Anything two pieces use together (a library, a design-system component, a shape of data) is a place where one team's release can break another's. Sharing saves bytes and costs independence, so share on purpose and as little as you can.
- Some libraries must exist once per page. If a library keeps internal state that every user of it has to see, two copies cannot cooperate. React is the well-known case.
- Compatibility is a contract, and it must be checked by machines. Version ranges and loader settings express a promise. They do not prove it. The release process must test the combinations that can occur.
Three illustrations run through the lesson, each labelled:
- Minimal example: a few lines that check semver ranges, plus the small block of sharing configuration they feed.
- Realistic example: the storefront upgrading React across three teams, in "In practice".
- Failure examples: two copies of React on one page, and a browser tab left open across a deployment.
What "independently deployable" asks for
Cam Jackson's article on martinfowler.com defines micro-frontends as "an architectural style where independently deliverable frontend applications are composed into a greater whole", and lists independent deployment among the benefits: each piece has its own delivery pipeline, so a team can release without waiting for others. Turn that into testable properties:
- No lockstep release. Shipping catalog does not require shipping anything else. Jackson discourages build-time integration, where each piece is published as a package that a container app depends on, because it produces a lockstep release process.
- Independent rollback. You can revert one piece without reverting the others.
- A published contract. What a piece exposes (a module, a mount function, events) and what it needs (shared libraries and their versions) is written down and tested.
Three kinds of artefact are deployed in the storefront, and each has an owner and a caching policy:
| Artefact | Owner | Changes | Caching idea (design judgement) |
|---|---|---|---|
| Shell page and its entry file | Shell team | Rarely, but touches everyone | Short-lived or revalidated, so users get new versions |
| A remote's entry file (the file that lists what it exposes) | The remote's team | Every release | Revalidated, because its content changes under a fixed URL |
| A remote's hashed chunks | The remote's team | Never, once published | Cached for a long time, because a new release gets new file names |
The last row rests on a standard HTTP practice. MDN's caching guide says that for resources that change, a common best practice is to change the URL each time the content changes, so the URL can be cached for a long period, and shows Cache-Control: max-age=31536000, immutable for such versioned files. It describes no-cache as forcing revalidation: the response may be stored but is not reused without checking with the server. The lesson HTTP Caching explains both.
Figure 1. The storefront: a shell, three remotes and a share scope
How to read this: the shell puts the libraries it owns into the share scope. Each remote asks the scope for them instead of bringing its own copy, and every remote file is fetched from the CDN. The CDN files and the share scope are the two places where independent releases meet.
Why share at all
The simple alternative is to share nothing: each piece bundles its own React, router and date library. That gives the strongest independence, and it has two costs.
Download size. Every piece repeats the same library, so five pieces mean five copies.
Libraries that are not meant to be duplicated. The React documentation on invalid hook calls explains that, for hooks to work, the react import in your application code must resolve to the same module as the react import inside react-dom. If the imports resolve to two different exports objects, you get the warning, which can happen when you accidentally end up with two copies of the package. The same page then adds an important clarification: React supports using multiple independent copies on one page (an app and a third-party widget that both use it, for instance). It breaks only when a component and the react-dom copy it is rendered with disagree about which copy they use.
So duplication of React is not forbidden. Mixing copies is. A piece that brings its own matched react and react-dom pair and renders into its own container can work. What fails is a piece that renders with the shell's react-dom but imports a different react. Sharing is how you avoid that class of mistake, at the price of agreeing on versions.
Other libraries have the same property for other reasons: a router that stores its location, a state library holding one store, a design-system package that creates a theme context. If two pieces each bundle their own copy, each copy has its own module-level state, so one copy never sees what the other stored. Part of the lesson Cross-App Communication in Micro-Frontends shows exactly this effect in a test.
Version skew: three ways the pieces drift apart
Version skew means that two parts of a system that must agree are running different versions. The Next.js self-hosting documentation names the version for its own case: with rolling deployments or several instances, a client may request JavaScript or CSS files that no longer exist on the server, call a server function by an ID from a previous build, or navigate with prefetched data from an old deployment. Micro-frontends have the same disease with more patients.
| Kind of skew | What disagrees | Typical symptom | Who sees it |
|---|---|---|---|
| Contract skew | A remote's exposed module or mount function versus what the shell expects | "is not a function", missing props, a blank region | Users of that one region |
| Dependency skew | The library range a remote was built for versus the version the page provides | A warning in the console, an error on a missing API, or hooks failing | Users of the remote that needs the other version |
| Asset skew | A page opened before a deployment versus files that exist after it | Failed chunk loads, a region that never appears | Users who kept a tab open |
Contract skew is a normal interface-versioning problem. Treat what a remote exposes the way a server team treats a public API: add things, do not rename or remove them without an overlap period. Dependency skew is the main subject of the next sections. Asset skew comes from deleting old files while old pages still ask for them.
Figure 2. A tab left open across a deployment (asset skew)
How to read this: the entry file was fetched in the morning and still names the old chunk. The cure is on the CDN side: keep old hashed chunks for as long as old pages may be open, and delete them on a schedule rather than on deploy. The Next.js documentation says the same for immutable assets: if you keep adding them to the existing static folder or CDN bucket, there is no version skew without tracking deployment IDs.
Semver: what a version range promises
Dependency skew is expressed in semantic versioning (semver) ranges, so you need to read them exactly.
The semver specification says a version number is MAJOR.MINOR.PATCH. You raise MAJOR for incompatible public API changes, MINOR for compatible additions and PATCH for compatible fixes. Two rules matter for shared libraries: major version zero (0.y.z) is for initial development, where "anything MAY change at any time", and a released version's contents must not be modified, so a fix ships as a new version. A pre-release such as 1.0.0-rc.1 has lower precedence than 1.0.0.
Ranges are the notation a dependency uses to say which versions it accepts. npm's documentation lists the common forms: ~1.0.4 for patch releases, ^1.0.4 for minor releases and * or x for any version. The node-semver README gives the exact meaning of the caret: it allows changes that do not modify the left-most non-zero element of [major, minor, patch], so ^1.2.3 means >=1.2.3 <2.0.0-0 and ^0.2.3 means >=0.2.3 <0.3.0-0. A tilde allows patch-level changes when a minor version is given. A pre-release version satisfies a range only if a comparator in the range has the same [major, minor, patch] tuple and also carries a pre-release tag.
Here is the first illustration. The shell offers one version of React, and each team states the range it was built against. The script uses the node-semver package (7.8.5), and the output below is what it printed on Node 26.12.1 on 2026-11-08.
const semver = require("semver");
const offered = "18.3.1"; // illustrative: what the shell provides
const wants = { catalog: "^18.2.0", basket: "^18.0.0", checkout: "^19.0.0", legacy: "^17.0.2" };
console.log(`shell offers react ${offered}`);
for (const [team, range] of Object.entries(wants)) {
const ok = semver.satisfies(offered, range);
console.log(`${team.padEnd(9)} wants ${range.padEnd(8)} -> ${ok ? "satisfied" : "NOT satisfied"}`);
}
console.log(semver.intersects("^18.2.0", "^19.0.0"));
console.log(semver.intersects("^18.2.0 || ^19.0.0", "^19.0.0"));
Output:
shell offers react 18.3.1
catalog wants ^18.2.0 -> satisfied
basket wants ^18.0.0 -> satisfied
checkout wants ^19.0.0 -> NOT satisfied
legacy wants ^17.0.2 -> NOT satisfied
false
true
Two ranges overlap if some version satisfies both (intersects). ^18.2.0 and ^19.0.0 do not, so no single copy of React can serve catalog and checkout at once. A range written as ^18.2.0 || ^19.0.0 overlaps both, which is how a team crosses a major version without a flag day, if the code really works on both.
A range is a promise made by a library's maintainers, not a test result. Satisfying ^18.2.0 says the maintainers claim no incompatible changes inside 18.x. It does not say your code, your other libraries or your bundler configuration work with the particular 18.x on the page. Ranges narrow the search, and tests decide.
How Module Federation shares libraries
Module Federation is a bundler feature, described on webpack's concepts page as letting multiple separate builds form a single application: each build acts as a container that can expose code and consume code from other builds. (The page notes it is often known as micro-frontends but is not limited to that.) The lesson Module Federation in React covers the setup of a host and a remote. This lesson covers only the sharing part.
Vocabulary from the webpack documentation. A container is created through a container entry (usually a remote's remoteEntry.js) and exposes asynchronous access to its modules through get and init methods. init is called with the share scope, an object filled with the modules provided by the host. A shared module is one a build both provides into the scope and consumes from it, instead of bundling a private copy, with its own local copy as the fallback.
Figure 3. Loading a remote and negotiating React
How to read this: the remote does not import the shell's React directly. It asks the scope, and the scope applies the remote's declared range. What happens next depends on the options in the next figure.
The options that decide a mismatch
The webpack documentation lists the sharing hints. These are the ones that matter here:
| Option | What it does (as documented) | Default |
|---|---|---|
singleton |
Allow only one version of the module in the scope; with several versions present, the highest semantic version is used. Meant for libraries with global internal state, "react, react-dom" being the examples. | off |
requiredVersion |
The range this build needs, a semver range such as ^1.2.3; false accepts whatever the scope offers. |
from package.json |
strictVersion |
Reject the shared module if the version is not valid; throws a runtime error if the required version is not found. | true when a local fallback exists and the module is not a singleton, otherwise false |
eager |
Include the module in the initial chunk instead of fetching it asynchronously; all provided and fallback modules are then always downloaded. | off |
shareScope |
The name of the scope. Both the providing and the consuming side must name the same one. | 'default' |
import |
The module placed in the scope, which also acts as the fallback. false means no fallback is bundled. |
the property name |
The documentation gives these worked mismatch cases for a shared vue: with singleton: true and requiredVersion: "^2.6.5", the shared version is always used but a warning is printed when it is outside the range; with import: false and the same range, a warning is emitted; and with import: false, the same range and strictVersion: true, an error is thrown. The module-federation.io page for shared words the singleton rule as "the higher version wins; the party with the lower version receives a warning".
Figure 4. What happens when the provided version does not satisfy the range
satisfies requiredVersion?"} -->|"yes"| U["Use the shared copy"] A -->|"no"| B{"singleton: true?"} B -->|"yes"| C{"strictVersion: true?"} C -->|"yes"| E["Runtime error"] C -->|"no"| W["Use the shared copy
and warn"] B -->|"no"| D{"Local fallback
bundled?"} D -->|"yes"| F["Use the remote's own copy
(two copies on the page)"] D -->|"no"| G{"strictVersion: true?"} G -->|"yes"| E G -->|"no"| W2["Warn and use
what the scope has"]
How to read this: it follows the webpack option descriptions and worked cases above. The cheapest outcome to ignore is the warning, and the warning path is exactly the one where a remote runs on a library version it was not built for. The two documentation sites word some details differently, for example the module-federation.io page says that a version below requiredVersion produces a console warning and the smallest available shared version is used, so test your own bundler version before you rely on a path.
A configuration sketch
This is the shape of the shell's and a remote's sharing block, following the option names in the webpack documentation. It was not run for this lesson, and the ports and names are illustrative.
// shell: provides react and react-dom, and treats them as singletons
new ModuleFederationPlugin({
name: "shell",
remotes: {
checkout: "checkout@https://cdn.example.com/checkout/remoteEntry.js",
},
shared: {
react: { singleton: true, requiredVersion: "^18.3.1", strictVersion: true },
"react-dom": { singleton: true, requiredVersion: "^18.3.1", strictVersion: true },
},
});
// checkout remote: states the range it was built and tested against
new ModuleFederationPlugin({
name: "checkout",
filename: "remoteEntry.js",
exposes: { "./Checkout": "./src/Checkout" },
shared: {
react: { singleton: true, requiredVersion: "^18.2.0 || ^19.0.0", strictVersion: true },
"react-dom": { singleton: true, requiredVersion: "^18.2.0 || ^19.0.0", strictVersion: true },
},
});
The remote entry URL is a deployment decision as much as a code one: a static URL means "whatever the checkout team last published at this address". The webpack documentation says dynamic remotes, whose location is not known at build time, are loaded by hand. The Module Federation 2.0 announcement describes a runtime separated from the bundler and a generated manifest file (mf-manifest.json) that lists the remote entry, shared dependencies and exposes, and says it supports fine-grained version and gray-release management. That is the documented basis for the "pointer" design discussed below.
The async boundary
One failure is so common that webpack's troubleshooting section lists it by message: "Shared module is not available for eager consumption". It happens when the application's entry file imports a shared library synchronously before the share scope has been set up. The documented fix, which the page strongly recommends, is an asynchronous boundary:
// index.js
import("./bootstrap");
// bootstrap.js
import { createRoot } from "react-dom/client";
import App from "./App";
createRoot(document.getElementById("root")).render(<App />);
The alternative is eager: true on the shared module, which the same page says works but can have drawbacks, because all provided and fallback modules are always downloaded; it recommends providing eagerly at only one point, such as the shell.
Import maps: the standards-based alternative
An import map is a JSON object, declared in <script type="importmap">, that controls how the browser resolves module specifiers in import statements and import() calls. MDN describes its imports key (bare names or prefixes mapped to URLs) and its scopes key (mappings used only when the importing script's URL falls under a given path). Baseline status on MDN is "widely available", across browsers since March 2023.
<script type="importmap">
{
"imports": {
"react": "https://cdn.example.com/vendor/react-18.3.1.js",
"react-dom/client": "https://cdn.example.com/vendor/react-dom-client-18.3.1.js"
},
"scopes": {
"/legacy-admin/": {
"react": "https://cdn.example.com/vendor/react-17.0.2.js"
}
}
}
</script>
Every module that imports the bare name react is resolved to the URL in the map, so all pieces of the page that use it end up asking for the same file. A scope lets one path prefix use a different mapping, here the legacy admin pieces on another version. The URLs and file names above are illustrative.
Rules from MDN that shape the design:
- The map must be declared and processed before any module script that imports through it. Several import maps can be declared, and they are merged; a specifier that already resolved keeps its earlier result.
- It applies to module specifiers in documents. It does not apply to the
srcof a<script>element or to modules loaded into workers or worklets. - It has an
integritykey for integrity metadata on mapped modules. - The HTML Standard states that a bare specifier that is not remapped to anything fails with a
TypeError.
What import maps do not do. They have no version negotiation. A map says "react is this URL"; nothing compares a range with a version, warns about a mismatch or lets two teams express different needs. Someone owns the map and picks the version. That is a clean fit for a platform decision (the shell team publishes the one React for the page) and a poor fit for teams who want to discover compatibility at run time.
| Approach | Version rule | One copy on the page? | Failure when versions disagree | Cost |
|---|---|---|---|---|
| Bundle privately | None needed | No, one per piece | Mixed copies if a piece renders with another's react-dom |
Bytes, plus no shared state between copies |
Module Federation singleton |
Ranges negotiated in the share scope | Yes | Warning or error by the options above | Bundler coupling, runtime negotiation to understand |
| Import map | One URL per specifier, chosen by the map's owner | Same file requested by every importer | The wrong version for some piece, with no automatic warning | A central owner, and every piece must be built to leave react external |
| Iframes | Each frame has its own page | No, one per frame | None between frames, because nothing is shared | Bytes, layout and routing limits (Jackson: isolation but limited flexibility) |
Designing so that releases stay independent
Collect the mechanisms into a few working rules. These are design judgements built on the sources above, not requirements of any tool.
- Share a short list. Share what must be single (React, the router, a global store, a design-system provider) and what is large and common. Everything else is bundled privately. Each shared package is a clause in the contract.
- Give one owner to each shared version. The shell (or a platform team) decides it, and every remote declares a range around it. A remote must not pin an exact version unless it truly needs one.
- Cross a major version with an overlap window. First, make every remote declare a range that includes both the old and the new major, and test both. Then switch the shell. Then remove the old half of the range. Each step is an ordinary, independent release.
- Make skew visible. A warning that nobody reads is not a control. Use
strictVersionwhere a wrong version would break users, and report the mismatch to your monitoring where it would only degrade them. Observability and SLOs covers how to turn that into an alert. - Test the combinations that can occur. Run each remote's tests against the shell version currently in production and the one about to be released, and the shell's tests against each remote's current production build. This is the contract test Jackson recommends in his advice on verifying integrations.
- Keep old chunks, revalidate entries. Hashed chunks stay on the CDN for as long as old pages may need them. Entry files are revalidated, so new visitors get the new release at once.
- Release a pointer, not a file. If the shell reads a small manifest that says which URL each remote's current release lives at, rollback is a one-line change to the manifest, with the old files still in place. The manifest itself must be small, revalidated and have an owner.
Figure 5. What to share
every user of it must see?
(React, router, store, theme)"} -->|"yes"| S["Share as a singleton,
shell owns the version"] Q1 -->|"no"| Q2{"Large and used by
several pieces?"} Q2 -->|"yes"| R["Share with a version range,
fallback allowed"] Q2 -->|"no"| P["Bundle privately in each piece"]
How to read this: the first question is about correctness and the second is about bytes. Correctness forces sharing. Bytes only suggest it, and a private copy keeps the piece independent.
Trade-offs
| Decision | You gain | You pay |
|---|---|---|
| Share more libraries | Smaller downloads, one copy of stateful code | More clauses in the contract, more skew surface, upgrades need coordination |
| Share fewer libraries | Strong independence, simple releases | Larger downloads, duplicated code |
| Wide version ranges | Teams can upgrade on their own schedule | A range you have not tested is an untested claim |
strictVersion on |
Mismatch fails loudly, close to the cause | A failed region instead of a degraded one |
| Dynamic remote pointer | Fast rollback, staged releases | One more small service that must be correct and available |
In practice: upgrading React across three storefront teams
This is a realistic scenario, not a case history. It is a composite built from common patterns. It describes no specific company or product, and every number in it is an illustrative assumption.
The situation
The storefront runs React 18.3.1 in the shell. Catalog, basket and checkout are federated remotes owned by three teams, each releasing about weekly (illustrative). The platform team wants React 19 in the shell for a feature checkout needs. The three teams have different priorities: checkout is ready next week, catalog has a freeze for a sale, and basket depends on a third-party widget that has not declared support for the new major.
Starting architecture
Figure 6. React 18 in the shell, three remotes with different ranges
How to read this: the warning-coloured box asks for a React major the shell does not provide. Under a lenient singleton configuration, that mismatch would be logged and ignored, and checkout would run on React 18.
Options considered
| Option | What it fixes | What it costs |
|---|---|---|
| Everyone moves to 19 the same day | One clean state | A coordinated release; a freeze or a failing widget blocks everyone |
| Checkout bundles its own React 19 | Unblocks checkout now | Two Reacts on the page; checkout must never render with the shell's react-dom |
| Overlap window with ranges including both majors | Each team moves when ready | Every remote must pass tests on both versions for a while |
| Stay on 18 until every team is ready | No risk now | Checkout's feature waits |
The decision
| Field | Decision record |
|---|---|
| Context | Three teams on different schedules; one needs a newer React major soon; a third-party widget in basket is unverified on it. |
| Decision | Run the overlap window. Step 1: each remote widens its range to ^18.2.0 || ^19.0.0 only after its own tests pass on both, and ships independently. Step 2: the platform team switches the shell to React 19 on a staged rollout. Step 3: remotes narrow their ranges when the old major is gone. Keep strictVersion on in the shell so an unsupported remote fails visibly. |
| Consequences, good | No flag day; any step can be rolled back by moving a manifest pointer; each team's testing is bounded. |
| Consequences, bad | Two test runs per remote for the window; basket may stay on its old range longer than planned. |
| Revisit when | Basket's widget cannot support the new major by the agreed date. Then load basket in an iframe or give it its own matched React pair and accept the extra bytes. |
Rollout and verification
- CI matrix. Each remote's pipeline runs its tests against the shell's current production React and the candidate one. A remote that fails one cell does not widen its range.
- Staged shell release. Release the shell on React 19 to a small share of sessions (illustrative: 5 percent) with remote manifests unchanged, and compare error rates per region between the two groups.
- Watch skew signals. Count version-mismatch warnings and failed remote loads per remote. A rising number in one region points to a contract or range problem.
- Keep the rollback real. Rehearse the manifest rollback for one remote and for the shell before the staged release, and confirm old chunks are still on the CDN.
- Narrow ranges last. Only after the old major has been gone for an agreed period.
The 2 a.m. failure
At 02:10 the checkout error rate climbs for new sessions, while catalog and basket look normal. Checkout had been released before the shell switch with requiredVersion: "^19.0.0" and strictVersion left off, and the rollout schedule slipped, so the shell still provided 18.3.1. Under a lenient singleton configuration the mismatch is a console warning, so the build and the release passed, and checkout runs on a React version it was not built for. Code that calls an API only the newer major has fails in the browser.
First three checks: (1) compare the React version actually loaded on the page with the range in checkout's entry; (2) look for a version warning in the console of an affected session and in your client error reports; (3) check which release of the shell and checkout are live together. Mitigation: move checkout's manifest pointer back to its previous release, which declares the wider range, and confirm the error rate falls. Follow-up: turn strictVersion on for singletons so a mismatch fails at load with a clear message and a fallback region, add the CI matrix cell for the shell's current production version, and gate remote releases on it.
Architect's notes
- Decide who owns each shared version before the second team joins. Ownership is the cheapest control and the hardest one to retrofit.
- Keep the shared list short and in one file. If it is hard to say which libraries are shared, the contract is already unclear.
- Prefer failing loudly for singletons. A silent wrong-version run is harder to find than a failed region with a fallback.
- Treat a range as a claim that needs a test. Widen it only after a test passes on both ends.
- Plan for old pages. Retention of old chunks and short-lived entry files are part of the deployment design, not storage housekeeping.
Common mistakes
- Treating
^as a guarantee. It is a maintainers' promise; run the tests. - Using a range such as
^19.0.0in a remote and leaving the shell on 18 with warnings off. The mismatch becomes a runtime surprise. - Pinning exact versions in remotes so that every shell upgrade forces every remote to ship.
- Sharing everything. Each extra shared package is another place where skew can occur.
- Deleting old hashed chunks at deploy time, which breaks tabs that were open before the release.
- Caching a remote's entry file for a year under a fixed URL, so users never see new releases.
- Assuming import maps negotiate versions. They resolve names to URLs, and that is all.
Key takeaways
- Independent deployment means every combination of releases that can be live together works. Shared code is where it breaks.
- Skew comes in three kinds: contract, dependency and asset. Each has a different cure.
- Some libraries (React is the standard example) must be a single copy per page for hooks to work. Multiple matched, independent copies are allowed, but mixing them is not.
- A semver range is a promise, not a proof.
^1.2.3means>=1.2.3 <2.0.0-0, and^0.2.3stays inside0.2.x. - In Module Federation,
singleton,requiredVersion,strictVersion,eagerandimportdecide what happens at a mismatch, and the lenient defaults can hide it behind a warning. - Use an async boundary, not
eager, to avoid eager-consumption errors. - Import maps resolve names to URLs in the browser. They give one visible list and no negotiation.
- Cross a major version with an overlap window of widened ranges, tested on both versions.
- Keep hashed chunks, revalidate entry files and make rollback a pointer change.
Practice exercises
Exercise 1 — Starter (30 min): Use node-semver or the semver calculator of your choice to check which of these versions satisfy ^1.4.2, ~1.4.2, ^0.4.2 and >=1 <3: 1.4.9, 1.5.0, 2.0.0, 0.4.9, 0.5.0, 1.5.0-beta.1. Write the result as a table and explain each cell in one sentence.
Exercise 2 — Intermediate (2 hours): Write the sharing block for a shell and two remotes that share react, react-dom and a design-system package, with the shell owning the versions. For each shared package, decide singleton, requiredVersion and strictVersion, and write one sentence on what a mismatch should do for users. Then build a one-page "skew matrix" listing every shell and remote version pair that can be live during an upgrade.
Exercise 3 — Advanced (half day): In a scratch project with a Module Federation shell and one remote (the lesson Module Federation in React covers the setup), set the remote's requiredVersion outside the shell's version with strictVersion off and then on. Record what the console and the page show in each case. Check whether your bundler version matches the behaviour the documentation describes, and write down any difference.
Related reading
In this course:
- Monolith vs Micro-Frontends, whether to split at all and where to cut.
- Micro-Frontend Composition Approaches, the ways to join the pieces.
- Cross-App Communication in Micro-Frontends, the contract between pieces once they share a page.
- HTTP Caching, the headers behind entry files and hashed chunks.
- Code Splitting, Lazy Loading and Tree Shaking, how chunks are produced.
Elsewhere on the site:
- System Design: Observability and SLOs, turning skew signals into alerts.
Official documentation
- webpack: ModuleFederationPlugin, the sharing hints, remotes and exposes.
- webpack: Module Federation concepts, containers, the share scope, dynamic remotes and troubleshooting.
- Module Federation: shared configuration and its 2.0 announcement.
- MDN:
<script type="importmap">and the HTML Standard: import maps. - Semantic Versioning 2.0.0 and the node-semver README for range grammar.
- React: invalid hook call warning, MDN: HTTP caching, Next.js: self-hosting, version skew and Martin Fowler: Micro Frontends.