Mediator Pattern for Loose Component Coupling in
Reduce chaotic dependencies between UI components by introducing a mediator that centralizes communication, preventing explicit references between peers
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Mediator Pattern for Loose Component Coupling in Frontend Apps
The Mediator pattern defines an object that encapsulates how a set of objects interact. Instead of components referring to each other directly, they refer to a mediator, reducing the number of explicit connections from many-to-many to many-to-one. This is essential for complex UIs where dozens of components need to stay synchronized.
When to Use This
- Components have many-to-many relationships that would otherwise create tight coupling
- Reusing components independently is difficult because they depend on specific peers
- Communication logic is scattered and hard to test
Problem
A dashboard with filters, charts, tables, and maps requires each widget to notify every other widget when data changes. Each widget holds references to 5-6 others, creating a dependency nightmare.
Solution
// mediator/DashboardMediator.ts
interface Mediator {
notify(sender: Component, event: string, data?: unknown): void;
}
abstract class Component {
constructor(protected mediator: Mediator) {}
send(event: string, data?: unknown): void {
this.mediator.notify(this, event, data);
}
}
class FilterPanel extends Component {
private selectedRegion = 'all';
selectRegion(region: string): void {
this.selectedRegion = region;
this.send('region-changed', region);
}
}
class ChartWidget extends Component {
private data: unknown[] = [];
updateData(data: unknown[]): void {
this.data = data;
this.render();
}
private render(): void {
console.log('Chart rendered with', this.data.length, 'points');
}
}
class TableWidget extends Component {
private rows: unknown[] = [];
updateRows(rows: unknown[]): void {
this.rows = rows;
console.log('Table updated with', rows.length, 'rows');
}
}
class MapWidget extends Component {
private center = { lat: 0, lng: 0 };
panTo(center: { lat: number; lng: number }): void {
this.center = center;
console.log('Map centered at', center);
}
}
// Mediator orchestrates all communication
class DashboardMediator implements Mediator {
private filters: FilterPanel;
private chart: ChartWidget;
private table: TableWidget;
private map: MapWidget;
setComponents(
filters: FilterPanel,
chart: ChartWidget,
table: TableWidget,
map: MapWidget
): void {
this.filters = filters;
this.chart = chart;
this.table = table;
this.map = map;
}
notify(sender: Component, event: string, data?: unknown): void {
switch (event) {
case 'region-changed': {
const filteredData = this.fetchDataForRegion(data as string);
this.chart.updateData(filteredData);
this.table.updateRows(filteredData);
this.map.panTo(this.getRegionCenter(data as string));
break;
}
case 'chart-point-clicked': {
const point = data as { lat: number; lng: number };
this.map.panTo(point);
break;
}
}
}
private fetchDataForRegion(region: string): unknown[] {
return [{ id: 1, region }];
}
private getRegionCenter(region: string): { lat: number; lng: number } {
const centers: Record<string, { lat: number; lng: number }> = {
'north': { lat: 45, lng: 0 },
'south': { lat: -45, lng: 0 },
};
return centers[region] || { lat: 0, lng: 0 };
}
}
// Usage
const mediator = new DashboardMediator();
const filters = new FilterPanel(mediator);
const chart = new ChartWidget(mediator);
const table = new TableWidget(mediator);
const map = new MapWidget(mediator);
mediator.setComponents(filters, chart, table, map);
filters.selectRegion('north');
Variation: Event Bus Mediator
// mediator/EventBus.ts
class EventBus implements Mediator {
private listeners = new Map<string, Set<(data: unknown) => void>>();
subscribe(event: string, callback: (data: unknown) => void): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(callback);
return () => this.listeners.get(event)?.delete(callback);
}
notify(_sender: Component, event: string, data?: unknown): void {
this.listeners.get(event)?.forEach(cb => cb(data));
}
emit(event: string, data?: unknown): void {
this.notify(null as unknown as Component, event, data);
}
}
const bus = new EventBus();
bus.subscribe('user-login', user => console.log('Logged in:', user));
bus.emit('user-login', { id: 1 });
How It Works
- Mediator declares the communication interface
- Concrete Mediator implements coordination logic between colleagues
- Colleague components send events to the mediator instead of each other
- Client creates and wires the mediator with all colleagues
Production Considerations
- Keep mediators focused on one domain; do not create a god object
- Use typed events to prevent stringly-typed communication bugs
- Consider state management libraries (Redux, Zustand) as evolved mediators. See Singleton for service instance management.
Common Mistakes
- Creating a mediator so large it becomes unmaintainable
- Bypassing the mediator for direct component communication
- Not unsubscribing event listeners, causing memory leaks
FAQ
Q: How is this different from Observer? A: Observer is one-to-many broadcast. Mediator is many-to-many routing through a central coordinator.
Q: When should I use a state manager instead? A: When the primary need is shared state, not just communication. Mediator handles messages; state managers handle data.
Is this pattern suitable for small projects?
For small projects with few components, this pattern may add unnecessary complexity. Start simple and introduce the pattern when you feel the pain it solves.
How does this pattern compare to alternatives?
Each pattern makes different trade-offs. Review the variants table above and consider your specific constraints: team size, performance requirements, and future scaling plans.
Can I partially apply this pattern?
Yes. Many teams adopt patterns incrementally. Start with the core idea and add sophistication as needed. The pattern is a guide, not a strict blueprint.
Advanced Topics
Scenario: Mediator for Chat Room
// Mediator pattern: centralize communication between components
interface ChatMediator {
sendMessage(msg: string, user: User): void;
registerUser(user: User): void;
}
abstract class User {
constructor(protected mediator: ChatMediator, public name: string) {}
abstract send(msg: string): void;
abstract receive(msg: string): void;
}
class ChatUser extends User {
send(msg: string) { this.mediator.sendMessage(msg, this); }
receive(msg: string) { console.log(`[${this.name}] received: ${msg}`); }
}
// Concrete mediator
class ChatRoom implements ChatMediator {
private users: User[] = [];
registerUser(user: User) { this.users.push(user); }
sendMessage(msg: string, sender: User) {
// Broadcast to all except sender
this.users.filter(u => u !== sender).forEach(u => u.receive(msg));
}
}
// Usage: users do not know each other
const chat = new ChatRoom();
const alice = new ChatUser(chat, "Alice");
const bob = new ChatUser(chat, "Bob");
const charlie = new ChatUser(chat, "Charlie");
chat.registerUser(alice);
chat.registerUser(bob);
chat.registerUser(charlie);
alice.send("Hello everyone");
// [Bob] received: Hello everyone
// [Charlie] received: Hello everyone
// Without Mediator: each user needs reference to all others
// With Mediator: they only know the mediator
Lessons:
- Mediator centralizes communication: components do not know each other
- Adding new user does not require changing existing ones
- The mediator can filter, transform or log messages
- Reduces coupling from N*(N-1) to N*1 (each only knows the mediator)
- CQRS uses mediator: commands and queries go through a bus
- Event bus is a form of mediator with pub/sub
### Mediator vs Observer: which do I use?
Mediator centralizes: components talk to the mediator and it redirects. Observer decentralizes: the subject notifies observers directly. Use Mediator when interaction logic is complex and you want to centralize it (chat room, wizard form). Use Observer when you just need to notify changes (data binding, event handling). Mediator knows the components; Observer does not.
End of document. Review and update quarterly. Related Resources
Observer Pattern
Define a subscription mechanism to notify multiple objects about events. A behavioral design pattern for event-driven communication.
PatternFacade Pattern
Provide a simplified interface to a complex subsystem. A structural pattern that hides implementation details behind a clean API.
PatternBackend for Frontend (BFF) Pattern
Create dedicated backend services tailored to the specific needs of each frontend client type, aggregating downstream APIs and optimizing data shapes per platform.
PatternInterpreter Pattern for Domain-Specific Expression Languages
Build a language interpreter that evaluates expressions and rules by representing grammar as composable objects, useful for formulas, queries, and business rules
PatternAbstract Factory for Cross-Platform UI Component Families
Create families of related objects without specifying concrete classes, enabling platform-specific implementations that share a common interface
PatternBridge Pattern for Decoupling UI Components from Themes
Separate an abstraction from its implementation so both can vary independently using the Bridge pattern for pluggable UI themes and rendering engines
PatternComposite Pattern for UI Component Trees in React
Use the Composite pattern to compose objects into tree structures, letting clients treat individual objects and compositions uniformly in UI component hierarchies