Skip to content

Global Events Across Different Frameworks

Global events do not automatically decouple microfrontends. Browser-native events can be useful as a transport mechanism for a small number of transient, framework-neutral platform intents. As soon as remotes use them to distribute domain events, synchronize state, or control the reactions of other remotes, however, they create hidden horizontal coupling.

A remote may ask the platform for a global presentation or technical reaction. It should not use a global event bus to coordinate the domain logic of other remotes.

Global Events May Coordinate the Platform—Not the Domain Architecture

Section titled “Global Events May Coordinate the Platform—Not the Domain Architecture”

Consider an application with an Angular host, an Angular remote, and a React remote. Both remotes can modify data. After a successful save operation, the product should display a global success notification.

The notification system belongs to the host. It owns the global presentation, knows the theme, accounts for focus management and accessibility, and decides where the message appears and how long it remains visible. The React remote cannot use that Angular component. It should not know an Angular service, a host store, or an NgRx dispatcher either.

The same applies to the Angular remote. Accessing Angular internals of the host may be technically easier for it, but that does not mean it should do so. The fact that both sides use the same framework is merely a technical circumstance. The platform contract remains an application boundary.

Both remotes only need to express what the user should be told:

type NotificationSeverity = 'success' | 'info' | 'warning' | 'error';
interface NotificationIntent {
severity: NotificationSeverity;
message: string;
}

This contract describes neither an Angular component nor a React component. It contains neither a store nor a service. It merely says that the platform should inform the user with a message of a particular severity.

This is the first important distinction: NotificationIntent describes the semantic meaning of the message. It says nothing yet about how that message crosses the technical boundary or how the host processes it.

Intent, Transport, and Processing Are Three Different Layers

Section titled “Intent, Transport, and Processing Are Three Different Layers”

Framework-spanning communication often mixes three separate questions:

  1. What does the message mean?
  2. How does it cross the application boundary?
  3. What does the receiver do with it internally?

In the notification example, the meaning is that the user should be informed about a successful operation. The transport may be a direct function call or a CustomEvent on an EventTarget. Behind the boundary, the Angular host may call a service, update a signal, enqueue a notification, modify a Signal Store, or dispatch an internal NgRx event.

These decisions do not belong in the same contract.

The intent is not the browser event. The browser event is merely one possible transport mechanism for the intent. Likewise, an internal NgRx event is not the framework-spanning contract. It is an implementation decision inside the host.

The platform contract ends at the host adapter. The host’s internal architecture begins behind it.

The host can provide a small, framework-neutral platform context:

interface PlatformNotifications {
show(intent: NotificationIntent): void;
}
interface PlatformContext {
notifications: PlatformNotifications;
}

The Angular remote uses this contract:

platform.notifications.show({
severity: 'success',
message: 'The planning data was saved.',
});

The React remote uses the same contract:

platform.notifications.show({
severity: 'success',
message: 'Changes saved.',
});

Both applications see the same platform capability. Neither knows the host’s concrete notification component. The Angular remote injects no host service, modifies no foreign store, and imports no internal event creator. The React remote knows neither Angular dependency injection nor NgRx.

The host merely supplies the adapter:

const platformContext: PlatformContext = {
notifications: {
show: (intent) => {
notificationAdapter.show(intent);
},
},
};

The adapter translates the stable platform contract into the current internal implementation. Today, it may call a notification service. Later, it may introduce a queue or translate the intent into a host-internal event. As long as the external contract remains stable, the remotes do not have to follow that change.

NgRx, signals, or an internal event dispatcher are not the problem. The problem would be turning them into the integration contract beyond the host boundary.

A remote does not send an NgRx event to Angular. It expresses a framework-neutral intent that the Angular host translates into its own architecture.

An Angular remote and a React remote pass the same notification intent through a framework-neutral platform contract to an Angular host adapter, which translates it into internal host mechanisms and the global notification UI.

A Direct Platform Contract Is Often the Simpler Path

Section titled “A Direct Platform Contract Is Often the Simpler Path”

When the host mounts a remote itself, it already has a direct technical relationship with that application. It can pass the platform context during mounting:

interface RemoteApplication {
mount(container: HTMLElement, context: PlatformContext): void;
unmount(): void;
}

The host can then start the React remote like this:

reactRemote.mount(container, platformContext);

This is neither an Angular mechanism nor a React mechanism. It is an ordinary JavaScript contract between two applications.

This variant has a visible owner and one expected receiver. The call can be typed, is easy to test, and is bound to the remote’s lifecycle. There is no global search for listeners and no unknown number of possible reactions.

A direct platform contract is therefore often easier to understand when the host can provide the platform context anyway. Communication across framework boundaries does not automatically require browser-native events.

That does not make the function call universally superior. It merely describes a different form of technical addressing. A callback can transport a poor domain contract as well. Conversely, a browser event can transport a clearly bounded platform intent.

A callback and a CustomEvent are different pipes for the same message. Neither pipe determines whether the message is architecturally sound.

When a Browser-Native Event Can Make Sense

Section titled “When a Browser-Native Event Can Make Sense”

Not every integration has a direct mounting relationship. Applications may load independently. The platform context may not be directly available, or a deliberately defined technical channel may be part of the product platform.

A browser-native event can then be an appropriate transport mechanism:

platformEvents.dispatchEvent(
new CustomEvent('platform:notification-requested', {
detail: {
severity: 'success',
message: 'Changes saved.',
},
}),
);

The Angular host registers a listener on this channel and then passes the payload to its adapter. The internal Angular architecture begins only behind that adapter.

The channel must remain bounded. A product-specific EventTarget provided through the platform context is easier to control than an unrestricted event bus on window:

interface PlatformContext {
events: EventTarget;
}

Global for the product does not have to mean global on window.

An EventTarget can be useful when no direct reference should exist or when several explicitly intended technical observers are required. It does not solve independent loading times, however: anyone who is not registered when the event is dispatched misses the message. It is therefore not automatically the better solution merely because Angular and React are involved.

The involvement of different frameworks only establishes the need for a framework-neutral contract. It does not prescribe how that contract must be transported.

The technical class CustomEvent says nothing about the architectural meaning of its payload. It can transport an event, an intent, a state change, or a problematic control command.

A concise semantic distinction helps:

Event
└── something has happened
Intent
└── the platform should do something
State
└── something is currently true

OrderCreated describes a completed domain fact. NotificationRequested asks the platform to present something. CurrentLocale describes the state that currently applies.

These terms do not need to be resolved academically in every edge case. What matters is not confusing the transport with the meaning. A CustomEvent called refreshRemoteB remains a problematic remote-to-remote command. A direct function call, by contrast, may pass a legitimate platform intent.

The browser API does not classify the architecture. What matters is what the message means, who owns the reaction, and who becomes dependent on its occurrence.

Global notifications are a useful example because they have one clear UI owner. The host controls the shared product interface. It can handle presentation, theme, positioning, focus, and accessibility consistently.

The intent is also transient. A remote that starts later does not need to reconstruct an earlier success message. The notification is not an authoritative domain-state source. The sending remote normally expects no response, and no other remote needs to react to the message in domain terms.

These properties limit the dependency:

  • one clear owner in the host
  • a simple, framework-neutral payload
  • a short-lived effect
  • no domain reaction chain between remotes
  • no claim to represent durable current state

Similar platform capabilities may include global help, navigation, a Command Palette, or telemetry. Even there, the platform should still ask whether a direct function is easier to understand than an event. The global channel is not a collection point for everything that affects more than one application.

With transient notifications, it does not matter if a remote loaded later never saw an earlier message. Platform state is different.

An event such as localeChanged can announce that the language changed. A remote that starts afterwards has missed that event, however. Without another source, it does not know the current value.

Platform state therefore needs a representation that can be queried at any time and, optionally, a notification mechanism for changes:

interface LocaleSource {
current(): string;
subscribe(listener: (locale: string) => void): () => void;
}

The same principle applies to theme, session, the current user, permissions, feature configuration, tenant, or network state. An event may announce a state change. It must not be the only source of the current state.

Transient messages must not replace state that a remote loaded later needs to reconstruct.

This boundary matters because global event channels can easily look like a simple synchronization mechanism. First, only localeChanged is distributed. Then sessionUpdated, permissionsChanged, and featureFlagsLoaded follow. With every additional event, the system increasingly assumes that all relevant applications are listening at the right time and receive every message in exactly the expected order.

That is not a resilient state architecture. It is temporal coupling.

Domain Events Between Remotes Remain Horizontal Communication

Section titled “Domain Events Between Remotes Remain Horizontal Communication”

The global channel becomes problematic as soon as one remote publishes domain messages so that other remotes can react.

An Angular remote might publish:

planningSaved

A React remote then reloads data, a Vue remote updates a badge, and the host changes the navigation.

The sender imports no function from the other remotes. It may not even know their names. Nevertheless, every consumer depends on its message: on the event name, the payload, the timing, the order, the absence or duplicate delivery of the message, and the current contract version.

A visible connection

Remote A ─────────▶ Remote B

becomes an indirect structure:

Remote A
└── planningSaved
global Event Bus
├── Remote B reloads Data
├── Remote C updates a Badge
└── Host changes Navigation

The dependency graph has not become smaller. It has merely become harder to see.

The sender no longer knows its receivers. Their dependency on its message still remains.

This does not contradict the principle that remotes should not communicate directly. It makes the principle more precise: an indirect domain reaction chain through a global bus is still horizontal communication. It does not become vertical merely because the browser sits between sender and receiver.

No Direct Reference Does Not Mean Independence

Section titled “No Direct Reference Does Not Mean Independence”

Direct coupling is easy to recognize:

remoteB.reload();

Event-based coupling looks more diffuse:

events.dispatchEvent(new CustomEvent('planning:saved'));

If Remote B relies on reloading after planning:saved, the dependency remains real. It is simply no longer located where the event is dispatched. Instead, it is distributed across listeners, registrations, and reaction logic.

An event bus can reduce technical references. It does not guarantee domain independence. In the worst case, it obscures which applications must be changed and tested together.

Loose addressing is not the same as loose domain coupling.

The myth says that because the sender does not know its consumers, the applications are decoupled. In reality, only the direction of the static reference has been removed. The behavioral dependency remains. If the meaning of planningSaved changes, several unknown locations may react incorrectly. If the event is not emitted, projections remain stale. If it is emitted twice, duplicate reloads occur. If a consumer starts too late, it misses the message.

A global event bus does not remove the coupling. It frequently removes it only from the visible dependency graph.

A visible direct dependency between two remotes is compared with a global event bus through which planningSaved triggers several implicit domain reactions in other remotes and the host.

The Event Bus Becomes a Hidden Application Architecture

Section titled “The Event Bus Becomes a Hidden Application Architecture”

A global channel rarely begins as comprehensive domain infrastructure. At first, it contains a notification and perhaps a technical remoteReady. Then planningSaved, customerChanged, selectionChanged, refreshRequested, and dataReloaded are added.

Over time, reaction chains emerge:

planningSaved
└── refreshRequested
└── dataReloaded
└── selectionChanged
└── badgeUpdated

Every individual message may look locally reasonable. Together, however, they form a distributed process whose control is no longer fully visible anywhere.

Order and loading time now matter. Events may be lost or processed more than once. Listeners remain active after unmounting. Mounting again registers the same handler a second time. Notifications appear twice, data is loaded repeatedly, and tests depend on which application started first.

Ownership also becomes unclear. Who may emit selectionChanged? Which payload is binding? Does a new consumer have to understand every historical special case? Who decides when an event may be removed? Which reaction marks the end of the chain?

The event bus becomes a hidden application architecture as soon as domain processes depend on its reaction chains.

The problem is not the browser API. The same architecture could emerge with a custom dispatcher or a shared library. The problem is the global, implicit behavioral contract between independently owned domain applications.

Vertical to the Platform, Not Horizontal Between Remotes

Section titled “Vertical to the Platform, Not Horizontal Between Remotes”

The notification intent has a clear direction:

Remote
└── Notification Intent
Host Platform
└── owns Presentation and Reaction

The host is the defined owner of the global UI. Other remotes do not need to react to the message in domain terms. The contract remains framework-neutral and describes a platform capability.

A domain event between remotes has a different structure:

Remote A
└── Domain Event
global Event Bus
Remote B and Remote C
└── must react

This creates several implicit consumers and domain reaction chains. Completion is unclear, the dependency graph is spread across the application, and a change may affect several remotes at once.

Vertical communication with the platform can be sensible. Horizontal coordination between remotes remains problematic.

This distinction does not depend on the event name alone. Even notificationRequested would be poorly designed if several remotes used it to trigger their own domain processes. Conversely, a host may use any internal mechanism it chooses as long as that mechanism remains behind its boundary and does not become a shared contract for all remotes.

The meaning, direction, and ownership of a message determine whether it describes a platform capability or a hidden horizontal dependency.

Even Small Platform Contracts Need Maintenance

Section titled “Even Small Platform Contracts Need Maintenance”

A legitimate platform intent is not exempt from governance. Its contract needs one clear owner, a documented payload, and an unambiguous name.

platform:notification-requested describes the intent better than update, changed, or showSuccess. The name refers to the platform capability, not to a specific snackbar component. The payload should consist of simple, framework-neutral data.

Component instances, Angular services, React nodes, stores, or domain aggregates do not belong in a global transport contract. Nor should framework callbacks in event payloads indirectly make the boundary permeable again.

Changes must be deliberate. New optional fields are easier to introduce compatibly than a completely changed meaning. For larger breaks, a new name or contract version may be clearer. This does not require an extensive schema registry. It first requires acknowledging that even a small browser event is a maintained platform contract.

Anyone offering a global event channel must also own its lifecycle.

A listener is registered during mounting and removed during unmounting. This symmetry is part of the integration contract, not cleanup added afterwards.

mount
└── register Listener
unmount
└── remove Listener

Without explicit cleanup, stale handlers remain active. Mounting again creates duplicate registrations. Applications that are no longer visible continue to react, tests affect one another, and short-lived technical messages leave global residual state behind.

A global event without explicit cleanup is not loose coupling. It is global residual state.

Again, this is not a general argument against browser events. Their lifecycle simply needs to be designed as deliberately as the channel itself.

A Small Event Channel Is a Sign of Quality

Section titled “A Small Event Channel Is a Sign of Quality”

A sensible global channel contains only a small number of deliberately selected platform contracts. They have one clear owner, transport simple data, and trigger no domain reaction chains between remotes.

Suitable messages are transient technical or UI-related platform intents that require no response and do not replace durable state that must be reconstructable.

The channel should not contain synchronization of domain state, remote-to-remote commands such as refreshRemoteB, global CRUD events, orchestration of business processes, foreign stores, or a replacement for an authoritative data source.

The size of the channel is therefore more than a matter of taste. It reveals whether the product platform offers a few clearly owned capabilities or whether the bus has already taken over the domain integration of the remotes.

A good global event channel remains small, transient, and boring.

Angular and React remotes can pass the same framework-neutral notification intent to an Angular host. Whether they use a direct platform contract or a browser-native event is a technical transport decision. Only the host adapter translates the intent into a service call, a signal, a store, a queue, or an internal NgRx event.

The transport must not be confused with the semantic meaning of the message. A notification is a transient platform intent with one clear UI owner. Current platform state, by contrast, also requires a queryable source so that remotes loaded later can reconstruct it.

Domain state and domain events do not belong on a global channel when other remotes become dependent on them. The event bus then removes only the direct reference, not the horizontal coupling. Its technical elegance changes nothing about the shared names, payloads, ordering, loading times, and reaction chains.

No direct reference does not mean independent evolvability.

A good global event channel remains small, transient, and boring: it coordinates the platform, not the domain.