Skip to content

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:

import { publish, subscribe, useSubscribe } from '@ptkl/components/event';

API

publish(topic, payload)

Broadcasts a message to all current subscribers of a topic. Fire-and-forget — returns void.

publish(topic: string, payload: T): 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.

subscribe(topic: string, handler: (payload: T) => void): () => void
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.

useSubscribe<T>(topic: string): T | undefined
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:

// lager embedded view — item was added to cart
publish('cart:updated', { itemCount: newTotal });
// host app header
const cart = useSubscribe<{ itemCount: number }>('cart:updated');

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:

locale              ← platform reserved
cart:updated
customer:selected
order:submitted
user:profile-saved

Avoid single generic words (update, change, data) as they are likely to collide across apps.


Notes

  • Shadow DOM: window events are not affected by shadow boundaries. Embedded views inside a Shadow DOM receive all pub/sub messages normally.
  • No replay: useSubscribe returns undefined until 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 via AppView.
  • 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: subscribe returns an unsubscribe function. Always call it — or use useSubscribe which handles cleanup automatically.