React Hooks for App Bridge
@launchmystore/app-bridge-react ships a typed hook for every App Bridge
action family. Each hook wraps the underlying SDK action class from
@launchmystore/app-bridge/actions, so the wire protocol stays exactly the
same — you just don’t have to construct messages by hand.
Installation
npm install @launchmystore/app-bridge @launchmystore/app-bridge-react
AppBridgeProvider
Mount once near the root of your iframe app:
import { AppBridgeProvider } from '@launchmystore/app-bridge-react';
function Root() {
return (
<AppBridgeProvider
config={{
apiKey: process.env.NEXT_PUBLIC_APP_CLIENT_ID,
host: new URLSearchParams(location.search).get('host'),
}}
>
<MyApp />
</AppBridgeProvider>
);
}
AppBridgeProvider takes a single config prop ({ apiKey, host }).
The earlier docs showed apiKey / host as separate props — that form is
not supported.
The provider creates one App instance, tears it down on unmount, and
hands it to every hook through a React context. Calling any hook outside
the provider throws useAppBridge must be used within an AppBridgeProvider.
Hook Index
| Family | Hooks |
|---|
| Core | useAppBridge |
| Auth | useSessionToken, useAuthenticatedFetch, useAppQuery, useAppMutation |
| UI surface | useToast, useModal, useConfirmationModal, useTitleBar, useNavigationMenu, useContextualSaveBar, useDirtyState, useLoading, useFullscreen, useLeaveConfirmation, useUnsavedChanges |
| Resource picker | useResourcePicker, useProductPicker, useCollectionPicker, useCustomerPicker, useFilePicker |
| Navigation | useRedirect |
| Browser APIs | usePrint, useShare, useClipboard, useCopyToClipboard, useHistory |
| Lifecycle | useLifecycle, useOnFocus, useOnBlur, useOnVisible, useOnHidden |
| Data APIs | useUser, useConfig, useEnvironment, useFeatures |
| Subscriptions | useAppSubscription, useAppDispatch, useAppDispatchAndWait |
| Checkout | useCart |
Core
useAppBridge
Access the raw App instance — useful when a built-in hook doesn’t cover
your case or you want to call app.dispatch(action, payload) directly.
import { useAppBridge } from '@launchmystore/app-bridge-react';
function CustomButton() {
const app = useAppBridge();
return (
<button onClick={() => app.dispatch('TOAST_SHOW', { message: 'Hi!' })}>
Show toast
</button>
);
}
Authentication
useSessionToken
Manages a cached JWT for authenticated backend calls. The hook fetches one
token on mount and exposes getToken() (returns the cached token unless
it’s near expiry, then refreshes) plus refresh() (forces a new token).
import { useSessionToken } from '@launchmystore/app-bridge-react';
function Reviews() {
const { token, loading, error, getToken, refresh } = useSessionToken();
const onClick = async () => {
const currentToken = await getToken();
await fetch('/api/reviews', {
headers: { Authorization: `Bearer ${currentToken}` },
});
};
if (loading) return <div>Authenticating…</div>;
if (error) return <div>Auth error: {error.message}</div>;
return <button onClick={onClick}>Load reviews</button>;
}
Return shape:
| Field | Type | Description |
|---|
token | string | null | The current cached token. |
loading | boolean | True until the first token resolves. |
error | Error | null | Error from the initial fetch. |
getToken() | () => Promise<string> | Resolve with a valid token (cached if fresh, refreshed if within 30s of expiry). |
refresh() | () => Promise<string> | Force a fresh round-trip to the host. |
useAuthenticatedFetch, useAppQuery, useAppMutation
Helpers built on top of useSessionToken. useAuthenticatedFetch()
returns a wrapped fetch that injects the Bearer header automatically:
const authFetch = useAuthenticatedFetch();
const res = await authFetch('/api/orders');
See Sessions & Authentication for the data-
fetching variants (useAppQuery, useAppMutation).
UI Surface
useToast
Returns an object with show, success, error, warning, info — not
a callable. Toast options accept { message, duration?, type? }.
import { useToast } from '@launchmystore/app-bridge-react';
function SaveButton() {
const toast = useToast();
const onSave = async () => {
try {
await saveData();
toast.success('Saved!');
} catch {
toast.error('Save failed');
}
};
return <button onClick={onSave}>Save</button>;
}
interface UseToastReturn {
show: (opts: { message: string; duration?: number; type?: 'success' | 'error' | 'warning' | 'info' }) => void;
success: (message: string, duration?: number) => void;
error: (message: string, duration?: number) => void;
warning: (message: string, duration?: number) => void;
info: (message: string, duration?: number) => void;
}
The admin host reads payload.type for the toast variant. The React
checkout host reads payload.variant. The useToast hook always sends
type — if you’re posting toasts from a checkout iframe, build the
payload manually with app.dispatch('TOAST_SHOW', { message, variant }).
useModal
Opens a non-promise modal whose button callbacks fire as you set them.
import { useModal } from '@launchmystore/app-bridge-react';
function DeleteButton({ onConfirm }) {
const modal = useModal({
title: 'Delete item?',
message: 'This cannot be undone.',
primaryAction: { label: 'Delete', onAction: onConfirm },
secondaryActions: [{ label: 'Cancel' }],
onClose: () => console.log('modal closed'),
});
return <button onClick={modal.open}>Delete</button>;
}
Returns { open(): void; close(): void }. Each open() call creates and
dispatches a fresh modal instance; close() dismisses the last one.
useConfirmationModal
Promise-returning variant — perfect for inline await.
import { useConfirmationModal } from '@launchmystore/app-bridge-react';
function DeleteButton({ id }) {
const confirm = useConfirmationModal();
const onClick = async () => {
const ok = await confirm({
title: 'Delete review',
message: 'Permanently remove this review?',
confirmLabel: 'Delete',
cancelLabel: 'Keep',
size: 'small', // 'small' | 'medium' | 'large'
});
if (ok) await deleteReview(id);
};
return <button onClick={onClick}>Delete</button>;
}
useTitleBar
Configures the host’s title bar. Returns update() (merges new options
into the live title bar) and setPrimaryLoading() for the common case of
showing a spinner on the primary button during async work.
const titleBar = useTitleBar({
title: 'Edit product',
primaryAction: {
label: 'Save',
onAction: async () => {
titleBar.setPrimaryLoading(true);
await saveProduct();
titleBar.setPrimaryLoading(false);
},
},
secondaryActions: [
{ label: 'Duplicate', onAction: dup },
{ label: 'Delete', onAction: del, destructive: true },
],
breadcrumbs: [{ label: 'Products', url: '/admin/products' }],
});
Declares the menu rendered by the admin chrome when your app is mounted.
Provide an items array; the host renders them next to the page title.
import { useNavigationMenu } from '@launchmystore/app-bridge-react';
function AppNav() {
useNavigationMenu({
items: [
{ label: 'Dashboard', destination: '/' },
{ label: 'Reviews', destination: '/reviews' },
{ label: 'Settings', destination: '/settings' },
],
active: '/reviews',
});
return null;
}
useContextualSaveBar / useDirtyState
useContextualSaveBar is the low-level hook — call show() / hide() /
update() / setSaveLoading() / setDiscardLoading() directly.
const saveBar = useContextualSaveBar({
message: 'Unsaved changes',
onSave: async () => { await saveSettings(); },
onDiscard: () => resetSettings(),
});
useEffect(() => {
if (isDirty) saveBar.show();
else saveBar.hide();
}, [isDirty]);
useDirtyState glues that together with an internal dirty flag:
const { isDirty, setDirty } = useDirtyState({
onSave: () => saveSettings(),
onDiscard: () => resetSettings(),
});
<input onChange={(e) => { update(e.target.value); setDirty(true); }} />
useLoading
Drives the host’s global thin-progress-bar via the LOADING_START /
LOADING_STOP actions. Use wrap() for the common pattern.
import { useLoading } from '@launchmystore/app-bridge-react';
function FetchButton() {
const loading = useLoading();
const onClick = () =>
loading.wrap(async () => {
const data = await fetch('/api/heavy').then((r) => r.json());
// ...
});
return <button onClick={onClick}>Fetch</button>;
}
Returns { start, stop, wrap<T>(fn: () => Promise<T>): Promise<T> }.
useFullscreen
const { isFullscreen, enter, exit, toggle } = useFullscreen();
isFullscreen flips to true when the host posts back
FULLSCREEN_ENTERED (similarly false on FULLSCREEN_EXITED), so you
can mirror the host’s actual state instead of guessing.
useLeaveConfirmation / useUnsavedChanges
useLeaveConfirmation exposes manual enable() / disable() /
setMessage(). useUnsavedChanges wraps it with a setDirty(boolean)
helper that enables/disables in lock-step with form state.
const { isDirty, setDirty } = useUnsavedChanges('You have unsaved changes.');
<form onChange={() => setDirty(true)}>…</form>
Both hooks also wire the browser’s native beforeunload event under the
hood, so closing the tab triggers the same confirmation.
Resource Picker
useResourcePicker
Generic resource picker — pass resourceType plus optional multiple,
initialSelectionIds, filter, onSelect, onCancel.
import { useResourcePicker } from '@launchmystore/app-bridge-react';
function ProductSelector({ onPick }) {
const picker = useResourcePicker({
resourceType: 'product', // or 'Product'
multiple: true,
onSelect: (selection) => onPick(selection), // [{ id, … }, …]
onCancel: () => console.log('cancelled'),
});
return <button onClick={picker.open}>Pick products</button>;
}
Returns { open(): void; close(): void }.
Typed convenience hooks
import {
useProductPicker,
useCollectionPicker,
useCustomerPicker,
useFilePicker,
} from '@launchmystore/app-bridge-react';
Each takes the same options as useResourcePicker minus resourceType.
There is no useOrderPicker / useArticlePicker / useMenuPicker. For
those, call the generic useResourcePicker({ resourceType: 'order' }).
The full type list lives in Actions
Reference.
Navigation
useRedirect
Returns a menu of typed navigation helpers — the generic navigate()
plus open() (new tab) and one helper per Admin resource detail page.
import { useRedirect } from '@launchmystore/app-bridge-react';
function Links({ productId }) {
const r = useRedirect();
return (
<>
<button onClick={() => r.toProduct(productId)}>View product</button>
<button onClick={() => r.navigate('/admin/orders')}>Orders</button>
<button onClick={() => r.open('https://docs.launchmystore.io')}>Docs (new tab)</button>
</>
);
}
interface UseRedirectReturn {
navigate: (url: string) => void;
open: (url: string) => void; // newContext: true
toApp: (path?: string) => void;
toAdmin: (path?: string) => void;
toProduct: (productId: string) => void;
toCollection: (collectionId: string) => void;
toOrder: (orderId: string) => void;
toCustomer: (customerId: string) => void;
}
Browser APIs
useClipboard
copy() writes through the browser’s navigator.clipboard.writeText API.
paste() round-trips to the host (CLIPBOARD_READ_REQUEST) — required
because Chrome blocks clipboard.readText() in cross-origin iframes.
const { copy, paste, copied, loading, error, isAvailable, reset } = useClipboard();
<button onClick={() => copy('SKU-12345').then(() => setTimeout(reset, 2000))}>
{copied ? 'Copied!' : 'Copy SKU'}
</button>
useCopyToClipboard() is the slim variant — just (text) => Promise<void>.
usePrint
usePrint() returns a single function: print() => void. The host opens
the browser print dialog scoped to your iframe.
useShare
Wraps the Web Share API (mobile). useShare() returns
{ share: (data: { title, url, text? }) => Promise<void>; isSupported }.
useHistory
Drives the host browser history — push, replace, go, back,
forward. Useful when your app uses its own router and you want
host-aware history.
Lifecycle
useLifecycle({ onFocus, onBlur, onVisible, onHidden }) plus the four
single-event variants useOnFocus, useOnBlur, useOnVisible,
useOnHidden. Fired when the host detects the iframe has focused,
blurred, become visible, or been hidden.
useOnVisible(() => refetchData()); // refresh when the merchant comes back
Data APIs
| Hook | Returns | When to use |
|---|
useUser() | { user, loading, error, refresh } | Get the merchant user record (id, email, roles). |
useConfig() | { config, loading, error, refresh } | App-specific config the merchant set. |
useEnvironment() | { environment, loading, error, refresh } | Host environment info: locale, currency, host kind. |
useFeatures() | { features, loading, error, hasFeature(name), refresh } | Feature flags exposed by the host. |
All four are RPCs the first time you call them and cache the result;
refresh() forces a round-trip.
Subscriptions
Three escape hatches when no typed hook exists for what you need:
useAppSubscription(action, callback) — subscribe to a host-initiated
action (e.g. MODAL_PRIMARY_ACTION). Returns nothing; cleanup happens
on unmount.
useAppDispatch() — returns the bound (action, payload?) => string
function from the underlying App.
useAppDispatchAndWait() — returns the bound
(action, payload?) => Promise<payload> function.
import { useAppSubscription, useAppDispatchAndWait } from '@launchmystore/app-bridge-react';
function CartPanel() {
const dispatchAndWait = useAppDispatchAndWait();
useAppSubscription('CART_UPDATED', () => {
// Host emitted a cart-updated event — refetch
dispatchAndWait('CART_GET').then(setCart);
});
// …
}
Checkout
useCart
Returns a memoised Cart action instance for the checkout host. Use it
inside checkout extensions to mutate the host cart:
import { useCart } from '@launchmystore/app-bridge-react';
function AddUpsell() {
const cart = useCart();
const add = () =>
cart.applyCartLinesChange({
type: 'addCartLine',
merchandiseId: 'gid://launchmystore/Variant/abc',
quantity: 1,
});
return <button onClick={add}>Add gift wrap</button>;
}
See App Bridge for Checkout for the full payload
shapes (applyDiscountCodeChange, applyNoteChange,
applyAttributeChange, applyGiftCardChange, etc.).
Complete Example
A realistic admin block that combines auth, save bar, resource picker,
title bar, and the loading indicator:
import {
AppBridgeProvider,
useSessionToken,
useToast,
useConfirmationModal,
useLoading,
useTitleBar,
useProductPicker,
useDirtyState,
} from '@launchmystore/app-bridge-react';
import { useEffect, useState } from 'react';
function Root() {
return (
<AppBridgeProvider
config={{
apiKey: process.env.NEXT_PUBLIC_APP_CLIENT_ID,
host: new URLSearchParams(location.search).get('host'),
}}
>
<ReviewsPanel />
</AppBridgeProvider>
);
}
function ReviewsPanel() {
const [selected, setSelected] = useState(null);
const { getToken } = useSessionToken();
const toast = useToast();
const confirm = useConfirmationModal();
const loading = useLoading();
const picker = useProductPicker({
multiple: false,
onSelect: ([product]) => { setSelected(product); setDirty(true); },
});
const { setDirty } = useDirtyState({
onSave: () =>
loading.wrap(async () => {
const token = await getToken();
await fetch('/api/reviews', {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ productId: selected.id }),
});
toast.success('Saved');
}),
onDiscard: () => { setSelected(null); },
});
useTitleBar({
title: 'Product reviews',
primaryAction: { label: 'Pick product', onAction: picker.open },
});
const onDelete = async () => {
if (!(await confirm({ title: 'Delete review?', message: 'Cannot be undone.' }))) return;
toast.success('Deleted');
};
return (
<div>
{selected ? <p>Reviewing: {selected.title}</p> : <p>No product picked</p>}
<button onClick={onDelete}>Delete</button>
</div>
);
}
TypeScript
Every hook ships with typed return values, options, and accompanying
types — UseToastReturn, UseModalOptions, UseResourcePickerReturn,
UseSessionTokenReturn, etc. Import from the package root.
import type {
UseToastReturn,
UseModalOptions,
UseResourcePickerOptions,
UseSessionTokenReturn,
} from '@launchmystore/app-bridge-react';
useI18n
Subscribes to host-driven locale changes and exposes translate,
formatNumber, formatDate helpers (backed by Intl.*).
import { useI18n } from '@launchmystore/app-bridge-react';
function SettingsHeader() {
const { locale, translate, formatNumber } = useI18n();
return (
<>
<h1>{translate('settings.title', { default: 'Settings', vars: {} })}</h1>
<p>Subtotal: {formatNumber(19.99, { style: 'currency', currency: 'USD' })}</p>
<small>Locale: {locale}</small>
</>
);
}
The hook re-renders whenever the host pushes a new I18N_UPDATE, so
locale changes from the admin chrome propagate without manual handling.
useRestApi
Returns authenticated fetch / fetchJson helpers for the LaunchMyStore
REST API. Session token and API base URL are resolved lazily and
cached.
import { useRestApi } from '@launchmystore/app-bridge-react';
function ProductCount() {
const { fetchJson } = useRestApi();
const [count, setCount] = useState<number | null>(null);
useEffect(() => {
fetchJson<{ products: unknown[] }>('/api/v1/products?limit=250')
.then((d) => setCount(d.products.length));
}, [fetchJson]);
return <p>You have {count ?? '…'} products.</p>;
}
Use REST verbs (GET / POST / PUT / PATCH / DELETE) against /api/v1/...
paths. The hook handles Authorization: Bearer <token> and resolves the
base URL from CONFIG_GET so your iframe works in dev, staging, and
production.
useIntents
Launches another app’s admin action by intent target.
import { useIntents } from '@launchmystore/app-bridge-react';
function SendInvoiceButton({ orderId }: { orderId: string }) {
const intents = useIntents();
return (
<button onClick={async () => {
const result = await intents.launchAndWait('invoice-app.send-invoice', { orderId });
if (result.error) alert(`Couldn't launch: ${result.error}`);
}}>
Send invoice
</button>
);
}
useApi — the umbrella hook
For pages that need most of the App Bridge surface, useApi() returns a
single object containing every helper:
import { useApi } from '@launchmystore/app-bridge-react';
function ProductDetailExtension() {
const { i18n, restApi, picker, toast, modal, navigation, intents, data } = useApi();
// …
}
This is the recommended entry point for admin block / admin action
React components — one shared API surface, automatic listener cleanup,
and stable references across renders.
See Also