Shopify app frontends 

Gadget offers a comprehensive toolkit for quickly constructing frontends for Shopify Apps. When connecting a Gadget app to Shopify, it automatically includes a basic, hosted frontend powered by Vite. This frontend comes preloaded with support for OAuth, Shopify's Polaris design system, multi-tenant data security, and integration with Shopify's App Bridge for embedded applications. You have the flexibility to customize this frontend according to your preferences, or you can create an entirely new external frontend from scratch using Gadget's React packages.

Frontends for Shopify apps interact with Gadget backends through the GraphQL API specific to your Gadget app, along with the corresponding JavaScript client for your application. To work with Gadget from a Shopify app frontend, there are several npm packages that are essential:

PackageDescriptionAvailable from
@shopify/app-bridgeShopify's React package for embedding React applications within the Shopify Adminnpm
@gadget-client/example-appThe JS client for your specific Gadget applicationGadget NPM registry
@gadgetinc/reactThe Gadget React bindings library, providing React hooks for making API callsnpm
@gadgetinc/react-shopify-app-bridgeThe Gadget Shopify wrapper library for Shopify Embedded App setup and authenticationnpm

Gadget installs these packages into your Gadget-hosted frontend automatically, but if you're building an external frontend you must install them yourself.

Once you've set up your Shopify connection, your Gadget app will have a built-in frontend ready for construction in your app's frontend folder. This frontend can access data from your backend, including both models synced from Shopify and models created within your application. By default, your app uses React and Shopify's standard @shopify/app-bridge-react library, so Shopify's normal helpers for navigation and data fetching are ready for use as well.

Gadget frontends for Shopify include Shopify's design system Polaris via the @shopify/polaris package as well, so your app is compliant with Shopify's App Store guidelines out of the box.

If you'd like to build your frontend outside of Gadget, refer to the external frontends guide.

Reading data from your backend 

To facilitate easy access to data from your Gadget application, including both the non-rate-limited copies of Shopify models and your custom models, you can leverage the @gadgetinc/react hooks library in your frontend. This library provides a set of hooks, such as useFindOne, useFindMany, and useFindFirst, specifically designed for fetching data from your Gadget app.

When utilizing these hooks, each one returns an object that includes the requested data, the current fetching state, and an error object if any error occurred during the data retrieval process. Additionally, the returned object includes a refetch function that allows you to refresh the data if needed.

By using the provided hooks from the @gadgetinc/react library, you can easily fetch and manage data from your Gadget app within your frontend code. These hooks simplify the process of data retrieval, provide relevant states and error handling, and offer a convenient mechanism for refreshing the data when necessary.

For example, if you have the Shopify Product model enabled in your Connection, we can fetch product records in a variety of ways:

JavaScript
// fetch one product by id
const [{ data, fetching, error }, refetch] = useFindOne(api.shopifyProduct, "10");
// fetch one product by id
const [{ data, fetching, error }, refetch] = useFindOne(api.shopifyProduct, "10");
JavaScript
// fetch the first 10 products
const [{ data, fetching, error }, refetch] = useFindMany(api.shopifyProduct, {
first: 10,
});
// fetch the first 10 products
const [{ data, fetching, error }, refetch] = useFindMany(api.shopifyProduct, {
first: 10,
});
JavaScript
// fetch the first product with the title field equal to "Socks", throw if it isn't found
const [{ data, fetching, error }, refetch] = useFindFirst(api.shopifyProduct, {
where: { title: "Socks" },
});
// fetch the first product with the title field equal to "Socks", throw if it isn't found
const [{ data, fetching, error }, refetch] = useFindFirst(api.shopifyProduct, {
where: { title: "Socks" },
});
JavaScript
// fetch the first product with the title field equal to "Socks", return null if it isn't found
const [{ data, fetching, error }, refetch] = useMaybeFindFirst(api.shopifyProduct, {
where: { title: "Socks" },
});
// fetch the first product with the title field equal to "Socks", return null if it isn't found
const [{ data, fetching, error }, refetch] = useMaybeFindFirst(api.shopifyProduct, {
where: { title: "Socks" },
});

Data from other models that you've created in your application is accessed the same way. For example, if we're building a free shipping banner app, you might create a Banner model that stores details about each shipping banner created. We can fetch banner records with the same React hooks:

JavaScript
// fetch one banner by id
const [{ data, fetching, error }, refetch] = useFindOne(api.banner, "10");
// fetch one banner by id
const [{ data, fetching, error }, refetch] = useFindOne(api.banner, "10");
JavaScript
// fetch the first 10 banners
const [{ data, fetching, error }, refetch] = useFindMany(api.banner, { first: 10 });
// fetch the first 10 banners
const [{ data, fetching, error }, refetch] = useFindMany(api.banner, { first: 10 });

Each of these hooks must be wrapped in a React component to render. For example, we can use useFindMany to display a list of products in a component:

React
1import { useFindMany } from "@gadgetinc/react";
2import { api } from "../api";
3
4export const ProductsList = () => {
5 const [{ data, fetching, error }, _refetch] = useFindMany(api.shopifyProduct, {
6 first: 10,
7 });
8
9 if (fetching || !data) {
10 return <div>Loading...</div>;
11 }
12
13 if (error) {
14 return <div>Error: {error.message}</div>;
15 }
16
17 return (
18 <ul>
19 {data.map((product) => (
20 <li key={product.id}>{product.title}</li>
21 ))}
22 </ul>
23 );
24};
1import { useFindMany } from "@gadgetinc/react";
2import { api } from "../api";
3
4export const ProductsList = () => {
5 const [{ data, fetching, error }, _refetch] = useFindMany(api.shopifyProduct, {
6 first: 10,
7 });
8
9 if (fetching || !data) {
10 return <div>Loading...</div>;
11 }
12
13 if (error) {
14 return <div>Error: {error.message}</div>;
15 }
16
17 return (
18 <ul>
19 {data.map((product) => (
20 <li key={product.id}>{product.title}</li>
21 ))}
22 </ul>
23 );
24};

For more on reading data in the frontend, see the building frontends guide and the @gadgetinc/react reference.

Shopify permissions 

Gadget incorporates multi-tenant data permissions as the default configuration for your application. By default, an app loaded for a specific Shopify shop can only access data within that particular shop and is not authorized to access data from other shops. This default setup utilizes the Shopify App User role for enforcing these permissions, and you have the flexibility to customize the permissions for this role on the Roles & Permissions page.

In Gadget, multi-tenant data permissions are automatically enforced for Shopify models. However, access to your own models is not automatically granted. To enable frontend access to your models, you need to assign permissions to the Shopify App User role for each model you wish to make accessible. For instance, if you want to access a model named Banner, you can navigate to the Roles & Permissions screen and select the Read permission checkbox for the Banner model.

For further guidance and details on managing access control, refer to the access control guide, which provides comprehensive information on configuring and customizing data permissions in Gadget.

Writing data back to your backend 

Shopify app frontends can use the useActionForm, useAction and useGlobalAction hooks from the @gadgetinc/react hooks library to write data back to your database.

To update data within Shopify, you must make an API call to Shopify directly from your backend. See the Calling Shopify API section for more information.

To write data back to your database for models you've created, or fields you've added to Shopify Models, use useActionForm, useAction or useGlobalAction hook in a React component.

For example, if we create a new model within our app called banner, we can use the useActionForm hook to create a form for new banner records:

React
1import { useActionForm } from "@gadgetinc/react";
2import { api } from "../api";
3
4export const CreateBannerForm = () => {
5 const {
6 register,
7 submit,
8 formState: { isSubmitting },
9 error,
10 } = useActionForm(api.banner.create);
11
12 if (isSubmitting) {
13 return <div>Saving...</div>;
14 }
15
16 if (error) {
17 return <div>Error: {error.message}</div>;
18 }
19
20 return (
21 <form onSubmit={submit}>
22 <label>Message</label>
23 <textarea {...register("message")} />
24 <input type="submit" />
25 </form>
26 );
27};
1import { useActionForm } from "@gadgetinc/react";
2import { api } from "../api";
3
4export const CreateBannerForm = () => {
5 const {
6 register,
7 submit,
8 formState: { isSubmitting },
9 error,
10 } = useActionForm(api.banner.create);
11
12 if (isSubmitting) {
13 return <div>Saving...</div>;
14 }
15
16 if (error) {
17 return <div>Error: {error.message}</div>;
18 }
19
20 return (
21 <form onSubmit={submit}>
22 <label>Message</label>
23 <textarea {...register("message")} />
24 <input type="submit" />
25 </form>
26 );
27};

For more details on the useActionForm hook, see the @gadgetinc/react reference, and see the Building Frontends guide for more examples.

Calling Global Actions 

Shopify app frontends can call your backend's Global Actions with the useGlobalAction hook. See the Building Frontends guide for more information.

Calling HTTP routes 

Shopify app frontends can call your backend's HTTP Routes with api.fetch, or any other HTTP client for React. See the Building Frontends guide for more information.

If your HTTP routes require authentication, or need to access the connections object server side, you must ensure you pass the correct authentication headers to your HTTP route from your frontend. You can do this automatically by using api.fetch instead of the built-in browser fetch.

For more information, see the Building Frontends guide.

Calling the Shopify API 

In Gadget, it is generally recommended to read data through Gadget's API, as Gadget's API does not impose the same access restrictions as Shopify's API. Unlike Shopify's API, your Gadget app's API is not rate-limited, allowing you to fetch data without the need for meticulous request management.

However, if you require access to data that your Gadget app doesn't sync or if you need to retrieve data that Gadget doesn't have direct access to, you can utilize Shopify's API directly.

To make requests to Shopify's Admin API, it is best to initiate the calls from the backend of your Gadget app's API. This involves making a request from your frontend application to your Gadget backend, which in turn makes the necessary calls to Shopify's API. The results are then returned from your Gadget backend to your frontend application.

Calling Shopify within actions 

Code within a model actions or global actions can make API calls to Shopify using the connections.shopify.current Shopify API client Gadget provides.

For example, if we want to create a product from the frontend of our Gadget application, we can create an global action that calls the Shopify API:

First, we create a new global action called createProduct.js within the api/actions folder in Gadget.

View of the global actions editor with a freshly created Global Action

Then, we can add the following code to the global action:

api/actions/createProduct.js
JavaScript
1export const run: ActionRun = async ({ logger, connections }) => {
2 const shopify = connections.shopify.current;
3 if (!shopify) {
4 throw new Error("Missing Shopify connection");
5 }
6
7 const product = await shopify.graphql(
8 `mutation ($input: ProductInput!) {
9 productCreate(input: $input) {
10 product {
11 title
12 }
13 }
14 userErrors {
15 message
16 }
17 }`,
18 {
19 input: {
20 title: "New Product",
21 },
22 }
23 );
24 logger.info({ product }, "created new product in shopify");
25 return product;
26};
1export const run: ActionRun = async ({ logger, connections }) => {
2 const shopify = connections.shopify.current;
3 if (!shopify) {
4 throw new Error("Missing Shopify connection");
5 }
6
7 const product = await shopify.graphql(
8 `mutation ($input: ProductInput!) {
9 productCreate(input: $input) {
10 product {
11 title
12 }
13 }
14 userErrors {
15 message
16 }
17 }`,
18 {
19 input: {
20 title: "New Product",
21 },
22 }
23 );
24 logger.info({ product }, "created new product in shopify");
25 return product;
26};

This action uses Gadget's connections object to access the Shopify API client, and then calls the product.create method to create a new product in Shopify.

We can then call this global action from our Shopify frontend with the useGlobalAction React hook. For example, we can call this action when a button is clicked in the app:

web/components/CreateProductButton.js
React
1import { useGlobalAction } from "@gadgetinc/react";
2import { api } from "../api";
3
4export const CreateProductButton = () => {
5 const [{ data, fetching, error }, createProduct] = useGlobalAction(api.createProduct);
6 createProduct;
7
8 return (
9 <button disabled={fetching} onClick={() => void createProduct()}>
10 Create Product
11 </button>
12 );
13};
1import { useGlobalAction } from "@gadgetinc/react";
2import { api } from "../api";
3
4export const CreateProductButton = () => {
5 const [{ data, fetching, error }, createProduct] = useGlobalAction(api.createProduct);
6 createProduct;
7
8 return (
9 <button disabled={fetching} onClick={() => void createProduct()}>
10 Create Product
11 </button>
12 );
13};

Gadget's synced Shopify models are one-way synced out of Shopify. You can't call the api.shopifyProduct.create action from the frontend to create a product, as that would put your Gadget app out of sync with Shopify. You must use the Shopify API to create resources that Shopify owns.

Calling Shopify in HTTP Routes 

Route code within HTTP Routes can access the Shopify API using request.connections.shopify to create a Shopify API client object, similarly to Actions or Global Actions.

From authenticated API clients 

If you are calling your HTTP route from an API client that has been authenticated with a Shopify session token, for example, you are using useFetch inside your embedded frontend, the value of connections.shopify.current can be used in the exact same way as you would from an action.

For example, an HTTP route could return an up-to-date list of products from the Shopify API:

api/routes/GET-products.js
JavaScript
1import { RouteHandler } from "gadget-server";
2
3const route: RouteHandler = async ({ request, reply, api, logger, connections }) => {
4 const shopify = connections.shopify.current;
5 if (!shopify) {
6 throw new Error("Missing Shopify connection");
7 }
8
9 const products = await shopify.graphql(
10 `query {
11 products(first: 10) {
12 edges {
13 node {
14 id
15 title
16 handle
17 }
18 cursor
19 }
20 pageInfo {
21 hasNextPage
22 }
23 }
24 }`
25 );
26
27 await reply.send({
28 products,
29 });
30};
31
32export default route;
1import { RouteHandler } from "gadget-server";
2
3const route: RouteHandler = async ({ request, reply, api, logger, connections }) => {
4 const shopify = connections.shopify.current;
5 if (!shopify) {
6 throw new Error("Missing Shopify connection");
7 }
8
9 const products = await shopify.graphql(
10 `query {
11 products(first: 10) {
12 edges {
13 node {
14 id
15 title
16 handle
17 }
18 cursor
19 }
20 pageInfo {
21 hasNextPage
22 }
23 }
24 }`
25 );
26
27 await reply.send({
28 products,
29 });
30};
31
32export default route;

From unauthenticated API clients 

If you are calling your HTTP route from an unauthenticated API client, for example, from a Shopify theme app extension, the value of connections.shopify.current will not be set. Instead, you can use one of the following methods to create a Shopify API client object:

If your route has access to the shop id, you can use the forShopId method to create a Shopify API client object:

api/routes/GET-products.js
JavaScript
1import { RouteHandler } from "gadget-server";
2
3/** @type { RouteHandler<{ Params: { shopId: string }; }> }*/
4const route = async ({ request, reply, api, logger, connections }) => {
5 const { shopId } = request.params;
6 const shopify = await connections.shopify.forShopId(shopId);
7
8 const products = await shopify.graphql(
9 `query {
10 products(first: 10) {
11 edges {
12 node {
13 id
14 title
15 handle
16 }
17 cursor
18 }
19 pageInfo {
20 hasNextPage
21 }
22 }
23 }`
24 );
25
26 await reply.send({
27 products,
28 });
29};
30
31export default route;
1import { RouteHandler } from "gadget-server";
2
3const route: RouteHandler<{ Params: { shopId: string } }> = async ({
4 request,
5 reply,
6 api,
7 connections,
8}) => {
9 const { shopId } = request.params;
10 const shopify = await connections.shopify.forShopId(shopId);
11
12 const products = await shopify.graphql(
13 `query {
14 products(first: 10) {
15 edges {
16 node {
17 id
18 title
19 handle
20 }
21 cursor
22 }
23 pageInfo {
24 hasNextPage
25 }
26 }
27 }`
28 );
29
30 await reply.send({
31 products,
32 });
33};
34
35export default route;

If your route has access to the shop domain, you can use the forShopDomain method to create a Shopify API client object:

api/routes/GET-products.js
JavaScript
1import { RouteHandler } from "gadget-server";
2
3/** @type { RouteHandler<{ Params: { shopDomain: string }; }> }*/
4const route = async ({ request, reply, api, logger, connections }) => {
5 const { shopDomain } = request.params;
6 const shopify = await connections.shopify.forShopDomain(shopDomain);
7
8 const products = await shopify.graphql(
9 `query {
10 products(first: 10) {
11 edges {
12 node {
13 id
14 title
15 handle
16 }
17 cursor
18 }
19 pageInfo {
20 hasNextPage
21 }
22 }
23 }`
24 );
25
26 await reply.send({
27 products,
28 });
29};
30
31export default route;
1import { RouteHandler } from "gadget-server";
2
3const route: RouteHandler<{ Params: { shopDomain: string } }> = async ({
4 request,
5 reply,
6 api,
7 connections,
8}) => {
9 const { shopDomain } = request.params;
10 const shopify = await connections.shopify.forShopDomain(shopDomain);
11
12 const products = await shopify.graphql(
13 `query {
14 products(first: 10) {
15 edges {
16 node {
17 id
18 title
19 handle
20 }
21 cursor
22 }
23 pageInfo {
24 hasNextPage
25 }
26 }
27 }`
28 );
29
30 await reply.send({
31 products,
32 });
33};
34
35export default route;

Because the previous two methods need to make a database query to get the shop by id or domain, if you also need to load the shop record in your route, you can save a database query by using the forShop method:

api/routes/GET-products.js
JavaScript
1import { RouteHandler } from "gadget-server";
2
3/** @type { RouteHandler<{ Params: { shopId: string }; }> }*/
4const route = async ({ request, reply, api, logger, connections }) => {
5 const { shopId } = request.params;
6
7 const shop = await api.internal.shopifyShop.findOne(shopId);
8 // @ts-ignore
9 const shopify = connections.shopify.forShop(shop);
10
11 const products = await shopify.graphql(
12 `query {
13 products(first: 10) {
14 edges {
15 node {
16 id
17 title
18 handle
19 }
20 cursor
21 }
22 pageInfo {
23 hasNextPage
24 }
25 }
26 }`
27 );
28
29 await reply.send({
30 products,
31 });
32};
33
34export default route;
1import { RouteHandler } from "gadget-server";
2
3const route: RouteHandler<{ Params: { shopId: string } }> = async ({
4 request,
5 reply,
6 api,
7 connections,
8}) => {
9 const { shopId } = request.params;
10
11 const shop = await api.internal.shopifyShop.findOne(shopId);
12 // @ts-ignore
13 const shopify = connections.shopify.forShop(shop);
14
15 const products = await shopify.graphql(
16 `query {
17 products(first: 10) {
18 edges {
19 node {
20 id
21 title
22 handle
23 }
24 cursor
25 }
26 pageInfo {
27 hasNextPage
28 }
29 }
30 }`
31 );
32
33 await reply.send({
34 products,
35 });
36};
37
38export default route;

When calling HTTP routes from an unauthenticated client, take care to ensure that you are safe guarding sensitive data.

Standalone Shopify Apps 

With a standalone Shopify app, you'll need to handle authentication and tenancy yourself as there is no Shopify session token to derive this from.

If your Shopify app is not an embedded app, you can still use the Gadget frontends to build your app. To do so, you will need to set the type prop on GadgetProvider to AppType.Standalone.

web/components/App.jsx
React
1export const App = () => {
2 return (
3 <GadgetProvider type={AppType.Standalone} shopifyApiKey={window.gadgetConfig.apiKeys.shopify} api={api}>
4 <AuthenticatedApp />
5 </GadgetProvider>
6 );
7};
1export const App = () => {
2 return (
3 <GadgetProvider type={AppType.Standalone} shopifyApiKey={window.gadgetConfig.apiKeys.shopify} api={api}>
4 <AuthenticatedApp />
5 </GadgetProvider>
6 );
7};

Shopify frontend security requirements 

Shopify requires app developers to meet a set of important security requirements when building embedded apps. Gadget meets each security requirement out of the box:

  • Gadget apps are always served over HTTPS
  • Gadget apps run behind a production-grade firewall and only expose the necessary services
  • Gadget apps use robust, multi-tenant authentication that limits access to data to the current shop (see the access control guide)
  • Gadget sets up GDPR webhook listeners automatically for Shopify apps (see the GDPR docs)
  • Gadget uses Shopify's Session Token authentication mechanism for authenticating the frontend to the backend.
  • Gadget automatically sets the Content-Security-Policy header necessary for Shopify's IFrame protection when serving Shopify apps (see Shopify's security docs)

If you have any questions about the security posture of Gadget applications for Shopify, please join us in our Discord to discuss!

Reference 

The Provider 

The @gadgetinc/react-shopify-app-bridge library handles authentication of your embedded app via the Provider component. This provider has two main benefits - it handles authentication and the series of redirects required to complete an embedded app OAuth flow in Shopify, and it handles retrieving a Shopify session token from the App Bridge and passing it along to Gadget for authenticated calls.

The Provider handles these key tasks automatically:

  • Starts the OAuth process with new users of the application using Gadget, escaping Shopify's iframe if necessary
  • Establishes an iframe-safe secure session with the Gadget backend using Shopify's Session Token authentication scheme
  • Sets up the correct React context for making backend calls to Gadget using @gadgetinc/react

The Provider has the following required props:

Provider Interface
TypeScript
export interface ProviderProps {
type: AppType; // 'AppType.Embedded' or 'AppType.Standalone'
shopifyApiKey: string; // the API key from your Shopify app in the partner dashboard that is used with the Shopify App Bridge
api: string; // the API client created using your Gadget application
}

The Gadget provider will handle detecting if your app is being rendered in an embedded context and redirect the user through Shopify's OAuth flow if necessary.

The useGadget React hook 

The Provider handles initializing the App Bridge for us. Now we can build our application component and use the initialized instance of App Bridge via the appBridge key returned from the embedded React hook useGadget.

useGadget provides the following properties:

useGadget React Hook
TypeScript
1export interface useGadget {
2 isAuthenticated: boolean; // 'true' if the user has completed a successful OAuth flow
3 isEmbedded: boolean; // 'true' if the app is running in an embedded context
4 isRootFrameRequest: boolean; // 'true' if a user is viewing a "type: AppType.Embedded" app in a non-embedded context, for example, accessing the app at a hosted Vercel domain
5 loading: boolean; // 'true' if the OAuth flow is in process
6 appBridge: AppBridge; // a ready-to-use app bridge from Shopify, you can also use the traditional useAppBridge hook in your components to retrieve it.
7}

Using Provider and useGadget to set up App Bridge 

Here is an example of how to set up App Bridge, and use it to open an App Bridge resource picker:

JavaScript
1// import the Gadget<->Shopify bindings that manage the auth process with Shopify
2import {
3 AppType,
4 Provider as GadgetProvider,
5 useGadget,
6} from "@gadgetinc/react-shopify-app-bridge";
7// import the instance of the Gadget API client for this app constructed in the other file
8import { api } from "../api";
9// import the useGlobalAction hook to help call your Gadget API
10import { useGlobalAction } from "@gadgetinc/react";
11
12export default function App() {
13 return (
14 // Wrap our main application's react components in the `<GadgetProvider/>` component
15 // to interface with Shopify. This wrapper sets up the Shopify App Bridge.
16 // It will automatically redirect to perform the OAuth authentication
17 // if the shopify shop doesn't yet have the store installed.
18 <GadgetProvider
19 type={AppType.Embedded}
20 shopifyApiKey={window.gadgetConfig.apiKeys.shopify}
21 api={api}
22 >
23 <SimplePage />
24 </GadgetProvider>
25 );
26}
27
28// An example component that uses the Gadget React hooks to work with data in the backend
29function SimplePage() {
30 const { loading, appBridge, isRootFrameRequest, isAuthenticated } = useGadget();
31
32 // makeSelections is a global action in this example
33 // it is called using Gadget's useGlobalAction hook
34 const [{ error }, makeSelections] = useGlobalAction(api.makeSelections);
35
36 return (
37 <>
38 {loading && <span>Loading...</span>}
39 {/* A user is viewing this page from a direct link so show them the home page! */}
40 {!loading && isRootFrameRequest && (
41 <div>Welcome to my cool app's webpage!</div>
42 )}
43 {error && <div>{error.message}</div>}
44 {!loading && isAuthenticated && (
45 <button
46 onClick={async () => {
47 if (appBridge) {
48 // open the App Bridge resource picker component
49 const selected = appBridge.resourcePicker({ type: "product" });
50 console.log({ selected });
51 // pass selections to a global action
52 await makeSelections({ selected });
53 }
54 }}
55 >
56 Open resource picker
57 </button>
58 )}
59 </>
60 );
61}
1// import the Gadget<->Shopify bindings that manage the auth process with Shopify
2import {
3 AppType,
4 Provider as GadgetProvider,
5 useGadget,
6} from "@gadgetinc/react-shopify-app-bridge";
7// import the instance of the Gadget API client for this app constructed in the other file
8import { api } from "../api";
9// import the useGlobalAction hook to help call your Gadget API
10import { useGlobalAction } from "@gadgetinc/react";
11
12export default function App() {
13 return (
14 // Wrap our main application's react components in the `<GadgetProvider/>` component
15 // to interface with Shopify. This wrapper sets up the Shopify App Bridge.
16 // It will automatically redirect to perform the OAuth authentication
17 // if the shopify shop doesn't yet have the store installed.
18 <GadgetProvider
19 type={AppType.Embedded}
20 shopifyApiKey={window.gadgetConfig.apiKeys.shopify}
21 api={api}
22 >
23 <SimplePage />
24 </GadgetProvider>
25 );
26}
27
28// An example component that uses the Gadget React hooks to work with data in the backend
29function SimplePage() {
30 const { loading, appBridge, isRootFrameRequest, isAuthenticated } = useGadget();
31
32 // makeSelections is a global action in this example
33 // it is called using Gadget's useGlobalAction hook
34 const [{ error }, makeSelections] = useGlobalAction(api.makeSelections);
35
36 return (
37 <>
38 {loading && <span>Loading...</span>}
39 {/* A user is viewing this page from a direct link so show them the home page! */}
40 {!loading && isRootFrameRequest && (
41 <div>Welcome to my cool app's webpage!</div>
42 )}
43 {error && <div>{error.message}</div>}
44 {!loading && isAuthenticated && (
45 <button
46 onClick={async () => {
47 if (appBridge) {
48 // open the App Bridge resource picker component
49 const selected = appBridge.resourcePicker({ type: "product" });
50 console.log({ selected });
51 // pass selections to a global action
52 await makeSelections({ selected });
53 }
54 }}
55 >
56 Open resource picker
57 </button>
58 )}
59 </>
60 );
61}

Once isAuthenticated is true, you will be able to make authenticated API requests using your Gadget API client and the @gadgetinc/react hooks.

The Provider and useGadget are already set up for you when you connect to Shopify using Gadget's frontends.

GraphQL queries 

When developing embedded Shopify apps, it is possible that the installed scopes of a Shop may not align with the required scopes in your Gadget app's connection. In such cases, it becomes necessary to re-authenticate with Shopify in order to obtain the updated scopes. To determine if re-authentication is required and gather information about missing scopes, you can execute the following GraphQL query using the app client provided to the Provider:

GraphQL
1query {
2 shopifyConnection {
3 requiresReauthentication
4 missingScopes
5 }
6}

Session model management 

The Shopify Connection in Gadget automatically manages records of the backend Session model when using @gadgetinc/react-shopify-app-bridge. When a merchant first loads up the frontend application, the <Provider/> will retrieve a Shopify Session Token from Shopify's API, and pass it to your Gadget backend application. The Gadget Shopify Connection will then validate this token. If valid, the connection will provision a new record of the Session model with the correct shopId field set up. This session is then passed to all your backend application's model filters and is available within Action code snippets.

Embedded app examples 

Want to see an example of an embedded Shopify app built using Gadget?

Check out some of our example apps on GitHub, including:

External Frontends with the Shopify CLI 

If the built-in Gadget frontend doesn't work for you, you can use the frontend generated by the Shopify CLI as an external frontend for your Gadget backend application. This lets you still take advantage of Gadget's OAuth handling, scalable backend database, and robust webhook processing while using the frontend generated by the Shopify CLI.

Using the Shopify CLI frontend 

To use Shopify's generated frontend as an external frontend, you will need to make a few changes to the code generated by the Shopify CLI. Instead of deleting the web folder from your repository after generating a CLI app, keep it in place, and follow these steps:

  • Update web/index.js to not implement Shopify OAuth, as Gadget handles OAuth and syncing data from the Shopify API. Instead, web/index.js just needs to serve the frontend application with the correct security headers for Shopify.

Replace the contents of web/index.js with the following:

web/index.js
JavaScript
1import { join } from "path";
2import * as fs from "fs";
3import express, { Request, Response } from "express";
4import serveStatic from "serve-static";
5
6const __dirname = new URL(".", import.meta.url).pathname;
7
8const PORT = parseInt(process.env["BACKEND_PORT"] || process.env["PORT"] || "0", 10);
9const STATIC_PATH =
10 process.env["NODE_ENV"] === "production"
11 ? `${__dirname}/frontend/dist`
12 : `${__dirname}/frontend/`;
13
14const app = express();
15
16// return Shopify's required iframe embedding headers for all requests
17app.use((req, res, next) => {
18 const shop = req.query.shop;
19 if (shop) {
20 res.setHeader(
21 "Content-Security-Policy",
22 `frame-ancestors https://${shop} https://admin.shopify.com;`
23 );
24 }
25 next();
26});
27
28// serve any static assets built by vite in the frontend folder
29app.use(serveStatic(STATIC_PATH, { index: false }));
30
31// serve the client side app for all routes, allowing it to pick which page to render
32app.use("/*", async (_req: Request, res: Response) => {
33 res
34 .status(200)
35 .set("Content-Type", "text/html")
36 .send(fs.readFileSync(join(STATIC_PATH, "index.html")));
37});
38
39app.listen(PORT);
1import { join } from "path";
2import * as fs from "fs";
3import express, { Request, Response } from "express";
4import serveStatic from "serve-static";
5
6const __dirname = new URL(".", import.meta.url).pathname;
7
8const PORT = parseInt(process.env["BACKEND_PORT"] || process.env["PORT"] || "0", 10);
9const STATIC_PATH =
10 process.env["NODE_ENV"] === "production"
11 ? `${__dirname}/frontend/dist`
12 : `${__dirname}/frontend/`;
13
14const app = express();
15
16// return Shopify's required iframe embedding headers for all requests
17app.use((req, res, next) => {
18 const shop = req.query.shop;
19 if (shop) {
20 res.setHeader(
21 "Content-Security-Policy",
22 `frame-ancestors https://${shop} https://admin.shopify.com;`
23 );
24 }
25 next();
26});
27
28// serve any static assets built by vite in the frontend folder
29app.use(serveStatic(STATIC_PATH, { index: false }));
30
31// serve the client side app for all routes, allowing it to pick which page to render
32app.use("/*", async (_req: Request, res: Response) => {
33 res
34 .status(200)
35 .set("Content-Type", "text/html")
36 .send(fs.readFileSync(join(STATIC_PATH, "index.html")));
37});
38
39app.listen(PORT);
  • You can then delete the other example code that @shopify/cli created in the web/ directory when it created your app if you like by running the following command in your app's root directory
terminal
rm -f web/shopify.js web/product-creator.js web/gdpr.js

Finally, we can set up our Gadget Client and use the Provider to handle OAuth for our embedded app.

You need to install your Gadget dependencies in the web/frontend directory of your Shopify CLI application! Change into this directory before running the following commands:

terminal
cd web/frontend
  • Install local-ssl-proxy in the web/frontend directory
web/frontend
npm install local-ssl-proxy
yarn add local-ssl-proxy
  • Update the dev script in web/frontend/package.json to vite & local-ssl-proxy --source 443 --target 3005.
web/frontend/package.json
json
1{
2 // ...
3 "scripts": {
4 "build": "vite build",
5 "dev": "vite & local-ssl-proxy --source 443 --target 3005",
6 "coverage": "vitest run --coverage"
7 }
8}
Working with Windows?

If you are working with Windows, the dev command above will not work. You will need to split it up into two separate commands and run them separately. For example, "dev": "vite" and "dev-proxy": "local-ssl-proxy --source 443 --target 3005".

This allows us to use our local frontend when doing development inside Shopify's admin, which uses HTTPS.

  • Replace your web/frontend/vite.config file with the following code:
web/frontend/vite.config.js
JavaScript
1import { defineConfig } from "vite";
2import { dirname } from "path";
3import { fileURLToPath } from "url";
4import react from "@vitejs/plugin-react";
5
6if (
7 process.env["npm_lifecycle_event"] === "build" &&
8 !process.env["CI"] &&
9 !process.env["SHOPIFY_API_KEY"]
10) {
11 console.warn(
12 "\nBuilding the frontend app without an API key. The frontend build will not run without an API key. Set the SHOPIFY_API_KEY environment variable when running the build command.\n"
13 );
14}
15
16const host = "localhost";
17const port = 3005;
18
19export default defineConfig({
20 root: dirname(fileURLToPath(import.meta.url)),
21 plugins: [react()],
22 define: {
23 "process.env": JSON.stringify({
24 SHOPIFY_API_KEY: process.env["SHOPIFY_API_KEY"],
25 }),
26 },
27 resolve: {
28 preserveSymlinks: true,
29 },
30 server: {
31 host,
32 port,
33 hmr: {
34 protocol: "ws",
35 host,
36 port,
37 clientPort: port,
38 },
39 },
40});
1import { defineConfig } from "vite";
2import { dirname } from "path";
3import { fileURLToPath } from "url";
4import react from "@vitejs/plugin-react";
5
6if (
7 process.env["npm_lifecycle_event"] === "build" &&
8 !process.env["CI"] &&
9 !process.env["SHOPIFY_API_KEY"]
10) {
11 console.warn(
12 "\nBuilding the frontend app without an API key. The frontend build will not run without an API key. Set the SHOPIFY_API_KEY environment variable when running the build command.\n"
13 );
14}
15
16const host = "localhost";
17const port = 3005;
18
19export default defineConfig({
20 root: dirname(fileURLToPath(import.meta.url)),
21 plugins: [react()],
22 define: {
23 "process.env": JSON.stringify({
24 SHOPIFY_API_KEY: process.env["SHOPIFY_API_KEY"],
25 }),
26 },
27 resolve: {
28 preserveSymlinks: true,
29 },
30 server: {
31 host,
32 port,
33 hmr: {
34 protocol: "ws",
35 host,
36 port,
37 clientPort: port,
38 },
39 },
40});
Changing the port

If you wish to change the port for your local server, make sure to modify both the port variable in web/frontend/vite.config and the target at the end of the dev script in the web/frontend/package.json. Note that the ports must be the same for the proxy to function correctly.

Shopify CLI apps using Gadget don't need to use ngrok and instead run at https://localhost. This vite config keeps vite's hot module reloading functionality working quickly without using ngrok which is faster and more reliable.

  • You need to register the Gadget NPM registry for the @gadget-client package scope:
web/frontend
npm config set @gadget-client:registry https://registry.gadget.dev/npm
yarn config set @gadget-client:registry https://registry.gadget.dev/npm
  • The following npm modules are required when creating an app that will be embedded in the Shopify Admin:
web/frontend
npm install @gadgetinc/react @gadgetinc/react-shopify-app-bridge @gadget-client/example-app
yarn add @gadgetinc/react @gadgetinc/react-shopify-app-bridge @gadget-client/example-app

Make sure to replace `example-app` with your app's package name!

  • To deploy your frontend using hosting platforms such as Vercel, Heroku or Netlify, you will need to add a new file web/frontend/.npmrc to help point to the Gadget registry.
web/frontend/.npmrc
@gadget-client:registry=https://registry.gadget.dev/npm
  • The next step is to set up your Gadget client in the application. You can use this client to make requests to your Gadget application. You can create a new file in your project, and add the following code:
web/frontend/api.js
JavaScript
import { Client } from "@gadget-client/example-app";
export const api = new Client();
import { Client } from "@gadget-client/example-app";
export const api = new Client();
  • Now you need to set up the Provider in web/frontend/App.jsx. We can also use the useGadget hook to ensure we are authenticated before we make requests using the API. Here is a small snippet as an example:
web/frontend/App.jsx
React
1import { AppType, Provider as GadgetProvider, useGadget } from "@gadgetinc/react-shopify-app-bridge";
2import { api } from "./api";
3import { PolarisProvider } from "./components";
4
5/**
6 * Gadget's Provider takes care of App Bridge authentication, you do not need Shopify's default AppBridgeProvider.
7 */
8export default function App() {
9 return (
10 <GadgetProvider type={AppType.Embedded} shopifyApiKey={process.env["SHOPIFY_API_KEY"]!} api={api}>
11 <PolarisProvider>
12 <EmbeddedApp />
13 </PolarisProvider>
14 </GadgetProvider>
15 );
16}
17
18// This is where we make sure we have auth'd with AppBridge
19// Once we have authenticated, we can render our app!
20// Feel free to use the default page navigation that Shopify's CLI sets up for you
21// example here - https://github.com/gadget-inc/examples/blob/main/packages/shopify-cli-embedded/web/frontend/App.jsx
22function EmbeddedApp() {
23 // we use `isAuthenticated` to render pages once the OAuth flow is complete!
24 const { isAuthenticated } = useGadget();
25 return isAuthenticated ? <span>Hello, world!</span> : <span>Authenticating...</span>;
26}
1import { AppType, Provider as GadgetProvider, useGadget } from "@gadgetinc/react-shopify-app-bridge";
2import { api } from "./api";
3import { PolarisProvider } from "./components";
4
5/**
6 * Gadget's Provider takes care of App Bridge authentication, you do not need Shopify's default AppBridgeProvider.
7 */
8export default function App() {
9 return (
10 <GadgetProvider type={AppType.Embedded} shopifyApiKey={process.env["SHOPIFY_API_KEY"]!} api={api}>
11 <PolarisProvider>
12 <EmbeddedApp />
13 </PolarisProvider>
14 </GadgetProvider>
15 );
16}
17
18// This is where we make sure we have auth'd with AppBridge
19// Once we have authenticated, we can render our app!
20// Feel free to use the default page navigation that Shopify's CLI sets up for you
21// example here - https://github.com/gadget-inc/examples/blob/main/packages/shopify-cli-embedded/web/frontend/App.jsx
22function EmbeddedApp() {
23 // we use `isAuthenticated` to render pages once the OAuth flow is complete!
24 const { isAuthenticated } = useGadget();
25 return isAuthenticated ? <span>Hello, world!</span> : <span>Authenticating...</span>;
26}

If you are looking for examples of how to use our API client, visit our examples repository.

Next steps 

Once you've updated your Shopify CLI web folder to function as an external frontend, you can start building your app! See the Shopify connection guide for more information on building apps for Shopify with Gadget.

Deployment 

If using the Shopify CLI's web folder as the frontend for your application, you'll need to deploy it to a hosting platform. See the External Frontends guide for more information on deploying your frontend elsewhere.

Shopify App Bridge V4 support 

Gadget supports using Shopify's App Bridge V4, which is the first version from Shopify to use their new CDN-based delivery mechanism. Read more about the App Bridge in Shopify's docs.

Requirements 

When using version 4 of Shopify's App Bridge, applications must depend on 3 pieces:

  • a <script/> tag to import the code for the app bridge from Shopify's CDN (automatically added at runtime by Gadget)
  • the @shopify/app-bridge-react package from npm, version 4 or higher
  • the @gadgetinc/react-shopify-app-bridge package from npm, version 0.14 or higher

If you were previously using the @shopify/app-bridge package from npm, it must be removed.

CDN Script Tag 

Shopify requires that the basic code for the app bridge is required from their CDN, instead of bundled into your application. This allows Shopify to ship changes to the app bridge like bug fixes and performance improvements without requiring you to redeploy your application. The CDN-based approach is what Shopify endorses, is required for the Built For Shopify badge, and the only version of the app-bridge still receiving updates. Gadget recommends adopting this CDN-based approach for all Shopify applications.

Gadget will automatically insert the <script/> tag to import the App Bridge from Shopify's CDN when using @gadgetinc/react-shopify-app-bridge version 0.14 or later.

Upgrade procedure 

For existing apps on older versions who wish to upgrade, follow these steps:

  1. Upgrade the @shopify/app-bridge-react package to 4.x.x by installing the latest version. For reference on using terminal commands within Gadget check out our guide here.
Run in the Gadget command palette
yarn
yarn upgrade @shopify/app-bridge-react@latest
  1. Upgrade the @gadgetinc/react-shopify-app-bridge package to 0.14.1 by installing the latest version.
Run in the Gadget command palette
yarn
yarn upgrade @gadgetinc/react-shopify-app-bridge@latest
  1. Now remove @shopify/app-bridge dependency by uninstalling it.
Run in the Gadget command palette
yarn
yarn remove @shopify/app-bridge
  1. Finally follow Shopify's guide here on updating your components and hooks to work with Shopify App Bridge V4.

Resulting code 

Each app's code for working with the new library may need to be different, depending on which hooks are being used. As an example, here's the base code for the web/components/App.jsx file for the new version of the Shopify App Bridge:

Before

web/components/App.jsx
React
1import { AppType, Provider as GadgetProvider, useGadget } from "@gadgetinc/react-shopify-app-bridge";
2import { NavigationMenu } from "@shopify/app-bridge-react";
3import { Page, Spinner, Text } from "@shopify/polaris";
4import { useEffect, useMemo } from "react";
5import { Outlet, Route, RouterProvider, createBrowserRouter, createRoutesFromElements, useLocation, useNavigate } from "react-router";
6import Index from "../routes/index";
7import AboutPage from "../routes/about";
8import { api } from "../api";
9
10function Error404() {
11 const navigate = useNavigate();
12 const location = useLocation();
13
14 useEffect(() => {
15 if (location.pathname === new URL(process.env["GADGET_PUBLIC_SHOPIFY_APP_URL"]).pathname) return navigate("/", { replace: true });
16 }, [location.pathname]);
17
18 return <div>404 not found</div>;
19}
20
21function App() {
22 const router = createBrowserRouter(
23 createRoutesFromElements(
24 <Route path="/" element={<Layout />}>
25 <Route index element={<Index />} />
26 <Route path="/about" element={<AboutPage />} />
27 <Route path="*" element={<Error404 />} />
28 </Route>
29 )
30 );
31
32 return (
33 <>
34 <RouterProvider router={router} />
35 </>
36 );
37}
38
39function Layout() {
40 const navigate = useNavigate();
41 const location = useLocation();
42 const history = useMemo(() => ({ replace: (path) => navigate(path, { replace: true }) }), [navigate]);
43
44 const appBridgeRouter = useMemo(
45 () => ({
46 location,
47 history,
48 }),
49 [location, history]
50 );
51
52 return (
53 <GadgetProvider type={AppType.Embedded} shopifyApiKey={window.gadgetConfig.apiKeys.shopify} api={api} router={appBridgeRouter}>
54 <AuthenticatedApp />
55 </GadgetProvider>
56 );
57}
58
59function AuthenticatedApp() {
60 // we use `isAuthenticated` to render pages once the OAuth flow is complete!
61 const { isAuthenticated, loading } = useGadget();
62 if (loading) {
63 return (
64 <div
65 style={{
66 display: "flex",
67 justifyContent: "center",
68 alignItems: "center",
69 height: "100%",
70 width: "100%",
71 }}
72 >
73 <Spinner accessibilityLabel="Spinner example" size="large" />
74 </div>
75 );
76 }
77 return isAuthenticated ? <EmbeddedApp /> : <UnauthenticatedApp />;
78}
79
80function EmbeddedApp() {
81 return (
82 <>
83 <Outlet />
84 <NavigationMenu
85 navigationLinks={[
86 {
87 label: "Shop Information",
88 destination: "/",
89 },
90 {
91 label: "About",
92 destination: "/about",
93 },
94 ]}
95 />
96 </>
97 );
98}
99
100function UnauthenticatedApp() {
101 return (
102 <Page title="App">
103 <Text variant="bodyMd" as="p">
104 App can only be viewed in the Shopify Admin.
105 </Text>
106 </Page>
107 );
108}
109
110export default App;

After

web/components/App.jsx
React
1import { AppType, Provider as GadgetProvider, useGadget } from "@gadgetinc/react-shopify-app-bridge";
2import { NavMenu } from "@shopify/app-bridge-react";
3import { Box, Card, Page, Spinner, Text } from "@shopify/polaris";
4import { useEffect } from "react";
5import { Link, Outlet, Route, RouterProvider, createBrowserRouter, createRoutesFromElements, useLocation, useNavigate } from "react-router";
6import { api } from "../api";
7import AboutPage from "../routes/about";
8import Index from "../routes/index";
9import "./App.css";
10
11function Error404() {
12 const navigate = useNavigate();
13 const location = useLocation();
14
15 useEffect(() => {
16 const appURL = process.env.GADGET_PUBLIC_SHOPIFY_APP_URL;
17
18 if (appURL && location.pathname === new URL(appURL).pathname) {
19 navigate("/", { replace: true });
20 }
21 }, [location.pathname]);
22
23 return <div>404 not found</div>;
24}
25
26function App() {
27 const router = createBrowserRouter(
28 createRoutesFromElements(
29 <Route path="/" element={<Layout />}>
30 <Route index element={<Index />} />
31 <Route path="/about" element={<AboutPage />} />
32 <Route path="*" element={<Error404 />} />
33 </Route>
34 )
35 );
36
37 return (
38 <>
39 <RouterProvider router={router} />
40 </>
41 );
42}
43
44function Layout() {
45 return (
46 <GadgetProvider type={AppType.Embedded} shopifyApiKey={window.gadgetConfig.apiKeys.shopify} api={api}>
47 <AuthenticatedApp />
48 </GadgetProvider>
49 );
50}
51
52function AuthenticatedApp() {
53 // we use `isAuthenticated` to render pages once the OAuth flow is complete!
54 const { isAuthenticated, loading } = useGadget();
55 if (loading) {
56 return (
57 <div
58 style={{
59 display: "flex",
60 justifyContent: "center",
61 alignItems: "center",
62 height: "100%",
63 width: "100%",
64 }}
65 >
66 <Spinner accessibilityLabel="Spinner example" size="large" />
67 </div>
68 );
69 }
70 return isAuthenticated ? <EmbeddedApp /> : <UnauthenticatedApp />;
71}
72
73function EmbeddedApp() {
74 return (
75 <>
76 <Outlet />
77 <NavMenu>
78 <Link to="/" rel="home">
79 Shop Information
80 </Link>
81 <Link to="/about">About</Link>
82 </NavMenu>
83 </>
84 );
85}
86
87function UnauthenticatedApp() {
88 return (
89 <Page>
90 <div style={{ height: "80px" }}>
91 <Card padding="500">
92 <Text variant="headingLg" as="h1">
93 App must be viewed in the Shopify Admin
94 </Text>
95 </Card>
96 </div>
97 </Page>
98 );
99}
100
101export default App;
1import { AppType, Provider as GadgetProvider, useGadget } from "@gadgetinc/react-shopify-app-bridge";
2import { NavMenu } from "@shopify/app-bridge-react";
3import { Box, Card, Page, Spinner, Text } from "@shopify/polaris";
4import { useEffect } from "react";
5import { Link, Outlet, Route, RouterProvider, createBrowserRouter, createRoutesFromElements, useLocation, useNavigate } from "react-router";
6import { api } from "../api";
7import AboutPage from "../routes/about";
8import Index from "../routes/index";
9import "./App.css";
10
11function Error404() {
12 const navigate = useNavigate();
13 const location = useLocation();
14
15 useEffect(() => {
16 const appURL = process.env.GADGET_PUBLIC_SHOPIFY_APP_URL;
17
18 if (appURL && location.pathname === new URL(appURL).pathname) {
19 navigate("/", { replace: true });
20 }
21 }, [location.pathname]);
22
23 return <div>404 not found</div>;
24}
25
26function App() {
27 const router = createBrowserRouter(
28 createRoutesFromElements(
29 <Route path="/" element={<Layout />}>
30 <Route index element={<Index />} />
31 <Route path="/about" element={<AboutPage />} />
32 <Route path="*" element={<Error404 />} />
33 </Route>
34 )
35 );
36
37 return (
38 <>
39 <RouterProvider router={router} />
40 </>
41 );
42}
43
44function Layout() {
45 return (
46 <GadgetProvider type={AppType.Embedded} shopifyApiKey={window.gadgetConfig.apiKeys.shopify} api={api}>
47 <AuthenticatedApp />
48 </GadgetProvider>
49 );
50}
51
52function AuthenticatedApp() {
53 // we use `isAuthenticated` to render pages once the OAuth flow is complete!
54 const { isAuthenticated, loading } = useGadget();
55 if (loading) {
56 return (
57 <div
58 style={{
59 display: "flex",
60 justifyContent: "center",
61 alignItems: "center",
62 height: "100%",
63 width: "100%",
64 }}
65 >
66 <Spinner accessibilityLabel="Spinner example" size="large" />
67 </div>
68 );
69 }
70 return isAuthenticated ? <EmbeddedApp /> : <UnauthenticatedApp />;
71}
72
73function EmbeddedApp() {
74 return (
75 <>
76 <Outlet />
77 <NavMenu>
78 <Link to="/" rel="home">
79 Shop Information
80 </Link>
81 <Link to="/about">About</Link>
82 </NavMenu>
83 </>
84 );
85}
86
87function UnauthenticatedApp() {
88 return (
89 <Page>
90 <div style={{ height: "80px" }}>
91 <Card padding="500">
92 <Text variant="headingLg" as="h1">
93 App must be viewed in the Shopify Admin
94 </Text>
95 </Card>
96 </div>
97 </Page>
98 );
99}
100
101export default App;

If main.jsx still references BrowserRouter, it should be removed as it will not be compatible with RouterProvider in App.jsx

Was this page helpful?