# Building with Remix [Framework v1.2.0+](https://docs.gadget-canary.xyz/guides/gadget-framework)  [Remix](https://remix.run/) 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. Gadget supports Remix for building app frontends. Hot module reloading (HMR) works out of the box while developing, and you can [deploy to production](https://docs.gadget-canary.xyz/guides/environments/deployment) 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 templates for Shopify is in SSR mode. You can also manually migrate between the two configurations. 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](https://remix.run/docs/en/main/file-conventions/root) 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](https://docs.gadget-canary.xyz/guides/development-tools/environment-variables#exposing-environment-variables-to-frontend-code). To provide this, Gadget uses a placeholder script tag containing `/* --GADGET_CONFIG-- */` to dynamically inject the configuration into the `window` object. This ` ``` 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](https://remix.run/docs/en/main/guides/spa-mode) 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](https://remix.run/docs/en/main/file-conventions/routes#nested-routes) 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](https://docs.gadget-canary.xyz/guides/frontend/building-frontends). ## 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: * React hooks provided by `@gadget-client/react` * [autocomponents](https://docs.gadget-canary.xyz/guides/frontend/autocomponents) ### Reading data with `loader` functions  In SSR mode, you can use the `loader` function from Remix to fetch data on the 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](https://docs.gadget-canary.xyz/guides/actions/code#action-context). This allows you to interact with your Gadget backend using `context.api` and pass data to your frontend. For example, you might have a `loader` function that fetches model data like this: ```tsx import { LoaderFunctionArgs, json } from "@remix-run/node"; export const loader = async ({ context, request }: LoaderFunctionArgs) => { // access the currentShopId in the context const shopId = context.connections.shopify.currentShopId; if (shopId === undefined) { throw new Error("Could not load current Shop"); } // use context.api to interact with your backend const shop = await context.api.shopifyShop.findOne(shopId.toString()); // return the data you want to pass to the frontend return json({ // use context to access environment variables GADGET_ENV: context.gadgetConfig.env.GADGET_ENV, shop, }); }; ``` The `context` object also includes a `logger` for writing structured logs: ```tsx export const loader = async ({ context }: LoaderFunctionArgs) => { context.logger.info({ shopId: context.connections.shopify.currentShopId }, "loading shop data"); // ... }; ``` ### 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: ```tsx import { redirect, ActionFunctionArgs } from "@remix-run/node"; // ... other imports export const action = async ({ context, request }: ActionFunctionArgs) => { const student = await context.api.student.create({ name: "John Doe", email: "john.doe@example.com", }); // ... additional action logic // redirect to the home page return redirect("/"); }; ``` 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](https://docs.gadget-canary.xyz/guides/plugins/authentication/helpers#csrftoken) on the `session` object, which you can . ```tsx import { redirect, ActionFunctionArgs } from "@remix-run/node"; import { Form, useOutletContext } from "@remix-run/react"; // Action function to handle form submission export const action = async ({ request, context }: ActionFunctionArgs) => { const formData = await request.formData(); const name = formData.get("name")?.toString(); // Create new student using Gadget API await context.api.student.create({ name }); // Redirect to students list on success return redirect("/students"); }; export default function NewStudent() { // get the csrfToken from the outlet context const { csrfToken } = useOutletContext<{ csrfToken: string }>(); // add the csrfToken to the form as a hidden input field return (
); } ``` #### 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 ``: ```tsx import { GadgetConfig } from "gadget-server"; import { json, LoaderFunctionArgs } from "@remix-run/node"; import { useLoaderData, Outlet } from "@remix-run/react"; export const loader = async ({ context }: LoaderFunctionArgs) => { const { gadgetConfig, session } = context; // ... additional loader logic // return the gadgetConfig to pass to the frontend return json({ gadgetConfig, csrfToken: session?.get("csrfToken"), }); }; export default function App() { const { gadgetConfig, csrfToken } = useLoaderData<{ gadgetConfig: GadgetConfig; csrfToken: string }>(); return ( ); } ``` Then to access it outside of the `root.jsx` file, you could use the `useOutletContext` hook from Remix like this: ```tsx import { useOutletContext } from "@remix-run/react"; import { GadgetConfig } from "gadget-server"; export default function () { // useOutletContext to get access to the gadgetConfig const { gadgetConfig, csrfToken } = useOutletContext<{ gadgetConfig: GadgetConfig; csrfToken: string }>(); } ``` Check out the Remix **reference** on [outlet context](https://remix.run/docs/en/main/hooks/use-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: ```tsx import { LoaderFunctionArgs, json } from "@remix-run/node"; export const loader = async ({ context, request }: LoaderFunctionArgs) => { // grab all widgets with an admin role and bypassing tenancy const widgets = await context.api.actAsAdmin.widget.findMany(); // return the data you want to pass to the frontend return json({ widgets, }); }; ``` ## 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`: ```bash // in add the following Remix packages yarn add @remix-run/node @remix-run/react ``` And then install the following dev dependency: ```bash // in 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: ```typescript import { defineConfig } from "vite"; import { gadget } from "gadget-server/vite"; import { remixViteOptions } from "gadget-server/remix"; import { vitePlugin as remix } from "@remix-run/dev"; export default defineConfig({ plugins: [ gadget(), remix({ ...remixViteOptions, ssr: false, }), ], }); ``` The [`remixViteOptions` object](https://docs.gadget-canary.xyz/reference/gadget-server#remixviteoptions) provides some pre-defined options to make it easier to set up your Vite app to work with Remix. The [`gadget` Vite plugin](https://docs.gadget-canary.xyz/reference/gadget-server#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. ```json // in package.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: ```typescript import { YourAppClient } from "@gadget-client/your-app"; export const api = new YourAppClient({ 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: ```typescript import { YourAppClient } from "@gadget-client/your-app"; export const api = new YourAppClient(); ``` 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](https://remix.run/docs/en/main/file-conventions/-client) 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: ```tsx import { Links, Scripts, ScrollRestoration } from "@remix-run/react"; import { AppProvider, Spinner } from "@shopify/polaris"; import enTranslations from "@shopify/polaris/locales/en.json"; import { AppType, Provider as GadgetProvider } from "@gadgetinc/react-shopify-app-bridge"; import appStylesHref from "./components/App.css?url"; import polarisStyles from "@shopify/polaris/build/esm/styles.css?url"; import { api } from "./api"; import { AuthenticatedApp } from "./components/App"; export const links = () => [ { rel: "stylesheet", href: appStylesHref }, { rel: "stylesheet", href: polarisStyles, }, ]; export const Layout = ({ children }: { children: React.ReactNode }) => { return ( {children} ); }; export default function App() { return ( ); } export function HydrateFallback() { return ; } ``` For more information on the root route, see the [Remix documentation](https://remix.run/docs/en/main/file-conventions/root). ### 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: ```tsx import { useGadget } from "@gadgetinc/react-shopify-app-bridge"; import { Outlet, Link } from "@remix-run/react"; import { NavMenu } from "@shopify/app-bridge-react"; import { Spinner, Page, Card, Text, Box } from "@shopify/polaris"; export function AuthenticatedApp() { // we use `isAuthenticated` to render pages once the OAuth flow is complete! const { isAuthenticated, loading } = useGadget(); if (loading) { return ; } return isAuthenticated ? : ; } function Unauthenticated() { return (
App must be viewed in the Shopify Admin Edit this page: web/components/App
); } function EmbeddedApp() { return ( <> Shop Information ); } ``` ### 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](https://remix.run/docs/en/main/discussion/routes#conventional-route-configuration).