Building with React Router v7 

React Router is a multi-strategy router for React built on top of Vite. Gadget supports React Router in both framework and declarative modes for building app frontends.

Getting started with React Router v7 

Picking a Mode 

React Router can be run in framework or declarative mode. The mode you choose depends on which "top-level" router API you're using and whether you are comfortable with Remix-style frontend development.

Gadget's web templates offer both framework and declarative modes. Currently, the Shopify template only provides the Remix and React Router declarative mode.

Declarative 

Declarative mode is also known as "client-side routing" or "Single Page Application (SPA) routing". Declarative mode enables basic routing features like matching URLs to components, navigating around the app, and providing active states with APIs like <Link>, useNavigate, and useLocation. This is the closest comparison to the React Router v6 API.

React
1import { BrowserRouter } from "react-router";
2
3ReactDOM.createRoot(root).render(
4 <BrowserRouter>
5 <App />
6 </BrowserRouter>
7);
1import { BrowserRouter } from "react-router";
2
3ReactDOM.createRoot(root).render(
4 <BrowserRouter>
5 <App />
6 </BrowserRouter>
7);

Framework 

Framework mode is very similar to the Remix framework, which supports features like server-side rendering (SSR), file-based routing and more.

However, the React Router framework mode introduces a new "typesafe Route Module API" feature that generates route-specific types for power type inference for URL parameters, loader data, and more.

All Gadget templates in the framework mode are configured with the file-based routing using the @react-router/fs-routes package to provide a greater development experiment and smoother transition between Remix and React Router.

For more information about the differences between both modes, check out the guide from React Router.

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.

Framework mode 

Framework mode allows you to render your frontend on the server. Framework mode has multiple benefits:

  • improved performance by reducing the time it takes to load the page
  • better search engine optimization (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

Some Gadget-provided tools will not be rendered server-side, including:

You could opt out of SSR in framework mode by setting ssr: false in the react-router.config.ts file. However, in this section, we only focus on the SSR.

Auto-generated route module types 

React Router generates route-specific types to power type inference for URL params, loader data, and more. Note that this is a TypeScript-only feature, and we have set it up for you.

To import the auto-generated types, you could write the import path like this:

File pathImport statement
web/routes/_index.tsximport type { Route } from "./+types/_index";
web/routes/_user.todos.tsximport type { Route } from "./+types/_user.todos";
web/routes/_public.blogs.$id.tsximport type { Route } from "./+types/_public.blogs.$id";

Note that this conversation is only valid for the file-based routing we have set up for you.

Then you can use the Route type like this:

web/routes/_public.blogs.$id.jsx
React
1import type { Route } from "./+types/_public.blogs.$id";
2
3// The \`context\` is the same you get in Gadget actions.
4// The \`params\` has the \`{ id: string; }\` type because the route has a \`$id\` parameter.
5export const loader = async ({ context, params }: Route.LoaderArgs) => {
6 const blog = context.api.blog.findById(params.id);
7 return {
8 blog,
9 };
10};
11
12export default function ({ loaderData, params }: Route.ComponentProps) {
13 const blog = loaderData.blog;
14
15 return (
16 <div>
17 <h1>Blog {params.id}</h1>
18 <div>{blog.title}</div>
19 </div>
20 );
21}
1import type { Route } from "./+types/_public.blogs.$id";
2
3// The \`context\` is the same you get in Gadget actions.
4// The \`params\` has the \`{ id: string; }\` type because the route has a \`$id\` parameter.
5export const loader = async ({ context, params }: Route.LoaderArgs) => {
6 const blog = context.api.blog.findById(params.id);
7 return {
8 blog,
9 };
10};
11
12export default function ({ loaderData, params }: Route.ComponentProps) {
13 const blog = loaderData.blog;
14
15 return (
16 <div>
17 <h1>Blog {params.id}</h1>
18 <div>{blog.title}</div>
19 </div>
20 );
21}

To learn more about how type safety works in React Router, check out the guides from React Router.

Reading data with loader functions 

You could use the loader function from Remix to fetch the 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 and pass data to your frontend.

For example, you might have a loader function that fetches the model data like this:

web/routes/_user.todos.jsx
React
1import type { Route } from "./+types/_user.todos";
2
3export const loader = async ({ context, request }: Route.LoaderArgs) => {
4 // The `api` client will act as the current session by default, which only returns data for the logged in user
5 const todos = context.api.todo.findMany();
6 // Add the `actAsAdmin` if you want to return all data instead
7 // const todos = context.api.actAsAdmin.todo.findMany()
8
9 // return the data you want to pass to the frontend
10 return {
11 todos,
12 };
13};
1import type { Route } from "./+types/_user.todos";
2
3export const loader = async ({ context, request }: Route.LoaderArgs) => {
4 // The `api` client will act as the current session by default, which only returns data for the logged in user
5 const todos = context.api.todo.findMany();
6 // Add the `actAsAdmin` if you want to return all data instead
7 // const todos = context.api.actAsAdmin.todo.findMany()
8
9 // return the data you want to pass to the frontend
10 return {
11 todos,
12 };
13};

Then you can access the todos data in the frontend like this:

web/routes/_user.todos.jsx
React
export default function ({ loaderData }: Route.ComponentProps) {
const todos = loaderData.todos;
return loaderData.todos.map((todo) => <div key={todo.id}>{todo.title}</div>);
}
export default function ({ loaderData }: Route.ComponentProps) {
const todos = loaderData.todos;
return loaderData.todos.map((todo) => <div key={todo.id}>{todo.title}</div>);
}

Declarative mode 

Declarative mode enables basic routing features like matching URLs to components, navigating around the app, and providing active states with APIs like <Link>, useNavigate, and useLocation. It is useful if you want to use React Router as simply as possible or are coming from v6 and are happy with the <BrowserRouter>.

Check out our guide on building frontend for more details.

Was this page helpful?