Templates
The Gadget platform comes with application templates for our three main use cases: web apps, Shopify apps, and BigCommerce apps. Each template is designed to provide a starting point for your project, with the necessary configuration and boilerplate code to get you up and running quickly.
Base templates
Base templates are the very basic starting point for your project. They have boilerplate already setup for you, using what the Gadget team regards as best practices. They are designed to be a blank canvas for you to build upon.
The available base templates are:
Template | Tenancy | Framework | SSR/SPA |
---|---|---|---|
Shopify | Public | React | SPA |
Shopify | Custom | React | SPA |
Shopify | Public | Remix | SSR |
Shopify | Custom | Remix | SSR |
BigCommerce | Public | React | SPA |
Web app | Multi-party auth | React Router v7 | SSR |
Web app | Single-party auth | React Router v7 | SSR |
Web app | No auth | React Router v7 | SSR |
SSR stands for server-side rendering, and SPA stands for single-page application.
Expanded templates
Apart from the base templates, we also have expanded templates. These templates are built on top of the base templates and include additional features and configurations. They are designed to provide a more complete starting point for your project.
You can find the expanded templates by following this link or on Github. You can also find the expanded templates from the app selection page. In the left nav, click on templates, and you will see the expanded templates.
React Router v7
Switching between SSR and SPA
This is only available when using the React Router v7 framework mode. You can switch between server-side rendering (SSR) and single-page application (SPA) mode by changing the React Router configs, adding the ssr
flag set to false
in the react-router.config
file.
import type { Config } from "@react-router/dev/config";import { reactRouterConfigOptions } from "gadget-server/react-router";export default { ...reactRouterConfigOptions, ssr: false } satisfies Config;
import type { Config } from "@react-router/dev/config";import { reactRouterConfigOptions } from "gadget-server/react-router";export default { ...reactRouterConfigOptions, ssr: false } satisfies Config;
For more information, take a look at the React Router v7 documentation.
From here, you will need to change the way that data loads, in your routes, from the SSR loader
function to the clientLoader
function.
1export const loader = async ({ context, request }: Route.LoaderArgs) => {2 const url = new URL(request.url);3 const code = url.searchParams.get("code");45 try {6 await context.api.user.verifyEmail({ code });7 return { success: true, error: null };8 } catch (error) {9 return {10 error: { message: (error as Error).message },11 success: false,12 };13 }14};
1export const loader = async ({ context, request }: Route.LoaderArgs) => {2 const url = new URL(request.url);3 const code = url.searchParams.get("code");45 try {6 await context.api.user.verifyEmail({ code });7 return { success: true, error: null };8 } catch (error) {9 return {10 error: { message: (error as Error).message },11 success: false,12 };13 }14};
The clientLoader
can be used in both SSR and SPA. For more information, take a look at the React Router
docs.
1export async function clientLoader({ serverLoader }: Route.ClientLoaderArgs) {2 // call the server loader (if using SSR)3 const serverData = await serverLoader();4 // And/or fetch data on the client5 const data = getDataFromClient();6 // Return the data to expose through useLoaderData()7 return data;8}
1export async function clientLoader({ serverLoader }: Route.ClientLoaderArgs) {2 // call the server loader (if using SSR)3 const serverData = await serverLoader();4 // And/or fetch data on the client5 const data = getDataFromClient();6 // Return the data to expose through useLoaderData()7 return data;8}
Switching between framework and declarative mode
To switch your React Router v7 mode to declarative mode, you need to add an index.html
file to the root of your project, including the following code:
html1<!DOCTYPE html>2<html lang="en">3 <head>4 <meta charset="UTF-8" />5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />6 <title>New Gadget App Welcome Page</title>7 <link rel="stylesheet" href="https://assets.gadget.dev/assets/reset.min.css" />8 <script src="https://assets.gadget.dev/assets/web-performance.min.js" defer></script>9 <!--GADGET_CONFIG-->10 </head>1112 <body>13 <div id="root"></div>14 <script type="module" src="/web/main.tsx"></script>15 </body>16</html>
You'll need to add a main.tsx
file to the web folder and add the following code:
1import React from "react";2import ReactDOM from "react-dom/client";3import App from "./components/App";45const root = document.getElementById("root");6if (!root) throw new Error("#root element not found for booting react app");78ReactDOM.createRoot(root).render(9 <React.StrictMode>10 <App />11 </React.StrictMode>12);
1import React from "react";2import ReactDOM from "react-dom/client";3import App from "./components/App";45const root = document.getElementById("root");6if (!root) throw new Error("#root element not found for booting react app");78ReactDOM.createRoot(root).render(9 <React.StrictMode>10 <App />11 </React.StrictMode>12);
You will then need to add the App.tsx
file that includes a browser router:
1import { Provider } from "@gadgetinc/react";2import { Suspense, useEffect } from "react";3import { BrowserRouter, Outlet, Route, Routes, useNavigate } from "react-router";4import { api } from "../api";5import "../app.css";6import ForgotPasswordPage from "../routes/forgot-password";7import IndexPage from "../routes/index";8import ProfilePage from "../routes/profile";9import InvitePage from "../routes/invite";10import TeamPage from "../routes/team";11import ResetPasswordPage from "../routes/reset-password";12import SignUpPage from "../routes/sign-up";13import SignedInPage from "../routes/signed-in";14import VerifyEmailPage from "../routes/verify-email";15import AnonLayout from "./layouts/AnonLayout";16import UserLayout from "./layouts/UserLayout";1718const App = () => {19 useEffect(() => {20 document.title = `${window.gadgetConfig.env.GADGET_APP}`;21 }, []);2223 return (24 <Suspense fallback={<></>}>25 <BrowserRouter>26 <Routes>27 <Route element={<Layout />}>28 {/* Example routes */}29 <Route element={<AnonLayout />}>30 <Route index element={<IndexPage />} />31 <Route path="forgot-password" element={<ForgotPasswordPage />} />32 <Route path="sign-up" element={<SignUpPage />} />33 <Route path="reset-password" element={<ResetPasswordPage />} />34 <Route path="verify-email" element={<VerifyEmailPage />} />35 </Route>36 <Route element={<UserLayout />}>37 <Route path="signed-in" element={<SignedInPage />} />38 <Route path="profile" element={<ProfilePage />} />39 <Route path="invite" element={<InvitePage />} />40 <Route path="team" element={<TeamPage />} />41 </Route>42 </Route>43 </Routes>44 </BrowserRouter>45 </Suspense>46 );47};4849const Layout = () => {50 const navigate = useNavigate();5152 return (53 <Provider api={api} navigate={navigate} auth={window.gadgetConfig.authentication}>54 <Outlet />55 </Provider>56 );57};5859export default App;
1import { Provider } from "@gadgetinc/react";2import { Suspense, useEffect } from "react";3import { BrowserRouter, Outlet, Route, Routes, useNavigate } from "react-router";4import { api } from "../api";5import "../app.css";6import ForgotPasswordPage from "../routes/forgot-password";7import IndexPage from "../routes/index";8import ProfilePage from "../routes/profile";9import InvitePage from "../routes/invite";10import TeamPage from "../routes/team";11import ResetPasswordPage from "../routes/reset-password";12import SignUpPage from "../routes/sign-up";13import SignedInPage from "../routes/signed-in";14import VerifyEmailPage from "../routes/verify-email";15import AnonLayout from "./layouts/AnonLayout";16import UserLayout from "./layouts/UserLayout";1718const App = () => {19 useEffect(() => {20 document.title = `${window.gadgetConfig.env.GADGET_APP}`;21 }, []);2223 return (24 <Suspense fallback={<></>}>25 <BrowserRouter>26 <Routes>27 <Route element={<Layout />}>28 {/* Example routes */}29 <Route element={<AnonLayout />}>30 <Route index element={<IndexPage />} />31 <Route path="forgot-password" element={<ForgotPasswordPage />} />32 <Route path="sign-up" element={<SignUpPage />} />33 <Route path="reset-password" element={<ResetPasswordPage />} />34 <Route path="verify-email" element={<VerifyEmailPage />} />35 </Route>36 <Route element={<UserLayout />}>37 <Route path="signed-in" element={<SignedInPage />} />38 <Route path="profile" element={<ProfilePage />} />39 <Route path="invite" element={<InvitePage />} />40 <Route path="team" element={<TeamPage />} />41 </Route>42 </Route>43 </Routes>44 </BrowserRouter>45 </Suspense>46 );47};4849const Layout = () => {50 const navigate = useNavigate();5152 return (53 <Provider api={api} navigate={navigate} auth={window.gadgetConfig.authentication}>54 <Outlet />55 </Provider>56 );57};5859export default App;
Note that the routes in the above file are an example and your own routes and components may be different.
Remix
Switching from SSR to SPA
You can manually migrate a Remix frontend from SSR mode to SPA mode by following these steps:
- Add the
ssr: false
flag to theremix
plugin in thevite.config.mjs
file:
1import { defineConfig } from "vite";2import { gadget } from "gadget-server/vite";3import { remixViteOptions } from "gadget-server/remix";4import { vitePlugin as remix } from "@remix-run/dev";56export default defineConfig({7 plugins: [8 gadget(),9 remix({10 ...remixViteOptions,1112 // add this13 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";56export default defineConfig({7 plugins: [8 gadget(),9 remix({10 ...remixViteOptions,1112 // add this13 ssr: false,14 }),15 ],16});
- Add the
/* --GADGET_CONFIG-- */
script tag toweb/root.jsx
. - SPA mode does not support the
loader
andaction
functions. You need to use Gadget's React hooks and autocomponents to read and write data in your frontend.
Switching from Remix to React Router v7
To switch from Remix to React Router v7, you need to add some packages and files to your application.
Add the following packages as dependencies:
yarn add react-dom react-router @react-router/fs-routes
Add the following packages as devDependencies:
yarn add -D @react-router/dev
Remove the following packages from your dependencies:
yarn remove @remix-run/node @remix-run/react @remix-run/dev
Change your root.tsx
to the following:
1import { Provider as GadgetProvider } from "@gadgetinc/react";2import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";3import { Suspense } from "react";4import { api } from "./api";5import "./app.css";6import type { GadgetConfig } from "gadget-server";7import type { Route } from "./+types/root";8import { ErrorBoundary as DefaultGadgetErrorBoundary } from "gadget-server/react-router";910export const links = () => [{ rel: "stylesheet", href: "https://assets.gadget.dev/assets/reset.min.css" }];1112export const meta = () => [13 { charset: "utf-8" },14 { name: "viewport", content: "width=device-width, initial-scale=1" },15 { title: "Gadget React Router app" },16];1718export type RootOutletContext = {19 gadgetConfig: GadgetConfig;20 csrfToken: string;21};2223export const loader = async ({ context }: Route.LoaderArgs) => {24 const { session, gadgetConfig } = context;2526 return {27 gadgetConfig,28 csrfToken: session?.get("csrfToken"),29 };30};3132export default function App({ loaderData }: Route.ComponentProps) {33 const { gadgetConfig, csrfToken } = loaderData;3435 return (36 <html lang="en" className="light">37 <head>38 <Meta />39 <Links />40 </head>41 <body>42 <Suspense>43 <GadgetProvider api={api}>44 <Outlet context={{ gadgetConfig, csrfToken }} />45 </GadgetProvider>46 </Suspense>47 <ScrollRestoration />48 <Scripts />49 </body>50 </html>51 );52}5354// Default Gadget error boundary component55// This can be replaced with your own custom error boundary implementation56// For more info, checkout https://reactrouter.com/how-to/error-boundary#1-add-a-root-error-boundary57export const ErrorBoundary = DefaultGadgetErrorBoundary;
1import { Provider as GadgetProvider } from "@gadgetinc/react";2import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";3import { Suspense } from "react";4import { api } from "./api";5import "./app.css";6import type { GadgetConfig } from "gadget-server";7import type { Route } from "./+types/root";8import { ErrorBoundary as DefaultGadgetErrorBoundary } from "gadget-server/react-router";910export const links = () => [{ rel: "stylesheet", href: "https://assets.gadget.dev/assets/reset.min.css" }];1112export const meta = () => [13 { charset: "utf-8" },14 { name: "viewport", content: "width=device-width, initial-scale=1" },15 { title: "Gadget React Router app" },16];1718export type RootOutletContext = {19 gadgetConfig: GadgetConfig;20 csrfToken: string;21};2223export const loader = async ({ context }: Route.LoaderArgs) => {24 const { session, gadgetConfig } = context;2526 return {27 gadgetConfig,28 csrfToken: session?.get("csrfToken"),29 };30};3132export default function App({ loaderData }: Route.ComponentProps) {33 const { gadgetConfig, csrfToken } = loaderData;3435 return (36 <html lang="en" className="light">37 <head>38 <Meta />39 <Links />40 </head>41 <body>42 <Suspense>43 <GadgetProvider api={api}>44 <Outlet context={{ gadgetConfig, csrfToken }} />45 </GadgetProvider>46 </Suspense>47 <ScrollRestoration />48 <Scripts />49 </body>50 </html>51 );52}5354// Default Gadget error boundary component55// This can be replaced with your own custom error boundary implementation56// For more info, checkout https://reactrouter.com/how-to/error-boundary#1-add-a-root-error-boundary57export const ErrorBoundary = DefaultGadgetErrorBoundary;
Change the vite.config
file to the following:
1import path from "path";2import { reactRouter } from "@react-router/dev/vite";3import { gadget } from "gadget-server/vite";4import { defineConfig } from "vite";56export default defineConfig({7 plugins: [gadget(), reactRouter()],8 resolve: {9 alias: {10 "@": path.resolve(__dirname, "./web"),11 },12 },13});
1import path from "path";2import { reactRouter } from "@react-router/dev/vite";3import { gadget } from "gadget-server/vite";4import { defineConfig } from "vite";56export default defineConfig({7 plugins: [gadget(), reactRouter()],8 resolve: {9 alias: {10 "@": path.resolve(__dirname, "./web"),11 },12 },13});
Add the following files. Note that the name of the file is at the top of the snippet:
import { type RouteConfig } from "@react-router/dev/routes";import { flatRoutes } from "@react-router/fs-routes";export default flatRoutes() satisfies RouteConfig;
import { type RouteConfig } from "@react-router/dev/routes";import { flatRoutes } from "@react-router/fs-routes";export default flatRoutes() satisfies RouteConfig;
import type { Config } from "@react-router/dev/config";import { reactRouterConfigOptions } from "gadget-server/react-router";export default reactRouterConfigOptions satisfies Config;
import type { Config } from "@react-router/dev/config";import { reactRouterConfigOptions } from "gadget-server/react-router";export default reactRouterConfigOptions satisfies Config;
tsconfig.jsonjson1{2 "files": [],3 "references": [4 {5 "path": "./tsconfig.web.json"6 },7 {8 "path": "./tsconfig.api.json"9 }10 ],11 "compilerOptions": {12 "strict": true,13 "allowJs": true,14 "checkJs": true,15 "noImplicitAny": true,16 "noEmit": true,17 "experimentalDecorators": true,18 "emitDecoratorMetadata": true,19 "skipLibCheck": true20 }21}
tsconfig.web.jsonjson1{2 "extends": "./tsconfig.json",3 "include": [4 ".react-router/types/**/*",5 "web/**/*",6 "web/**/.server/**/*",7 "web/**/.client/**/*"8 ],9 "compilerOptions": {10 "composite": true,11 "strict": true,12 "lib": ["DOM", "DOM.Iterable", "ES2022"],13 "types": ["vite/client"],14 "target": "ES2022",15 "module": "ES2022",16 "moduleResolution": "bundler",17 "jsx": "react-jsx",18 "rootDirs": [".", "./.react-router/types"],19 "paths": {20 "@/*": ["./web/*"]21 },22 "esModuleInterop": true,23 "resolveJsonModule": true24 }25}
tsconfig.api.jsonjson1{2 "extends": "./tsconfig.json",3 "include": ["api/**/*"],4 "compilerOptions": {5 "strict": true,6 "esModuleInterop": true,7 "allowJs": true,8 "checkJs": true,9 "noImplicitAny": true,10 "noEmit": true,11 "sourceMap": true,12 "experimentalDecorators": true,13 "emitDecoratorMetadata": true,14 "forceConsistentCasingInFileNames": true,15 "target": "es2020",16 "lib": ["es2020", "DOM"],17 "skipLibCheck": true,18 "jsx": "react-jsx",19 "resolveJsonModule": true,20 "moduleResolution": "node16",21 "module": "node16"22 }23}
Lastly, make sure to add the following to your .gitignore
file:
.gitignoreplaintext.react-router/
Read the React Router docs and our React Router in Gadget docs for more information on how to use the framework.