Skip to main content

Event subscription patterns

To receive events that originate in the Toss container (back button, home button, accessory button taps, etc.), a mini-app subscribes via the addEventListener APIs in the events namespace. This guide collects the shared structure in one place — each method page documents its own event type; the common shape (subscribe, cleanup, domain selection, anti-patterns) lives here.

At a glance

DomainAPIWhat it listens to
appsInTossEventappsInTossEvent.addEventListenerApps-in-Toss platform events (the type map is empty today, but it follows the same pattern when entries are added).
tdsEventtdsEvent.addEventListenerToss Design System component events. Today: navigationAccessoryEvent (accessory button taps).
graniteEventgraniteEvent.addEventListenerContainer navigation events. Today: backEvent, homeEvent.
One-offonVisibilityChangedByTransparentServiceWebTransparent service web visibility. Not part of the addEventListener family, but uses the same cleanup pattern.

Common call shape

The three domains (appsInTossEvent, tdsEvent, graniteEvent) share a single signature:

const removeListener = <domain>.addEventListener(eventName, {
onEvent: (data) => { /* ... */ },
onError?: (error) => { /* ... */ },
options?: { /* event-specific */ },
});

Key points:

  • The return is always a cleanup function (() => void). Keep it; call it when the subscription should end.
  • onEvent is required, onError and options are optional depending on the event type.
  • Event names are type-narrowed via K extends keyof <Domain>Event, so typos fail at compile time.

Standard React pattern

Subscribe in useEffect and return the cleanup directly.

import { graniteEvent } from '@apps-in-toss/web-framework';
import { useEffect } from 'react';

function MultiStepForm() {
useEffect(() => {
const removeBack = graniteEvent.addEventListener('backEvent', {
onEvent: () => {
// Intercept the container back gesture and convert it into "go to previous step".
goPreviousStep();
},
onError: (error) => {
console.warn('backEvent failed', error);
},
});

return removeBack;
}, []);

return <FormSteps />;
}

For multiple subscriptions in one effect, gather the cleanup calls:

useEffect(() => {
const removes = [
graniteEvent.addEventListener('backEvent', { onEvent: handleBack }),
graniteEvent.addEventListener('homeEvent', { onEvent: handleHome }),
];

return () => {
for (const remove of removes) remove();
};
}, []);

When to use which domain

graniteEvent — container navigation

backEvent and homeEvent fire when the user taps the container's back or home button. Subscribe only when you need to override the default behaviour (return to the Toss container).

  • Multi-step form: convert container back → previous step.
  • Full-screen video: home button → exit full-screen first.
  • In-game: show a "quit?" dialog.

Subscribing on screens that don't need to intercept blocks the natural exit path.

tdsEvent — TDS components

The only event defined today is navigationAccessoryEvent, fired when a button added via addAccessoryButton is tapped. Subscribe on screens that add such a button; clean up before leaving.

appsInTossEvent — platform-wide

The current type map (AppsInTossEvent = {}) is empty, but the API is in place for future additions. You rarely need this today, but the API exists for forward-compat.

The one-off — onVisibilityChangedByTransparentServiceWeb

onVisibilityChangedByTransparentServiceWeb isn't part of the addEventListener family but follows the same cleanup contract.

import { onVisibilityChangedByTransparentServiceWeb } from '@apps-in-toss/web-framework';
import { useEffect } from 'react';

function TransparentServiceWidget({ callbackId }: { callbackId: string }) {
useEffect(() => {
const remove = onVisibilityChangedByTransparentServiceWeb({
options: { callbackId },
onEvent: (isVisible) => {
if (isVisible) startAnalytics();
else stopAnalytics();
},
onError: (error) => console.warn('visibility subscription failed', error),
});

return remove;
}, [callbackId]);

return <Widget />;
}

callbackId is an identifier issued by Toss. Wrong values either trigger onError immediately or silently drop events.

Cleanup checklist

  • Are you capturing the return value of addEventListener into a variable? If not, you can't unsubscribe.
  • Does your useEffect have an empty deps array while the handler closes over props/state? Use a ref or add the deps so the effect re-subscribes.
  • Are you subscribing to the same event from multiple effects? Consolidate into one.
  • Have you verified cleanup actually runs on screen transitions? React 18 strict mode mounts/unmounts twice in dev — your effect should be idempotent.

Anti-patterns

Subscribing to fight the container default

// ❌ Useless: subscribes to backEvent and does nothing
useEffect(() => {
return graniteEvent.addEventListener('backEvent', { onEvent: () => {} });
}, []);

An empty handler only suppresses the container's default back flow without giving the user anything in return. Don't subscribe if you don't intercept.

Subscribing at module scope

// ❌ Lives outside the component lifecycle — there's no cleanup site
const remove = graniteEvent.addEventListener('homeEvent', { onEvent: handleHome });

This listener has no exit and accumulates on hot-reload. Subscribe inside an effect lifecycle.

Ignoring onError

A subscription without onError silently drops failures. The mock will look fine while the real Toss app starts misbehaving — a classic "works in dev only" bug. Always pass at least a console.warn-level onError.

Environment differences

  • Real Toss app: all events dispatch normally. graniteEvent back/home work alongside the OS gesture.
  • devtools mock: the @ait-co/devtools mock lets you trigger events manually from the panel. There's no auto-dispatch — verify time-based cases (e.g. visibility changes) on a real device.
  • External browser: no container, so the subscribe calls throw. No isSupported() guard is offered — simply don't call them outside the mini-app environment.

External references