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.
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 templates for Shopify is in SSR mode. 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:
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.
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:
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. 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:
Remix route file
React
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,
});
};
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:
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 (
<div>
<Form method="post">
<input type="hidden" name="csrfToken" value={csrfToken} />
<label htmlFor="name">Name:</label>
<input type="text" id="name" name="name" />
<button type="submit">Add student</button>
</Form>
</div>
);
}
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 (
<div>
<Form method="post">
<input type="hidden" name="csrfToken" value={csrfToken} />
<label htmlFor="name">Name:</label>
<input type="text" id="name" name="name" />
<button type="submit">Add student</button>
</Form>
</div>
);
}
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={...} />:
Then to access it outside of the root.jsx file, you could use the useOutletContext hook from Remix like this:
Remix route file
React
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 }>();
}
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 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
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,
});
};
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,
});
};