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
export const loader = async ({ context, request }: Route.LoaderArgs) => { const url = new URL(request.url); const code = url.searchParams.get("code"); try { await context.api.user.verifyEmail({ code }); return { success: true, error: null }; } catch (error) { return { error: { message: (error as Error).message }, success: false, }; } };
export const loader = async ({ context, request }: Route.LoaderArgs) => { const url = new URL(request.url); const code = url.searchParams.get("code"); try { await context.api.user.verifyEmail({ code }); return { success: true, error: null }; } catch (error) { return { error: { message: (error as Error).message }, success: false, }; } };

The clientLoader can be used in both SSR and SPA. For more information, take a look at the React Router docs.

SPA mode
React
export async function clientLoader({ serverLoader }: Route.ClientLoaderArgs) { // call the server loader (if using SSR) const serverData = await serverLoader(); // And/or fetch data on the client const data = getDataFromClient(); // Return the data to expose through useLoaderData() return data; }
export async function clientLoader({ serverLoader }: Route.ClientLoaderArgs) { // call the server loader (if using SSR) const serverData = await serverLoader(); // And/or fetch data on the client const data = getDataFromClient(); // Return the data to expose through useLoaderData() return data; }

Make sure that you also switch any usage of the gadgetConfig, loaded in the loader when using SSR, to window.gadgetConfig when using SPA mode. You can do that by first adding the following script tag to the head tag in your root.tsx file:

web/root.jsx
React
export const Layout = ({ children }: { children: React.ReactNode }) => { return ( <html lang="en"> <head> <Meta /> {/* This script tag injects the gadgetConfig object in the window */} <script suppressHydrationWarning>/* --GADGET_CONFIG-- */</script> <Links /> </head> <body> <Suspense fallback={<div>Loading...</div>}>{children}</Suspense> <ScrollRestoration /> <Scripts /> </body> </html> ); };
export const Layout = ({ children }: { children: React.ReactNode }) => { return ( <html lang="en"> <head> <Meta /> {/* This script tag injects the gadgetConfig object in the window */} <script suppressHydrationWarning>/* --GADGET_CONFIG-- */</script> <Links /> </head> <body> <Suspense fallback={<div>Loading...</div>}>{children}</Suspense> <ScrollRestoration /> <Scripts /> </body> </html> ); };

Then switch any usage of the gadgetConfig to window.gadgetConfig in your components.

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
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>New Gadget App Welcome Page</title> <link rel="stylesheet" href="https://assets.gadget.dev/assets/reset.min.css" /> <script src="https://assets.gadget.dev/assets/web-performance.min.js" defer></script> <!--GADGET_CONFIG--> </head> <body> <div id="root"></div> <script type="module" src="/web/main.tsx"></script> </body> </html>

You'll need to add a main.tsx file to the web folder and add the following code:

web/main.jsx
React
import React from "react"; import ReactDOM from "react-dom/client"; import App from "./components/App"; const root = document.getElementById("root"); if (!root) throw new Error("#root element not found for booting react app"); ReactDOM.createRoot(root).render( <React.StrictMode> <App /> </React.StrictMode> );
import React from "react"; import ReactDOM from "react-dom/client"; import App from "./components/App"; const root = document.getElementById("root"); if (!root) throw new Error("#root element not found for booting react app"); ReactDOM.createRoot(root).render( <React.StrictMode> <App /> </React.StrictMode> );

You will then need to add the App.tsx file that includes a browser router:

React
import { Provider } from "@gadgetinc/react"; import { Suspense, useEffect } from "react"; import { BrowserRouter, Outlet, Route, Routes, useNavigate } from "react-router"; import { api } from "../api"; import "../app.css"; import ForgotPasswordPage from "../routes/forgot-password"; import IndexPage from "../routes/index"; import ProfilePage from "../routes/profile"; import InvitePage from "../routes/invite"; import TeamPage from "../routes/team"; import ResetPasswordPage from "../routes/reset-password"; import SignUpPage from "../routes/sign-up"; import SignedInPage from "../routes/signed-in"; import VerifyEmailPage from "../routes/verify-email"; import AnonLayout from "./layouts/AnonLayout"; import UserLayout from "./layouts/UserLayout"; const App = () => { useEffect(() => { document.title = `${window.gadgetConfig.env.GADGET_APP}`; }, []); return ( <Suspense fallback={<></>}> <BrowserRouter> <Routes> <Route element={<Layout />}> {/* Example routes */} <Route element={<AnonLayout />}> <Route index element={<IndexPage />} /> <Route path="forgot-password" element={<ForgotPasswordPage />} /> <Route path="sign-up" element={<SignUpPage />} /> <Route path="reset-password" element={<ResetPasswordPage />} /> <Route path="verify-email" element={<VerifyEmailPage />} /> </Route> <Route element={<UserLayout />}> <Route path="signed-in" element={<SignedInPage />} /> <Route path="profile" element={<ProfilePage />} /> <Route path="invite" element={<InvitePage />} /> <Route path="team" element={<TeamPage />} /> </Route> </Route> </Routes> </BrowserRouter> </Suspense> ); }; const Layout = () => { const navigate = useNavigate(); return ( <Provider api={api} navigate={navigate} auth={window.gadgetConfig.authentication}> <Outlet /> </Provider> ); }; export default App;
import { Provider } from "@gadgetinc/react"; import { Suspense, useEffect } from "react"; import { BrowserRouter, Outlet, Route, Routes, useNavigate } from "react-router"; import { api } from "../api"; import "../app.css"; import ForgotPasswordPage from "../routes/forgot-password"; import IndexPage from "../routes/index"; import ProfilePage from "../routes/profile"; import InvitePage from "../routes/invite"; import TeamPage from "../routes/team"; import ResetPasswordPage from "../routes/reset-password"; import SignUpPage from "../routes/sign-up"; import SignedInPage from "../routes/signed-in"; import VerifyEmailPage from "../routes/verify-email"; import AnonLayout from "./layouts/AnonLayout"; import UserLayout from "./layouts/UserLayout"; const App = () => { useEffect(() => { document.title = `${window.gadgetConfig.env.GADGET_APP}`; }, []); return ( <Suspense fallback={<></>}> <BrowserRouter> <Routes> <Route element={<Layout />}> {/* Example routes */} <Route element={<AnonLayout />}> <Route index element={<IndexPage />} /> <Route path="forgot-password" element={<ForgotPasswordPage />} /> <Route path="sign-up" element={<SignUpPage />} /> <Route path="reset-password" element={<ResetPasswordPage />} /> <Route path="verify-email" element={<VerifyEmailPage />} /> </Route> <Route element={<UserLayout />}> <Route path="signed-in" element={<SignedInPage />} /> <Route path="profile" element={<ProfilePage />} /> <Route path="invite" element={<InvitePage />} /> <Route path="team" element={<TeamPage />} /> </Route> </Route> </Routes> </BrowserRouter> </Suspense> ); }; const Layout = () => { const navigate = useNavigate(); return ( <Provider api={api} navigate={navigate} auth={window.gadgetConfig.authentication}> <Outlet /> </Provider> ); }; export 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
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, // add this ssr: false, }), ], });
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, // add this ssr: false, }), ], });
  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
import { Provider as GadgetProvider } from "@gadgetinc/react"; import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router"; import { Suspense } from "react"; import { api } from "./api"; import "./app.css"; import type { GadgetConfig } from "gadget-server"; import type { Route } from "./+types/root"; import { ErrorBoundary as DefaultGadgetErrorBoundary } from "gadget-server/react-router"; export const links = () => [{ rel: "stylesheet", href: "https://assets.gadget.dev/assets/reset.min.css" }]; export const meta = () => [ { charset: "utf-8" }, { name: "viewport", content: "width=device-width, initial-scale=1" }, { title: "Gadget React Router app" }, ]; export type RootOutletContext = { gadgetConfig: GadgetConfig; csrfToken: string; }; export const loader = async ({ context }: Route.LoaderArgs) => { const { session, gadgetConfig } = context; return { gadgetConfig, csrfToken: session?.get("csrfToken"), }; }; export default function App({ loaderData }: Route.ComponentProps) { const { gadgetConfig, csrfToken } = loaderData; return ( <html lang="en" className="light"> <head> <Meta /> <Links /> </head> <body> <Suspense> <GadgetProvider api={api}> <Outlet context={{ gadgetConfig, csrfToken }} /> </GadgetProvider> </Suspense> <ScrollRestoration /> <Scripts /> </body> </html> ); } // Default Gadget error boundary component // This can be replaced with your own custom error boundary implementation // For more info, checkout https://reactrouter.com/how-to/error-boundary#1-add-a-root-error-boundary export const ErrorBoundary = DefaultGadgetErrorBoundary;
import { Provider as GadgetProvider } from "@gadgetinc/react"; import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router"; import { Suspense } from "react"; import { api } from "./api"; import "./app.css"; import type { GadgetConfig } from "gadget-server"; import type { Route } from "./+types/root"; import { ErrorBoundary as DefaultGadgetErrorBoundary } from "gadget-server/react-router"; export const links = () => [{ rel: "stylesheet", href: "https://assets.gadget.dev/assets/reset.min.css" }]; export const meta = () => [ { charset: "utf-8" }, { name: "viewport", content: "width=device-width, initial-scale=1" }, { title: "Gadget React Router app" }, ]; export type RootOutletContext = { gadgetConfig: GadgetConfig; csrfToken: string; }; export const loader = async ({ context }: Route.LoaderArgs) => { const { session, gadgetConfig } = context; return { gadgetConfig, csrfToken: session?.get("csrfToken"), }; }; export default function App({ loaderData }: Route.ComponentProps) { const { gadgetConfig, csrfToken } = loaderData; return ( <html lang="en" className="light"> <head> <Meta /> <Links /> </head> <body> <Suspense> <GadgetProvider api={api}> <Outlet context={{ gadgetConfig, csrfToken }} /> </GadgetProvider> </Suspense> <ScrollRestoration /> <Scripts /> </body> </html> ); } // Default Gadget error boundary component // This can be replaced with your own custom error boundary implementation // For more info, checkout https://reactrouter.com/how-to/error-boundary#1-add-a-root-error-boundary export const ErrorBoundary = DefaultGadgetErrorBoundary;

Change the vite.config file to the following:

vite.config.mjs
JavaScript
import path from "path"; import { reactRouter } from "@react-router/dev/vite"; import { gadget } from "gadget-server/vite"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [gadget(), reactRouter()], resolve: { alias: { "@": path.resolve(__dirname, "./web"), }, }, });
import path from "path"; import { reactRouter } from "@react-router/dev/vite"; import { gadget } from "gadget-server/vite"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [gadget(), reactRouter()], resolve: { alias: { "@": path.resolve(__dirname, "./web"), }, }, });

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
{ "files": [], "references": [ { "path": "./tsconfig.web.json" }, { "path": "./tsconfig.api.json" } ], "compilerOptions": { "strict": true, "allowJs": true, "checkJs": true, "noImplicitAny": true, "noEmit": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "skipLibCheck": true } }
tsconfig.web.json
json
{ "extends": "./tsconfig.json", "include": [ ".react-router/types/**/*", "web/**/*", "web/**/.server/**/*", "web/**/.client/**/*" ], "compilerOptions": { "composite": true, "strict": true, "lib": ["DOM", "DOM.Iterable", "ES2022"], "types": ["vite/client"], "target": "ES2022", "module": "ES2022", "moduleResolution": "bundler", "jsx": "react-jsx", "rootDirs": [".", "./.react-router/types"], "paths": { "@/*": ["./web/*"] }, "esModuleInterop": true, "resolveJsonModule": true } }
tsconfig.api.json
json
{ "extends": "./tsconfig.json", "include": ["api/**/*"], "compilerOptions": { "strict": true, "esModuleInterop": true, "allowJs": true, "checkJs": true, "noImplicitAny": true, "noEmit": true, "sourceMap": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "forceConsistentCasingInFileNames": true, "target": "es2020", "lib": ["es2020", "DOM"], "skipLibCheck": true, "jsx": "react-jsx", "resolveJsonModule": true, "moduleResolution": "node16", "module": "node16" } }

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?