Pub/Sub — Cross-Context Messaging
Forge apps run as independent bundles, each with their own React tree, state, and providers. The pub/sub system is a lightweight event bus built on top of window.CustomEvent that allows any component — whether in the host app or an embedded view — to communicate without sharing state or passing props across bundle boundaries.
How It Works
Host app Embedded view (lager, crm, …)
──────────────────────── ──────────────────────────────
publish("cart:updated", useSubscribe("cart:updated")
{ itemCount: 3 }) ──→ // re-renders with { itemCount: 3 }
All messages travel via window.dispatchEvent with a ptkl: prefix, so they are visible to every bundle running in the same window — including views embedded inside Shadow DOM. No shared module instance, no import coupling.
Installation
Pub/sub is a sub-entry of @ptkl/components. Import from the dedicated entry so your bundle does not pull in unrelated UI code:
API
publish(topic, payload)
Broadcasts a message to all current subscribers of a topic. Fire-and-forget — returns void.
publish('cart:updated', { itemCount: 3 });
publish('user:selected', { userId: 'abc-123' });
publish('locale', { locale: 'sr' });
subscribe(topic, handler)
Registers a callback for a topic. Returns an unsubscribe function — always call it on cleanup to avoid memory leaks.
const unsub = subscribe<{ userId: string }>('user:selected', ({ userId }) => {
console.log('selected:', userId);
});
// later, e.g. on component unmount:
unsub();
useSubscribe(topic)
React hook that subscribes to a topic and re-renders the component whenever a new message arrives. Returns the latest payload, or undefined before the first message.
import { useSubscribe } from '@ptkl/components/pubsub';
function CartBadge() {
const msg = useSubscribe<{ itemCount: number }>('cart:updated');
return <span>{msg ? msg.itemCount : 0}</span>;
}
Usage Patterns
Host → Embedded view
The host changes something (user selection, locale, active record) and the embedded view reacts:
// storefront — host
import { publish } from '@ptkl/components/pubsub';
function CustomerList() {
const handleSelect = (customerId: string) => {
publish('customer:selected', { customerId });
};
// ...
}
// crm embedded view
import { useSubscribe } from '@ptkl/components/pubsub';
function CustomerDetail() {
const msg = useSubscribe<{ customerId: string }>('customer:selected');
// fetch and render customer when msg changes
}
Embedded view → Host
A view inside a Shadow DOM can notify the host without any prop callbacks:
Outside React
subscribe works in plain JS — useful in toolkit scripts, analytics handlers, or legacy code:
import { subscribe } from '@ptkl/components/pubsub';
const unsub = subscribe('locale', ({ locale }) => {
i18nLib.setLanguage(locale);
});
Built-in Topics
The platform uses the following reserved topics internally. Avoid publishing on these yourself unless you intentionally want to trigger platform behaviour:
| Topic | Payload | Published by |
|---|---|---|
locale |
{ locale: "en_US" \| "sr" } |
LocaleProvider.setLocale() |
Subscribing to reserved topics is fine — for example, an embedded view can listen to locale to apply custom formatting without wrapping its own LocaleProvider.
Topic Naming Conventions
Use noun:verb or noun:event format to keep topics readable and collision-free:
Avoid single generic words (update, change, data) as they are likely to collide across apps.
Notes
- Shadow DOM:
windowevents are not affected by shadow boundaries. Embedded views inside a Shadow DOM receive all pub/sub messages normally. - No replay:
useSubscribereturnsundefineduntil the first message on that topic after the component mounts. If you need the current value on mount, combine pub/sub with a shared initial prop viaAppView. - No ordering guarantees: Multiple subscribers on the same topic are called in registration order within a synchronous dispatch. Do not rely on cross-bundle ordering.
- Cleanup:
subscribereturns an unsubscribe function. Always call it — or useuseSubscribewhich handles cleanup automatically.