Singleton Channel (neutral in-app bus)
-----------------------------------------
Publishers Subscribers
---------- -----------
+--------+ +--------+
| View A |----+ +-->| View C |
+--------+ | | +--------+
v |
+---------------------------+
| AppChannel<Event> | (singleton)
+---------------------------+
^ |
+--------+ | | +--------+
| Store |----+ +-->| Logger |
+--------+ +--------+
A Channel<T> is a globally shared, typed event bus. Any component can publish
a value of type T into it, and any component that has subscribed will receive
that value — FIFO (first subscriber registered, first notified). It’s simpler
than a full store because it carries no persistent state; it just broadcasts a
moment-in-time event.
class Channel<T> {
private subscribers: Array<(data: T) => void> = [];
subscribe(cb: (data: T) => void): () => void {
this.subscribers.push(cb);
return () => {
this.subscribers = this.subscribers.filter((s) => s !== cb);
};
}
publish(data: T): void {
for (const cb of this.subscribers) {
cb(data);
}
}
}Because it’s generic, TypeScript enforces that every publisher and subscriber
agrees on the shape of T at compile time — you can’t accidentally publish a
string on a Channel<UserEvent>.
Defining Global Singleton Channels
You declare each channel once and export it as a module-level singleton:
// channels.ts
interface UserLoggedIn {
userId: string;
role: "admin" | "user";
}
interface CartUpdated {
itemCount: number;
total: number;
}
export const userLoginChannel = new Channel<UserLoggedIn>();
export const cartChannel = new Channel<CartUpdated>();Any file can import and use these without passing them through props or constructors.
Usage Across Components
// ComponentA.ts — subscriber
import { cartChannel } from './channels';
const unsub = cartChannel.subscribe(({ itemCount, total }) => {
document.getElementById('cart-count')!.textContent = String(itemCount);
});
// call unsub() when component is destroyed to avoid memory leaks
// ComponentB.ts — publisher
import { cartChannel } from './channels';
function addToCart(item: Item) {
// ... update cart logic
cartChannel.publish({ itemCount: cart.length, total: cart.reduce(...) });
}