Building with Remix 

Remix is a full-stack web framework built on top of React Router and Vite, and supports advanced features like server-side rendering (SSR), seamless server and browser communication using loader and action functions, and file-based frontend routing.

Remix is also the framework that is used for Shopify's CLI applications.

Available in Gadget framework version 1.2+

Remix support in Gadget was added with Gadget framework version 1.2. If you're using an older version of Gadget, you'll need to upgrade your framework version to run Remix apps in Gadget.

Gadget supports Remix for building app frontends. Hot module reloading (HMR) works out of the box while developing, and you can deploy to production in a single click.

Getting started with Remix 

Remix can be run in both SSR and SPA (single page application) mode.

By default, Gadget's Remix template for Shopify is in SPA mode, and the Remix template for web apps uses SSR. You can also manually migrate between the two configurations.

Cold boot times for SSR

Gadget frontends are serverless and resources used are scaled to zero when not in use. This means that a frontend not in use needs to be started fresh, referred to as a cold boot. This cold boot time may negatively impact web vitals like LCP.

Contact us if you are building in SSR model and need to minimize cold boot times.

Default frontend configuration 

Remix frontends in Gadget have some defaults that are designed to make it easier to get started.

  • vite.config.mjs is used to define the Vite configuration for your frontend.
  • web/root.jsx is used to define the root route and is used to power the frontend.
  • The web/routes directory is used to define your frontend routes.
  • web/api.js defines the API client for your app.

Remix frontends in Gadget are built with Vite and configuration is defined in the vite.config.mjs file. A gadget() Vite plugin is used to inject configuration to the Vite config based on the frontend framework type, and a remix() Vite plugin is used to inject configuration for Remix.

Dynamic configuration injection in SPA mode 

Gadget provides a gadgetConfig object which contains useful data about your application, including frontend environment variables. To provide this, Gadget uses a placeholder script tag containing /* --GADGET_CONFIG-- */ to dynamically inject the configuration into the window object. This <script> tag is included in the web/root.jsx file:

inside the <head> element of web/root.jsx
React
<script suppressHydrationWarning>/* --GADGET_CONFIG-- */</script>
<script suppressHydrationWarning>/* --GADGET_CONFIG-- */</script>

Gadget modifies the HTML content dynamically which will cause Remix to throw hydration errors. The suppressHydrationWarning prop is included to suppress these errors.

Don't modify this script tag!

Remix SPA mode 

SPA mode in Remix brings file-based routing and nested routes to your frontends. This means that your frontend routing is determined by the files in the app/routes directory. See the Remix documentation for more information on file-based routing and nested routes.

Reading and writing data 

Gadget's React hooks and autocomponents can be used to interact with your backend in Remix's SPA mode.

This does not differ from using Gadget's React hooks and autocomponents in other contexts, and you can read more about using them in our frontend docs.

Migrating from SPA to SSR 

To migrate a Remix frontend from SPA mode to SSR, you need to make a few changes to your app.

  1. Install the isbot package if you are not planning to customize your entry.server file.
  2. Remove the ssr: false flag from the remix plugin in the vite.config.mjs file.
  3. Remove the /* --GADGET_CONFIG-- */ script tag from web/root.jsx.
  4. If you use window.gadgetConfig in your frontend code, use a loader function to pass the gadgetConfig to the frontend.
  5. Update your routes to use the loader function to fetch data and the action function to submit data.

Updating your routes is not required, but it is recommended to do so to take advantage of the benefits of SSR.

Additional steps for migrating Shopify SPA apps to SSR 

If you are migrating a Shopify Remix SPA app to SSR, you need to update the following files so the Shopify App Bridge works correctly.

  1. Update the web/components/App.jsx file:
  • Remove the use of the window object.
  • Export the existing Unauthenticated function. This component will be used in the next step.
  • Remove the use of the isAuthenticated value returned from the useGadget hook.

Below is an example of what the updated web/components/App.jsx file could look like if you are migrating the default Gadget Shopify Remix SPA app to SSR mode:

web/components/App.jsx
React
1import { useGadget } from "@gadgetinc/react-shopify-app-bridge";
2import { Outlet, Link } from "@remix-run/react";
3import { Spinner, Page, Card, Text, Box } from "@shopify/polaris";
4import { FullPageSpinner } from "./FullPageSpinner";
5import { NavMenu } from "./NavMenu";
6
7export function AuthenticatedApp() {
8 const { loading } = useGadget();
9
10 if (loading) {
11 return <FullPageSpinner />;
12 }
13
14 return <EmbeddedApp />;
15}
16
17function EmbeddedApp() {
18 return <Outlet />;
19}
20
21export function Unauthenticated() {
22 return (
23 <Page>
24 <div style={{ height: "80px" }}>
25 <Card padding="500">
26 <Text variant="headingLg" as="h1">
27 App must be viewed in the Shopify Admin
28 </Text>
29 </Card>
30 </div>
31 </Page>
32 );
33}
1import { useGadget } from "@gadgetinc/react-shopify-app-bridge";
2import { Outlet, Link } from "@remix-run/react";
3import { Spinner, Page, Card, Text, Box } from "@shopify/polaris";
4import { FullPageSpinner } from "./FullPageSpinner";
5import { NavMenu } from "./NavMenu";
6
7export function AuthenticatedApp() {
8 const { loading } = useGadget();
9
10 if (loading) {
11 return <FullPageSpinner />;
12 }
13
14 return <EmbeddedApp />;
15}
16
17function EmbeddedApp() {
18 return <Outlet />;
19}
20
21export function Unauthenticated() {
22 return (
23 <Page>
24 <div style={{ height: "80px" }}>
25 <Card padding="500">
26 <Text variant="headingLg" as="h1">
27 App must be viewed in the Shopify Admin
28 </Text>
29 </Card>
30 </div>
31 </Page>
32 );
33}
  1. Update the web/root.jsx file:
  • Return the Shopify API key from the loader function and pass it to the GadgetProvider.
  • Pass the location object from Remix's useLocation hook to the GadgetProvider.
  • Remove the "shopify-api-key" meta tag from the meta function.

Below is an example of what the web/root.jsx file could look like:

web/root.jsx
React
1import { Meta, Links, Scripts, ScrollRestoration, useLoaderData, useLocation } from "@remix-run/react";
2import { AppProvider } from "@shopify/polaris";
3import enTranslations from "@shopify/polaris/locales/en.json";
4import { AppType, Provider as GadgetProvider } from "@gadgetinc/react-shopify-app-bridge";
5import appStylesHref from "./app.css?url";
6import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";
7import { Suspense } from "react";
8import { api } from "./api";
9import { AdaptorLink } from "./components/AdaptorLink";
10import { AuthenticatedApp, Unauthenticated } from "./components/App";
11import { FullPageSpinner } from "./components/FullPageSpinner";
12import { LoaderFunctionArgs, json } from "@remix-run/node";
13import { Client } from "@gadget-client/example-app";
14
15export const links = () => [
16 { rel: "stylesheet", href: appStylesHref },
17 {
18 rel: "stylesheet",
19 href: polarisStyles,
20 },
21 {
22 rel: "stylesheet",
23 href: "https://assets.gadget.dev/assets/reset.min.css",
24 },
25];
26
27export const meta = () => [
28 { charset: "utf-8" },
29 {
30 name: "viewport",
31 content: "width=device-width, initial-scale=1",
32 },
33];
34
35export const loader = async ({ context }: LoaderFunctionArgs) => {
36 return json({
37 shopifyApiKey: context.gadgetConfig.apiKeys.shopify,
38 shopifyInstallState: context.gadgetConfig.shopifyInstallState,
39 GADGET_ENV: context.gadgetConfig.env.GADGET_ENV,
40 });
41};
42
43export default function App() {
44 const { shopifyApiKey, shopifyInstallState, GADGET_ENV } = useLoaderData<typeof loader>();
45 const location = useLocation();
46 return (
47 <html lang="en">
48 <head>
49 <meta name="shopify-api-key" content={shopifyApiKey} />
50 <script src="https://cdn.shopify.com/shopifycloud/app-bridge.js" />
51 <link rel="stylesheet" href="https://assets.gadget.dev/assets/reset.min.css" />
52 <Meta />
53 <Links />
54 </head>
55 <body>
56 <AppProvider i18n={enTranslations}>
57 <GadgetProvider
58 type={AppType.Embedded}
59 shopifyApiKey={shopifyApiKey}
60 api={new Client({ environment: GADGET_ENV })}
61 location={location}
62 shopifyInstallState={shopifyInstallState}
63 >
64 <Suspense>{shopifyInstallState ? <AuthenticatedApp /> : <Unauthenticated />}</Suspense>
65 </GadgetProvider>
66 </AppProvider>
67 <ScrollRestoration />
68 <Scripts />
69 </body>
70 </html>
71 );
72 }
1import { Meta, Links, Scripts, ScrollRestoration, useLoaderData, useLocation } from "@remix-run/react";
2import { AppProvider } from "@shopify/polaris";
3import enTranslations from "@shopify/polaris/locales/en.json";
4import { AppType, Provider as GadgetProvider } from "@gadgetinc/react-shopify-app-bridge";
5import appStylesHref from "./app.css?url";
6import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";
7import { Suspense } from "react";
8import { api } from "./api";
9import { AdaptorLink } from "./components/AdaptorLink";
10import { AuthenticatedApp, Unauthenticated } from "./components/App";
11import { FullPageSpinner } from "./components/FullPageSpinner";
12import { LoaderFunctionArgs, json } from "@remix-run/node";
13import { Client } from "@gadget-client/example-app";
14
15export const links = () => [
16 { rel: "stylesheet", href: appStylesHref },
17 {
18 rel: "stylesheet",
19 href: polarisStyles,
20 },
21 {
22 rel: "stylesheet",
23 href: "https://assets.gadget.dev/assets/reset.min.css",
24 },
25];
26
27export const meta = () => [
28 { charset: "utf-8" },
29 {
30 name: "viewport",
31 content: "width=device-width, initial-scale=1",
32 },
33];
34
35export const loader = async ({ context }: LoaderFunctionArgs) => {
36 return json({
37 shopifyApiKey: context.gadgetConfig.apiKeys.shopify,
38 shopifyInstallState: context.gadgetConfig.shopifyInstallState,
39 GADGET_ENV: context.gadgetConfig.env.GADGET_ENV,
40 });
41};
42
43export default function App() {
44 const { shopifyApiKey, shopifyInstallState, GADGET_ENV } = useLoaderData<typeof loader>();
45 const location = useLocation();
46 return (
47 <html lang="en">
48 <head>
49 <meta name="shopify-api-key" content={shopifyApiKey} />
50 <script src="https://cdn.shopify.com/shopifycloud/app-bridge.js" />
51 <link rel="stylesheet" href="https://assets.gadget.dev/assets/reset.min.css" />
52 <Meta />
53 <Links />
54 </head>
55 <body>
56 <AppProvider i18n={enTranslations}>
57 <GadgetProvider
58 type={AppType.Embedded}
59 shopifyApiKey={shopifyApiKey}
60 api={new Client({ environment: GADGET_ENV })}
61 location={location}
62 shopifyInstallState={shopifyInstallState}
63 >
64 <Suspense>{shopifyInstallState ? <AuthenticatedApp /> : <Unauthenticated />}</Suspense>
65 </GadgetProvider>
66 </AppProvider>
67 <ScrollRestoration />
68 <Scripts />
69 </body>
70 </html>
71 );
72 }

Remix SSR 

SSR in Remix allows you to render your frontend on the server. SSR has multiple benefits:

  • improved performance by reducing the time it takes to load the page
  • better SEO because search engines can index the page content returned from the server
  • data fetching with loader functions to reduce client-side requests
  • form submissions with action functions, allowing for full-stack actions without client-side JavaScript
  • code splitting and reduced bundle size

There are some Gadget-provided tools that will not be rendered server-side, including:

Reading data with loader functions 

In SSR mode, you could use the loader function from Remix to fetch the data on server-side.

Your Gadget app's context object is available in the loader function, and is the same as the context you get when you create an action in Gadget. This allows you to interact with your Gadget backend and pass data to your frontend.

For example, you might have a loader function that fetches the model data like this:

Remix route file
React
1import { LoaderFunctionArgs, json } from "@remix-run/node";
2
3export const loader = async ({ context, request }: LoaderFunctionArgs) => {
4 // access the currentShopId in the context
5 const shopId = context.connections.shopify.currentShopId;
6 if (shopId === undefined) {
7 throw new Error("Could not load current Shop");
8 }
9
10 // use context.api to interact with your backend
11 const shop = await context.api.shopifyShop.findOne(shopId.toString());
12
13 // return the data you want to pass to the frontend
14 return json({
15 // use context to access environment variables
16 GADGET_ENV: context.gadgetConfig.env.GADGET_ENV,
17 shop,
18 });
19};
1import { LoaderFunctionArgs, json } from "@remix-run/node";
2
3export const loader = async ({ context, request }: LoaderFunctionArgs) => {
4 // access the currentShopId in the context
5 const shopId = context.connections.shopify.currentShopId;
6 if (shopId === undefined) {
7 throw new Error("Could not load current Shop");
8 }
9
10 // use context.api to interact with your backend
11 const shop = await context.api.shopifyShop.findOne(shopId.toString());
12
13 // return the data you want to pass to the frontend
14 return json({
15 // use context to access environment variables
16 GADGET_ENV: context.gadgetConfig.env.GADGET_ENV,
17 shop,
18 });
19};

The comment block above the loader function is used to provide the types for the function parameters like context. This is optional but can help with type inference in the editor.

Writing data with action functions 

The action function in Remix is used to submit data to the backend when you are using SSR.

You can use the context object in the action function to call your actions.

For example, you might have an action function that creates new students after a form is submitted:

web/routes/students.create.jsx
React
1import { redirect, ActionFunctionArgs } from "@remix-run/node";
2// ... other imports
3
4export const action = async ({ context, request }: ActionFunctionArgs) => {
5 const student = await context.api.student.create({
6 name: "John Doe",
7 email: "[email protected]",
8 });
9
10 // ... additional action logic
11
12 // redirect to the home page
13 return redirect("/");
14};
1import { redirect, ActionFunctionArgs } from "@remix-run/node";
2// ... other imports
3
4export const action = async ({ context, request }: ActionFunctionArgs) => {
5 const student = await context.api.student.create({
6 name: "John Doe",
7 email: "[email protected]",
8 });
9
10 // ... additional action logic
11
12 // redirect to the home page
13 return redirect("/");
14};

Similar to the loader function, the comment block above the action function can provide type inference for the function parameters.

Submitting forms with csrfToken 

When submitting forms in SSR, you need to include the csrfToken in the form submission to prevent CSRF (cross-site request forgery) attacks. The csrfToken is available on the session object, which you can pass to the frontend with an <Outlet context={...} /> tag.

web/routes/students.create.jsx
React
1import { redirect, ActionFunctionArgs } from "@remix-run/node";
2import { Form, useOutletContext } from "@remix-run/react";
3
4// Action function to handle form submission
5export const action = async ({ request, context }: ActionFunctionArgs) => {
6 const formData = await request.formData();
7 const name = formData.get("name")?.toString();
8
9 // Create new student using Gadget API
10 await context.api.student.create({ name });
11 // Redirect to students list on success
12 return redirect("/students");
13};
14
15export default function NewStudent() {
16 // get the csrfToken from the outlet context
17 const { csrfToken } = useOutletContext<{ csrfToken: string }>();
18
19 // add the csrfToken to the form as a hidden input field
20 return (
21 <div>
22 <Form method="post">
23 <input type="hidden" name="csrfToken" value={csrfToken} />
24 <label htmlFor="name">Name:</label>
25 <input type="text" id="name" name="name" />
26 <button type="submit">Add student</button>
27 </Form>
28 </div>
29 );
30}
1import { redirect, ActionFunctionArgs } from "@remix-run/node";
2import { Form, useOutletContext } from "@remix-run/react";
3
4// Action function to handle form submission
5export const action = async ({ request, context }: ActionFunctionArgs) => {
6 const formData = await request.formData();
7 const name = formData.get("name")?.toString();
8
9 // Create new student using Gadget API
10 await context.api.student.create({ name });
11 // Redirect to students list on success
12 return redirect("/students");
13};
14
15export default function NewStudent() {
16 // get the csrfToken from the outlet context
17 const { csrfToken } = useOutletContext<{ csrfToken: string }>();
18
19 // add the csrfToken to the form as a hidden input field
20 return (
21 <div>
22 <Form method="post">
23 <input type="hidden" name="csrfToken" value={csrfToken} />
24 <label htmlFor="name">Name:</label>
25 <input type="text" id="name" name="name" />
26 <button type="submit">Add student</button>
27 </Form>
28 </div>
29 );
30}

Passing gadgetConfig to the frontend 

The context object contains a gadgetConfig object which allows you to access properties like the Gadget environment type and Shopify install state.

You can pass the gadgetConfig to the frontend with a Remix <Outlet context={...} />:

web/root.jsx
React
1import { GadgetConfig } from "gadget-server";
2import { json, LoaderFunctionArgs } from "@remix-run/node";
3import { useLoaderData, Outlet } from "@remix-run/react";
4
5export const loader = async ({ context }: LoaderFunctionArgs) => {
6 const { gadgetConfig, session } = context;
7
8 // ... additional loader logic
9
10 // return the gadgetConfig to pass to the frontend
11 return json({
12 gadgetConfig,
13 csrfToken: session?.get("csrfToken"),
14 });
15};
16
17export default function App() {
18 const { gadgetConfig, csrfToken } = useLoaderData<{ gadgetConfig: GadgetConfig; csrfToken: string }>();
19 return (
20 <html lang="en">
21 <body>
22 <Outlet context={{ gadgetConfig, csrfToken }} />
23 </body>
24 </html>
25 );
26}
1import { GadgetConfig } from "gadget-server";
2import { json, LoaderFunctionArgs } from "@remix-run/node";
3import { useLoaderData, Outlet } from "@remix-run/react";
4
5export const loader = async ({ context }: LoaderFunctionArgs) => {
6 const { gadgetConfig, session } = context;
7
8 // ... additional loader logic
9
10 // return the gadgetConfig to pass to the frontend
11 return json({
12 gadgetConfig,
13 csrfToken: session?.get("csrfToken"),
14 });
15};
16
17export default function App() {
18 const { gadgetConfig, csrfToken } = useLoaderData<{ gadgetConfig: GadgetConfig; csrfToken: string }>();
19 return (
20 <html lang="en">
21 <body>
22 <Outlet context={{ gadgetConfig, csrfToken }} />
23 </body>
24 </html>
25 );
26}

Then to access it outside of the root.jsx file, you could use the useOutletContext hook from Remix like this:

Remix route file
React
1import { useOutletContext } from "@remix-run/react";
2import { GadgetConfig } from "gadget-server";
3
4export default function () {
5 // useOutletContext to get access to the gadgetConfig
6
7 const { gadgetConfig, csrfToken } = useOutletContext<{ gadgetConfig: GadgetConfig; csrfToken: string }>();
8}
1import { useOutletContext } from "@remix-run/react";
2import { GadgetConfig } from "gadget-server";
3
4export default function () {
5 // useOutletContext to get access to the gadgetConfig
6
7 const { gadgetConfig, csrfToken } = useOutletContext<{ gadgetConfig: GadgetConfig; csrfToken: string }>();
8}

Check out the Remix reference on outlet context for more information.

actAsAdmin for bypassing tenancy 

In SSR mode, you can access your Gadget app's API client in your loader and action functions using context.api.

By default, the API client will have the role and permissions granted by the current session, which means that any requests made will have tenancy applied by default. This is different from using the API client in Gadget actions or HTTP routes, where tenancy is not applied by default.

If you need to make requests outside of the current tenancy you can use actAsAdmin in loader and action functions. For example:

example of a loader function using asAdmin
React
1import { LoaderFunctionArgs, json } from "@remix-run/node";
2export const loader = async ({ context, request }: LoaderFunctionArgs) => {
3 // grab all widgets with an admin role and bypassing tenancy
4 const widgets = await context.api.actAsAdmin.widget.findMany();
5
6 // return the data you want to pass to the frontend
7 return json({
8 widgets,
9 });
10};
1import { LoaderFunctionArgs, json } from "@remix-run/node";
2export const loader = async ({ context, request }: LoaderFunctionArgs) => {
3 // grab all widgets with an admin role and bypassing tenancy
4 const widgets = await context.api.actAsAdmin.widget.findMany();
5
6 // return the data you want to pass to the frontend
7 return json({
8 widgets,
9 });
10};

Migrating from SSR to SPA 

You can manually migrate a Remix frontend from SSR mode to SPA mode by following these steps:

  1. Add the ssr: false flag to the remix plugin in the vite.config.mjs file:
vite.config.mjs
JavaScript
1import { defineConfig } from "vite";
2import { gadget } from "gadget-server/vite";
3import { remixViteOptions } from "gadget-server/remix";
4import { vitePlugin as remix } from "@remix-run/dev";
5
6export default defineConfig({
7 plugins: [
8 gadget(),
9 remix({
10 ...remixViteOptions,
11
12 // add this
13 ssr: false,
14 }),
15 ],
16});
1import { defineConfig } from "vite";
2import { gadget } from "gadget-server/vite";
3import { remixViteOptions } from "gadget-server/remix";
4import { vitePlugin as remix } from "@remix-run/dev";
5
6export default defineConfig({
7 plugins: [
8 gadget(),
9 remix({
10 ...remixViteOptions,
11
12 // add this
13 ssr: false,
14 }),
15 ],
16});
  1. Add the /* --GADGET_CONFIG-- */ script tag to web/root.jsx.
  2. SPA mode does not support the loader and action functions. You need to use Gadget's React hooks and autocomponents to read and write data in your frontend.

Migrate from React Router to Remix 

If you would like to migrate your existing Gadget frontend app to Remix SPA mode, you can follow these steps.

Note that this sample migration is for a Shopify app.

Step 1: Install required packages 

To run Remix apps, you need the following packages in package.json:

add the following Remix packages
yarn add @remix-run/node @remix-run/react

And then install the following dev dependency:

add @remix-run/dev as a dev dependency
yarn add -D @remix-run/dev

Step 2: Update your Vite configuration 

You need to include the remix Vite plugin from @remix-run/dev as well as our Gadget Vite plugin with pre-defined options.

You can copy the following example into your vite.config.mjs file:

vite.config.mjs
JavaScript
1import { defineConfig } from "vite";
2import { gadget } from "gadget-server/vite";
3import { remixViteOptions } from "gadget-server/remix";
4import { vitePlugin as remix } from "@remix-run/dev";
5
6export default defineConfig({
7 plugins: [
8 gadget(),
9 remix({
10 ...remixViteOptions,
11 ssr: false,
12 }),
13 ],
14});
1import { defineConfig } from "vite";
2import { gadget } from "gadget-server/vite";
3import { remixViteOptions } from "gadget-server/remix";
4import { vitePlugin as remix } from "@remix-run/dev";
5
6export default defineConfig({
7 plugins: [
8 gadget(),
9 remix({
10 ...remixViteOptions,
11 ssr: false,
12 }),
13 ],
14});

The remixViteOptions object provides some pre-defined options to make it easier to set up your Vite app to work with Remix.

The gadget Vite plugin dynamically injects Vite configuration for you Gadget apps. Different configuration is injected depending on the type of frontend you are building.

Step 3: Update the build script in package.json 

We run the build script from the package.json file when deploying to production. You need to update this script to use the remix Vite plugin.

package.json
json
{
"scripts": {
"build": "NODE_ENV=production remix vite:build"
}
}

Step 4: Rename files that use the window object 

By default, your Gadget frontend has a web/api.js that uses window to get the Gadget config when setting up the API client:

web/api.js
JavaScript
import { Client } from "@gadget-client/<your-app-name>";
export const api = new Client({ environment: window.gadgetConfig.environment });
import { Client } from "@gadget-client/<your-app-name>";
export const api = new Client({ environment: window.gadgetConfig.environment });

Remove the window reference, Gadget clients will choose to create the correct client object based on whether or not it is in an SSR context or not:

web/api.js
JavaScript
import { Client } from "@gadget-client/<your-app-name>";
export const api = new Client();
import { Client } from "@gadget-client/<your-app-name>";
export const api = new Client();

Remix bundles your app server-side where there is no window object and a window is not defined error will be thrown when you load the app.

Remove any other references you have to the window object, in any of your frontend files.

Read the Remix guide on file conventions for more information.

Step 5: Set up the root route 

You need to add a web/root.jsx file that acts as the root routes in Remix.

This is how you could set up a Shopify frontend root route for Remix in SPA mode:

web/root.jsx
React
1import { Links, Scripts, ScrollRestoration } from "@remix-run/react";
2import { AppProvider, Spinner } from "@shopify/polaris";
3import enTranslations from "@shopify/polaris/locales/en.json";
4import { AppType, Provider as GadgetProvider } from "@gadgetinc/react-shopify-app-bridge";
5import appStylesHref from "./components/App.css?url";
6import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";
7import { api } from "./api";
8import { AuthenticatedApp } from "./components/App";
9
10export const links = () => [
11 { rel: "stylesheet", href: appStylesHref },
12 {
13 rel: "stylesheet",
14 href: polarisStyles,
15 },
16];
17
18export const Layout = ({ children }: { children: React.ReactNode }) => {
19 return (
20 <html lang="en">
21 <head>
22 <meta charSet="utf-8" />
23 <meta name="viewport" content="width=device-width, initial-scale=1" />
24 <link rel="stylesheet" href="https://assets.gadget.dev/assets/reset.min.css" />
25 <meta name="shopify-api-key" suppressHydrationWarning content="%SHOPIFY_API_KEY%" />
26 <script src="https://cdn.shopify.com/shopifycloud/app-bridge.js"></script>
27 <script suppressHydrationWarning>/* --GADGET_CONFIG-- */</script>
28 <Links />
29 </head>
30 <body>
31 {children}
32 <ScrollRestoration />
33 <Scripts />
34 </body>
35 </html>
36 );
37};
38
39export default function App() {
40 return (
41 <GadgetProvider type={AppType.Embedded} shopifyApiKey={window.gadgetConfig.apiKeys.shopify} api={api}>
42 <AppProvider i18n={enTranslations}>
43 <AuthenticatedApp />
44 </AppProvider>
45 </GadgetProvider>
46 );
47}
48
49export function HydrateFallback() {
50 return <Spinner />;
51}
1import { Links, Scripts, ScrollRestoration } from "@remix-run/react";
2import { AppProvider, Spinner } from "@shopify/polaris";
3import enTranslations from "@shopify/polaris/locales/en.json";
4import { AppType, Provider as GadgetProvider } from "@gadgetinc/react-shopify-app-bridge";
5import appStylesHref from "./components/App.css?url";
6import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";
7import { api } from "./api";
8import { AuthenticatedApp } from "./components/App";
9
10export const links = () => [
11 { rel: "stylesheet", href: appStylesHref },
12 {
13 rel: "stylesheet",
14 href: polarisStyles,
15 },
16];
17
18export const Layout = ({ children }: { children: React.ReactNode }) => {
19 return (
20 <html lang="en">
21 <head>
22 <meta charSet="utf-8" />
23 <meta name="viewport" content="width=device-width, initial-scale=1" />
24 <link rel="stylesheet" href="https://assets.gadget.dev/assets/reset.min.css" />
25 <meta name="shopify-api-key" suppressHydrationWarning content="%SHOPIFY_API_KEY%" />
26 <script src="https://cdn.shopify.com/shopifycloud/app-bridge.js"></script>
27 <script suppressHydrationWarning>/* --GADGET_CONFIG-- */</script>
28 <Links />
29 </head>
30 <body>
31 {children}
32 <ScrollRestoration />
33 <Scripts />
34 </body>
35 </html>
36 );
37};
38
39export default function App() {
40 return (
41 <GadgetProvider type={AppType.Embedded} shopifyApiKey={window.gadgetConfig.apiKeys.shopify} api={api}>
42 <AppProvider i18n={enTranslations}>
43 <AuthenticatedApp />
44 </AppProvider>
45 </GadgetProvider>
46 );
47}
48
49export function HydrateFallback() {
50 return <Spinner />;
51}

For more information on the root route, see the Remix documentation.

Step 6: Remove unused frontend files 

You can remove the following files as they are no longer needed:

  • index.html
  • web/main.jsx

Step 7: Rewrite App component 

Now that app setup is handled in web/root.jsx, you can simplify the web/components/App.jsx file to provide your app's UI only. This may include an AuthenticatedApp component that conditionally renders your app's UI based on the user's login state.

Here's what a web/components/App.jsx file might look like for a Shopify app:

web/components/App.jsx
React
1import { useGadget } from "@gadgetinc/react-shopify-app-bridge";
2import { Outlet, Link } from "@remix-run/react";
3import { NavMenu } from "@shopify/app-bridge-react";
4import { Spinner, Page, Card, Text, Box } from "@shopify/polaris";
5
6export function AuthenticatedApp() {
7 // we use `isAuthenticated` to render pages once the OAuth flow is complete!
8 const { isAuthenticated, loading } = useGadget();
9
10 if (loading) {
11 return <Spinner />;
12 }
13
14 return isAuthenticated ? <EmbeddedApp /> : <Unauthenticated />;
15}
16
17function Unauthenticated() {
18 return (
19 <Page>
20 <div style={{ height: "80px" }}>
21 <Card padding="500">
22 <Text variant="headingLg" as="h1">
23 App must be viewed in the Shopify Admin
24 </Text>
25 <Box paddingBlockStart="200">
26 <Text variant="bodyLg" as="p">
27 Edit this page: <a href={`/edit/${window.gadgetConfig.environment}`}>web/components/App</a>
28 </Text>
29 </Box>
30 </Card>
31 </div>
32 </Page>
33 );
34}
35
36function EmbeddedApp() {
37 return (
38 <>
39 <NavMenu>
40 <Link to="/" rel="home">
41 Shop Information
42 </Link>
43 <Link to="/about">About</Link>
44 </NavMenu>
45 <Outlet />
46 </>
47 );
48}
1import { useGadget } from "@gadgetinc/react-shopify-app-bridge";
2import { Outlet, Link } from "@remix-run/react";
3import { NavMenu } from "@shopify/app-bridge-react";
4import { Spinner, Page, Card, Text, Box } from "@shopify/polaris";
5
6export function AuthenticatedApp() {
7 // we use `isAuthenticated` to render pages once the OAuth flow is complete!
8 const { isAuthenticated, loading } = useGadget();
9
10 if (loading) {
11 return <Spinner />;
12 }
13
14 return isAuthenticated ? <EmbeddedApp /> : <Unauthenticated />;
15}
16
17function Unauthenticated() {
18 return (
19 <Page>
20 <div style={{ height: "80px" }}>
21 <Card padding="500">
22 <Text variant="headingLg" as="h1">
23 App must be viewed in the Shopify Admin
24 </Text>
25 <Box paddingBlockStart="200">
26 <Text variant="bodyLg" as="p">
27 Edit this page: <a href={`/edit/${window.gadgetConfig.environment}`}>web/components/App</a>
28 </Text>
29 </Box>
30 </Card>
31 </div>
32 </Page>
33 );
34}
35
36function EmbeddedApp() {
37 return (
38 <>
39 <NavMenu>
40 <Link to="/" rel="home">
41 Shop Information
42 </Link>
43 <Link to="/about">About</Link>
44 </NavMenu>
45 <Outlet />
46 </>
47 );
48}

Step 8: Update your routes 

Remix uses file-based routing, so you can move your existing React Router configuration into the web/routes directory.

This includes your web/routes/index.jsx file, would need to be renamed to web/routes/_index.jsx.

For information on Remix route configuration, see the Remix documentation.

Was this page helpful?