App Bridge 3 is a ground-up rewrite of Shopify's embedded app JavaScript layer. Gone are the CDN-loaded script, the separate Actions package, and the complex Redux-like dispatch model. In their place is a lightweight, tree-shakable ESM package that talks to the Admin host through a simple, promise-based API. If your app was built on App Bridge 2 — even a well-maintained one — you will need to migrate before the legacy package is deprecated. This tutorial walks you through every step, from replacing your npm packages to wiring up the new session token flow and contextual save bar.
What you'll build
You will migrate a typical embedded Remix app that currently uses @shopify/app-bridge-react v2 to @shopify/app-bridge-react v3. By the end of this guide your app will authenticate via session tokens (not cookies), render native Admin navigation, open resource pickers, trigger toast notifications, and show a contextual save bar — all using the new API surface.
Before you start
Make sure you have the following in place before touching any code.
- Node.js 18 or later (App Bridge 3 targets the ESM module system)
- Shopify CLI 3.x — run shopify version to confirm
- A Shopify Partner account with a development store and an existing app scaffold
- A working knowledge of React and either Remix or Next.js App Router
Swap the npm packages
App Bridge 3 ships as a single package: @shopify/app-bridge-react. You no longer need @shopify/app-bridge, @shopify/app-bridge-utils, or @shopify/app-bridge-react as separate packages — the new package bundles everything. Start by removing the old packages and adding the new one.
If you're using @shopify/shopify-app-remix or @shopify/shopify-app-express, upgrade those to their latest versions first — they declare @shopify/app-bridge-react v3 as a peer dependency. Installing an outdated version of the framework alongside App Bridge 3 will give you confusing type errors.
# Remove all legacy App Bridge packages
npm uninstall @shopify/app-bridge @shopify/app-bridge-utils @shopify/app-bridge-react
# Install the v3 package
npm install @shopify/app-bridge-react@latest
# If using the Remix adapter, upgrade it too
npm install @shopify/shopify-app-remix@latestExpected outcome: Your package.json should now list @shopify/app-bridge-react at ^3.x.x and have no remaining @shopify/app-bridge or @shopify/app-bridge-utils entries.
Remove the AppProvider wrapper
In App Bridge 2 you had to wrap your entire app in an <AppProvider> component and pass it your apiKey plus a host value decoded from the URL. In App Bridge 3 that wrapper is gone. The Admin host automatically injects App Bridge into the iframe; your code just needs to import and use hooks or components from @shopify/app-bridge-react and it will work.
Find every file in your app that imports AppProvider from @shopify/app-bridge-react and delete both the import and the JSX wrapper. Your root layout or _app file is the most common place this lives. If you were also calling createApp() from @shopify/app-bridge, delete those calls too.
// BEFORE (App Bridge 2)
import { AppProvider } from '@shopify/app-bridge-react';
export default function Root({ children }: { children: React.ReactNode }) {
const host = new URLSearchParams(window.location.search).get('host') ?? '';
return (
<AppProvider apiKey={process.env.SHOPIFY_API_KEY!} host={host}>
{children}
</AppProvider>
);
}
// AFTER (App Bridge 3)
// No wrapper needed — just render your children directly.
export default function Root({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}Expected outcome: The AppProvider import and JSX wrapper should be gone. The app should still compile; any hooks that depend on AppProvider context will now work via the injected global instead.
Replace dispatch-based actions with hooks
App Bridge 2 used an action-creator pattern. You called Toast.create(app, { message }) then dispatch(toastOptions). In App Bridge 3 every action has a dedicated React hook. The useToast hook returns a show function you call directly. Similarly, useNavigate, useModal, and useResourcePicker replace their action-creator equivalents.
The most common migration you'll make is swapping Toast.create / dispatch calls with useToast. Pay attention to the duration option: it's still in milliseconds. The isError option is now an optional type field with value 'error'.
// BEFORE (App Bridge 2)
import { Toast } from '@shopify/app-bridge/actions';
import { useAppBridge } from '@shopify/app-bridge-react';
function SaveButton() {
const app = useAppBridge();
const handleSave = () => {
const toast = Toast.create(app, { message: 'Saved!', duration: 3000 });
toast.dispatch(Toast.Action.SHOW);
};
return <button onClick={handleSave}>Save</button>;
}
// AFTER (App Bridge 3)
import { useToast } from '@shopify/app-bridge-react';
function SaveButton() {
const { show } = useToast();
const handleSave = () => {
show('Saved!', { duration: 3000 });
};
return <button onClick={handleSave}>Save</button>;
}Expected outcome: Toast notifications appear correctly in the Admin host without using dispatch or action creators.
Migrate the session token flow
App Bridge 2 required you to call getSessionToken(app) from @shopify/app-bridge/utilities on every API request, then attach it as a Bearer token in your Authorization header. App Bridge 3 abstracts this: if you're using @shopify/shopify-app-remix, the authenticate.admin() call handles token exchange automatically via the built-in authenticator.
For apps that still need to make client-side fetch calls directly to their own backend, App Bridge 3 exposes a fetchShopifyAPI helper or you can use the authenticatedFetch utility. In both cases, session tokens are fetched and refreshed automatically.
// BEFORE (App Bridge 2 — manual token fetch)
import { getSessionToken } from '@shopify/app-bridge/utilities';
import { useAppBridge } from '@shopify/app-bridge-react';
function useAuthenticatedFetch() {
const app = useAppBridge();
return async (url: string, options?: RequestInit) => {
const token = await getSessionToken(app);
return fetch(url, {
...options,
headers: { ...options?.headers, Authorization: `Bearer ${token}` },
});
};
}
// AFTER (App Bridge 3 — automatic via Remix loader)
// In a Remix loader/action, authenticate.admin() handles everything:
export async function loader({ request }: LoaderFunctionArgs) {
const { admin } = await authenticate.admin(request);
const response = await admin.graphql(
`#graphql
query getShop {
shop { name }
}`
);
const data = await response.json();
return json(data);
}Expected outcome: API calls to the Admin GraphQL API succeed without manually fetching or attaching session tokens. The Remix loader handles authentication transparently.
Update navigation with useNavigate
In App Bridge 2, navigating within the Admin frame meant dispatching a Redirect action: Redirect.create(app).dispatch(Redirect.Action.APP, '/some/path'). App Bridge 3 gives you the useNavigate hook, which returns a function that accepts the same path strings as before but is far simpler to call.
For opening external URLs in a new tab, pass a second options argument: navigate(url, { target: '_blank' }). To navigate to a page within the Shopify Admin itself (not your app), pass the Admin path directly: navigate('/admin/products/123').
// BEFORE (App Bridge 2)
import { Redirect } from '@shopify/app-bridge/actions';
import { useAppBridge } from '@shopify/app-bridge-react';
function BackButton() {
const app = useAppBridge();
return (
<button onClick={() => Redirect.create(app).dispatch(Redirect.Action.APP, '/')}>
Back to dashboard
</button>
);
}
// AFTER (App Bridge 3)
import { useNavigate } from '@shopify/app-bridge-react';
function BackButton() {
const navigate = useNavigate();
return (
<button onClick={() => navigate('/')}>Back to dashboard</button>
);
}Expected outcome: Clicking the button navigates within the embedded Admin frame without a full page reload.
Add the contextual save bar
One of the most visually important App Bridge features is the contextual save bar: the bar that appears at the top of the Admin iframe when a user has unsaved changes. In App Bridge 2 you had to dispatch ContextualSaveBar actions to show and hide it. In App Bridge 3 you render a <SaveBar> component.
The <SaveBar> component is declarative: it appears when rendered and disappears when unmounted. The open prop lets you show it conditionally. Wire its onSave and onDiscard callbacks to your form's submit and reset handlers respectively.
import { SaveBar } from '@shopify/app-bridge-react';
import { useState } from 'react';
export default function ProductForm() {
const [isDirty, setIsDirty] = useState(false);
const [title, setTitle] = useState('');
const handleDiscard = () => {
setTitle('');
setIsDirty(false);
};
const handleSave = async () => {
await fetch('/api/save', { method: 'POST', body: JSON.stringify({ title }) });
setIsDirty(false);
};
return (
<>
<SaveBar open={isDirty} onSave={handleSave} onDiscard={handleDiscard} />
<input
value={title}
onChange={(e) => { setTitle(e.target.value); setIsDirty(true); }}
/>
</>
);
}Expected outcome: The native Admin save bar appears at the top of the page whenever isDirty is true, and disappears after saving or discarding.
Replace ResourcePicker usage
ResourcePicker is one of App Bridge's most-used features — it opens the native Shopify resource selection modal for products, variants, collections, and more. In App Bridge 2 you dispatched ResourcePicker.create(app, options).dispatch(ResourcePicker.Action.OPEN). In v3, import ResourcePicker from @shopify/app-bridge-react and control it as a React component with an open boolean prop.
The onSelection callback receives a SelectPayload object with a selection array. Each item in the array has the shape { id, title, images, variants } for products. The id is the full GID string (gid://shopify/Product/123456789) — parse the numeric ID from the end if your backend needs it.
import { ResourcePicker } from '@shopify/app-bridge-react';
import { useState } from 'react';
export default function ProductSelector() {
const [open, setOpen] = useState(false);
const [selected, setSelected] = useState<string[]>([]);
return (
<>
<button onClick={() => setOpen(true)}>Pick products</button>
<ResourcePicker
resourceType="Product"
open={open}
onSelection={({ selection }) => {
setSelected(selection.map((p) => p.id));
setOpen(false);
}}
onCancel={() => setOpen(false)}
/>
<ul>{selected.map((id) => <li key={id}>{id}</li>)}</ul>
</>
);
}Expected outcome: Clicking 'Pick products' opens the native Shopify product picker modal. Selected product GIDs are stored in state.
What's next
You've completed the core migration. For the full API surface, check the App Bridge 3 reference docs. If you hit edge cases — particularly around Modal or ContextualSaveBar — the official migration guide has a full API comparison table.
Consider following up with an audit of your OAuth flow. With App Bridge 3 and Shopify CLI 3.x, the recommended pattern is token exchange via the Remix/Express adapter rather than a custom OAuth redirect loop. This eliminates the most common source of 'iframe auth redirect' bugs in embedded apps.
Frequently asked questions
- Can I incrementally migrate a large codebase — ship App Bridge 2 and v3 side by side?
Technically you can install both packages at once, but in practice the two versions compete for the same global App Bridge instance injected by the Admin host, causing unpredictable behavior. The safer approach is to migrate a full route or feature at a time in a short-lived branch, then merge. The migration for each file is mechanical and fast once you have the pattern down.
- My app uses the Modal action from App Bridge 2. What replaces it?
In App Bridge 3, modals are rendered as <Modal> components from @shopify/app-bridge-react. Pass open, onHide, and children as props. The modal renders inside the Admin host's overlay layer, outside your iframe, just like in v2.
