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:

TemplateTenancyFrameworkSSR/SPA
ShopifyPublicReactSPA
ShopifyCustomReactSPA
ShopifyPublicRemixSSR
ShopifyCustomRemixSSR
BigCommercePublicReactSPA
Web appMulti-party authReact Router v7SSR
Web appSingle-party authReact Router v7SSR
Web appNo authReact Router v7SSR

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.

react-router.config.js
JavaScript
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.

SSR mode
React
1export const loader = async ({ context, request }: Route.LoaderArgs) => {
2 const url = new URL(request.url);
3 const code = url.searchParams.get("code");
4
5 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");
4
5 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.

SPA mode
React
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 client
5 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 client
5 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:

html
1<!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>
11
12 <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:

web/main.jsx
React
1import React from "react";
2import ReactDOM from "react-dom/client";
3import App from "./components/App";
4
5const root = document.getElementById("root");
6if (!root) throw new Error("#root element not found for booting react app");
7
8ReactDOM.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";
4
5const root = document.getElementById("root");
6if (!root) throw new Error("#root element not found for booting react app");
7
8ReactDOM.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:

React
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";
17
18const App = () => {
19 useEffect(() => {
20 document.title = `${window.gadgetConfig.env.GADGET_APP}`;
21 }, []);
22
23 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};
48
49const Layout = () => {
50 const navigate = useNavigate();
51
52 return (
53 <Provider api={api} navigate={navigate} auth={window.gadgetConfig.authentication}>
54 <Outlet />
55 </Provider>
56 );
57};
58
59export 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";
17
18const App = () => {
19 useEffect(() => {
20 document.title = `${window.gadgetConfig.env.GADGET_APP}`;
21 }, []);
22
23 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};
48
49const Layout = () => {
50 const navigate = useNavigate();
51
52 return (
53 <Provider api={api} navigate={navigate} auth={window.gadgetConfig.authentication}>
54 <Outlet />
55 </Provider>
56 );
57};
58
59export 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:

  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.

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:

Terminal
yarn
yarn add react-dom react-router @react-router/fs-routes

Add the following packages as devDependencies:

Terminal
yarn
yarn add -D @react-router/dev

Remove the following packages from your dependencies:

Terminal
yarn
yarn remove @remix-run/node @remix-run/react @remix-run/dev

Change your root.tsx to the following:

web/root.jsx
React
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";
9
10export const links = () => [{ rel: "stylesheet", href: "https://assets.gadget.dev/assets/reset.min.css" }];
11
12export const meta = () => [
13 { charset: "utf-8" },
14 { name: "viewport", content: "width=device-width, initial-scale=1" },
15 { title: "Gadget React Router app" },
16];
17
18export type RootOutletContext = {
19 gadgetConfig: GadgetConfig;
20 csrfToken: string;
21};
22
23export const loader = async ({ context }: Route.LoaderArgs) => {
24 const { session, gadgetConfig } = context;
25
26 return {
27 gadgetConfig,
28 csrfToken: session?.get("csrfToken"),
29 };
30};
31
32export default function App({ loaderData }: Route.ComponentProps) {
33 const { gadgetConfig, csrfToken } = loaderData;
34
35 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}
53
54// Default Gadget error boundary component
55// This can be replaced with your own custom error boundary implementation
56// For more info, checkout https://reactrouter.com/how-to/error-boundary#1-add-a-root-error-boundary
57export 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";
9
10export const links = () => [{ rel: "stylesheet", href: "https://assets.gadget.dev/assets/reset.min.css" }];
11
12export const meta = () => [
13 { charset: "utf-8" },
14 { name: "viewport", content: "width=device-width, initial-scale=1" },
15 { title: "Gadget React Router app" },
16];
17
18export type RootOutletContext = {
19 gadgetConfig: GadgetConfig;
20 csrfToken: string;
21};
22
23export const loader = async ({ context }: Route.LoaderArgs) => {
24 const { session, gadgetConfig } = context;
25
26 return {
27 gadgetConfig,
28 csrfToken: session?.get("csrfToken"),
29 };
30};
31
32export default function App({ loaderData }: Route.ComponentProps) {
33 const { gadgetConfig, csrfToken } = loaderData;
34
35 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}
53
54// Default Gadget error boundary component
55// This can be replaced with your own custom error boundary implementation
56// For more info, checkout https://reactrouter.com/how-to/error-boundary#1-add-a-root-error-boundary
57export const ErrorBoundary = DefaultGadgetErrorBoundary;

Change the vite.config file to the following:

vite.config.mjs
JavaScript
1import path from "path";
2import { reactRouter } from "@react-router/dev/vite";
3import { gadget } from "gadget-server/vite";
4import { defineConfig } from "vite";
5
6export 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";
5
6export 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:

web/routes.js
JavaScript
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;
react-router.config.js
JavaScript
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.json
json
1{
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": true
20 }
21}
tsconfig.web.json
json
1{
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": true
24 }
25}
tsconfig.api.json
json
1{
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:

.gitignore
plaintext
.react-router/

Read the React Router docs and our React Router in Gadget docs for more information on how to use the framework.

Was this page helpful?