Skip to main content

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

FamilyHooks
CoreuseAppBridge
AuthuseSessionToken, useAuthenticatedFetch, useAppQuery, useAppMutation
UI surfaceuseToast, useModal, useConfirmationModal, useTitleBar, useNavigationMenu, useContextualSaveBar, useDirtyState, useLoading, useFullscreen, useLeaveConfirmation, useUnsavedChanges
Resource pickeruseResourcePicker, useProductPicker, useCollectionPicker, useCustomerPicker, useFilePicker
NavigationuseRedirect
Browser APIsusePrint, useShare, useClipboard, useCopyToClipboard, useHistory
LifecycleuseLifecycle, useOnFocus, useOnBlur, useOnVisible, useOnHidden
Data APIsuseUser, useConfig, useEnvironment, useFeatures
SubscriptionsuseAppSubscription, useAppDispatch, useAppDispatchAndWait
CheckoutuseCart

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:
FieldTypeDescription
tokenstring | nullThe current cached token.
loadingbooleanTrue until the first token resolves.
errorError | nullError 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' }],
});

useNavigationMenu

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.

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

HookReturnsWhen 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