Hey everyone,
We’ve been fighting the issue for weeks:
In our embedded Shopify app (React + App Bridge), clicking on sidebar links (or any navigation inside the admin) would often cause a white screen, slow load, UI flicker, or even a full iframe reload instead of a smooth SPA transition.
After deep debugging, we finally found the root cause – and it might help you too.
Root cause (the real problem)
Multiple App Bridge instances competing inside the same iframe.
We made two classic mistakes:
-
Using both the CDN script and the @shopify/app-bridge npm package
In index.html we had:
<script src="https://cdn.shopify.com/shopifycloud/app-bridge.js"></script>
And in our components we also used createApp() from the npm package (@shopify/app-bridge v3.7.11).
→ Two different App Bridge versions trying to control the same iframe.
-
Calling createApp() inside React components
In useAuthenticatedFetch.js and many other places, we did:
const app = createApp({ apiKey, host });
Every render created a new App Bridge instance. Because the hook was used in dozens of components, each navigation registered 10+ instances. Shopify Admin detected multiple instances and force‑reloaded the iframe as a safety measure.
-
Recreating QueryClient on every render (minor but added chaos)
We had new QueryClient() inside QueryProvider without useState, causing the whole React tree to unmount/remount.
The fix (what finally worked)
1. Stop using createApp() – rely on the CDN instance
Instead of creating your own App Bridge, use the one provided by Shopify CDN via @shopify/app-bridge-react:
// useAuthenticatedFetch.js – fixed version
import { useAppBridge } from '@shopify/app-bridge-react';
import { useCallback } from 'react';
export function useAuthenticatedFetch() {
const shopify = useAppBridge(); // ← CDN instance, no createApp()
return useCallback(async (uri, options) => {
const token = await shopify.idToken(); // token is cached internally
const response = await window.fetch(uri, {
...options,
headers: {
...options?.headers,
Authorization: `Bearer ${token}`,
},
});
if (response.headers.get('X-Shopify-API-Request-Failure-Reauthorize') === '1') {
const authUrl = response.headers.get('X-Shopify-API-Request-Failure-Reauthorize-Url') || '/api/auth';
const redirectUrl = authUrl.startsWith('/')
? `https://${window.location.host}${authUrl}`
: authUrl;
window.open(redirectUrl);
}
return response;
}, [shopify]);
}
2. Use native Shopify navigation instead of Redirect.create()
We replaced all Redirect.create(app).dispatch(...) with window.open('shopify://admin/...', '_top') – this is the official navigation scheme for Shopify Admin.
// ❌ Old way (creates new App Bridge instance)
const app = createApp({ apiKey, host });
const redirect = Redirect.create(app);
redirect.dispatch(Redirect.Action.ADMIN_SECTION, {
name: Redirect.ResourceType.Product,
resource: { id: product.id }
});
// ✅ New way (no extra instance, works reliably)
window.open(`shopify://admin/products/${product.id}`, '_top');
3. Stabilize QueryClient in your provider
// ❌ Before – new client on every render
const client = new QueryClient({ ... });
// ✅ After – created once
const [client] = useState(() => new QueryClient({ ... }));
4. Remove unnecessary packages
After the changes, we uninstalled @shopify/app-bridge and @shopify/app-bridge-utils – they are no longer needed.
Summary
Never call createApp() inside a React component.
Every call registers a new App Bridge instance. Multiple instances confuse Shopify Admin and lead to forced full‑reload navigations.
-
Need a token? Use shopify.idToken() from useAppBridge() (CDN instance).
-
Need navigation inside admin? Use window.open('shopify://admin/...', '_top').
-
Keep a single App Bridge instance (the one loaded from CDN).