Dependency Management and Global State in Federated Systems

Shared dependencies, version conflicts across remotes, error boundaries for blast radius, and why the Context API stops working across React trees.

March 21, 20264 min read5 / 6

Two problems surface almost immediately when working with module federation: what happens when shared dependencies get out of sync, and what happens to state that needs to be shared across separately-rendered React trees.

Shared Dependencies: The Singleton Problem

The right number of React instances in a federated app is one.

If each remote ships its own copy of React, users download and parse multiple copies of the same library. Two React instances in the same page also cause subtle bugs — hooks throw cryptic errors, context doesn't propagate correctly.

The singleton: true configuration in each remote's shared block tells the module federation runtime to enforce this:

TypeScript
shared: { react: { singleton: true, eager: true }, 'react-dom': { singleton: true, eager: true }, }

With singleton: true, the runtime uses the first-loaded version for everyone. This is the correct behavior 99% of the time.

Version Conflicts

The problem emerges when teams have different version requirements. Here's what it looks like in practice:

TypeScript
// Remote team's config — they want React 19+ only shared: { react: { singleton: true, requiredVersion: '>=19.0.0', }, }

If the shell is running React 18, the remote's version requirement isn't met. The module federation runtime throws an error: "version 18.3 does not meet requirement of >=19.0.0."

The remote crashes. If there's an error boundary, the failure is contained. If not, it propagates up the tree.

This is the coordination problem that doesn't go away with microfrontends — it just moves. In a monolith, a major dependency upgrade blocks everyone until the codebase is ready. In a federated system, one team can declare strict version requirements and break the integration for everyone currently on the old version.

The tooling can detect and surface these conflicts. Detecting them is only useful if there's a process for resolving them. That process is a people problem, not a technical one — and it's part of the overhead that comes with the autonomy.

Error Boundaries as Blast Radius Control

Error boundaries are important in any React app. They're essential in a federated one.

TSX
<ErrorBoundary fallback={<RemoteUnavailable name="Analytics Dashboard" />}> <Suspense fallback={<LoadingSpinner />}> <AnalyticsDashboard /> </Suspense> </ErrorBoundary>

Without an error boundary, a failing remote brings down the entire React tree. With one, the failing piece shows a fallback, and the rest of the app continues working.

The pattern I use: each remote gets its own error boundary in the shell. Failures in any one remote are contained to that remote's section of the UI. The shell, nav, and other remotes keep working.

This is the "team-scoped blast radius" benefit people talk about with microfrontends. In practice, it requires deliberately setting up error boundaries for each remote. It doesn't happen automatically.

The State Isolation Problem

Here's the one that surprised me most the first time I worked through it: the React Context API stops working the way you'd expect.

In a standard React app, you create a context at a high level, and any component in the tree can consume it:

TSX
// Standard monolith — this works function App() { return ( <AuthContext.Provider value={currentUser}> <Nav /> {/* can read AuthContext */} <Dashboard /> {/* can read AuthContext */} </AuthContext.Provider> ); }

With module federation, the shell and each remote are separate React trees. Context set up in the shell doesn't reach into the remote, because the remote has its own React instance, its own tree root.

The result: a user logged in at the shell level is "unknown" to the remote. The remote's code runs in its own context, sees no auth state, and behaves as if unauthenticated — even though the user is very much logged in in the shell.

This isn't a bug. It's the correct behavior given the architecture. The isolation that makes independent deployment possible is the same isolation that breaks context propagation.

The solution is an explicit coordination mechanism for shared state — something that lives outside any React tree and can be read by all of them. That's exactly what the communication patterns in the next post cover.

The Pattern That Contains the Complexity

The combination I've found works:

  1. singleton: true on all major shared dependencies
  2. Explicit version coordination process across teams (boring, but necessary)
  3. Error boundary per remote in the shell
  4. Global state mechanism for anything that needs to be shared across trees

None of this is technically hard. The configuration is a few lines. The error boundaries are standard React patterns. The complexity is in keeping the coordination processes working as teams and codebases evolve — which is fundamentally an organizational challenge that the technology can support but not replace.

Enjoyed this? Get more like it.

Deep dives on system design, React, web development, and personal finance — straight to your inbox. Free, always.