# 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 ``, `useNavigate`, and `useLocation`. This is the closest comparison to the React Router v6 API. ```tsx import { BrowserRouter } from "react-router"; ReactDOM.createRoot(root).render( ); ``` #### 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](https://reactrouter.com/start/modes). 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: * React hooks provided by `@gadget-client/react` * [Autocomponents](https://docs.gadget.dev/guides/frontend/autocomponents) 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 path | Import statement | | :--- | :--- | | `web/routes/_index.tsx` | `import type { Route } from "./+types/_index";` | | `web/routes/_user.todos.tsx` | `import type { Route } from "./+types/_user.todos";` | | `web/routes/_public.blogs.$id.tsx` | `import 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: ```tsx import type { Route } from "./+types/_public.blogs.$id"; // The \`context\` is the same you get in Gadget actions. // The \`params\` has the \`{ id: string; }\` type because the route has a \`$id\` parameter. export const loader = async ({ context, params }: Route.LoaderArgs) => { const blog = context.api.blog.findById(params.id); return { blog, }; }; export default function ({ loaderData, params }: Route.ComponentProps) { const blog = loaderData.blog; return (

Blog {params.id}

{blog.title}
); } ``` To learn more about how type safety works in React Router, check out the [guides from React Router](https://reactrouter.com/explanation/type-safety). ### 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](https://docs.gadget-canary.xyz/guides/actions/code#action-context) 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: ```tsx import type { Route } from "./+types/_user.todos"; export const loader = async ({ context, request }: Route.LoaderArgs) => { // The `api` client will act as the current session by default, which only returns data for the logged in user const todos = context.api.todo.findMany(); // Add the `actAsAdmin` if you want to return all data instead // const todos = context.api.actAsAdmin.todo.findMany() // return the data you want to pass to the frontend return { todos, }; }; ``` The `context` object also includes a `logger` for writing structured logs: ```tsx export const loader = async ({ context }: Route.LoaderArgs) => { context.logger.info({ userId: context.session?.id }, "fetching todos"); // ... }; ``` Then you can access the `todos` data in the frontend like this: ```tsx export default function ({ loaderData }: Route.ComponentProps) { const todos = loaderData.todos; return loaderData.todos.map((todo) =>
{todo.title}
); } ``` ### Submitting forms with `action` functions  Route `action` functions handle form submissions and data mutations on the server. You can use the `action` export in a route file to process a `
` submission and interact with your Gadget backend. When submitting forms in SSR, you must include a `csrfToken` in the form submission to prevent cross-site request forgery attacks. The [`csrfToken` is available](https://docs.gadget-canary.xyz/guides/plugins/authentication/helpers#csrftoken) on the `session` object in your root loader, and you can pass it to route components via ``. First, expose the `csrfToken` from your root loader: ```tsx import type { Route } from "./+types/root"; import { Outlet } from "react-router"; export const loader = async ({ context }: Route.LoaderArgs) => { return { gadgetConfig: context.gadgetConfig, csrfToken: context.session?.get("csrfToken"), }; }; export default function App({ loaderData }: Route.ComponentProps) { const { gadgetConfig, csrfToken } = loaderData; return ( ); } ``` Then in your route file, use `useOutletContext` to get the `csrfToken` and include it as a hidden input in the form: ```tsx import type { Route } from "./+types/_user.todos"; import { Form, useOutletContext, redirect } from "react-router"; export const action = async ({ context, request }: Route.ActionArgs) => { const formData = await request.formData(); const title = formData.get("title")?.toString(); await context.api.todo.create({ todo: { title } }); return redirect("/todos"); }; export default function NewTodoPage() { const { csrfToken } = useOutletContext<{ csrfToken: string }>(); return (
); } ``` The `
` component from `react-router` submits to the current route's `action` function using a `POST` request by default. The action function receives the submitted values from `request.formData()`. Route `action` functions run on the server and have access to the same [`context` object](https://docs.gadget-canary.xyz/guides/actions/code#action-context) as `loader` functions, including `context.api`, `context.logger`, and `context.session`. For other ways to call actions, see the [React Router actions guide](https://reactrouter.com/start/data/actions). ## Declarative mode  Declarative mode enables basic routing features like matching URLs to components, navigating around the app, and providing active states with APIs like ``, `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 ``. Check out our guide on [building frontend](https://docs.gadget-canary.xyz/guides/templates#switching-between-framework-and-declarative-mode) for more details. ## Migrate from Remix to React Router  Follow these steps to migrate your Gadget frontend from Remix to React Router v7 (framework mode): 1. Replace Remix with React Router dependencies. First install the React Router packages: ```bash yarn add react-router @react-router/fs-routes @react-router/node @react-router/serve ``` the required dev package: ```bash yarn add -D @react-router/dev ``` Then remove the Remix packages you no longer need: ```bash yarn remove @remix-run/react @remix-run/node @remix-run/dev ``` 2. Replace imports from `@remix-run/react` with `react-router`. Also replace any other imports from `@remix-run/*` with the [corresponding `react-router` packages](https://reactrouter.com/upgrading/remix#2-update-dependencies): ```markdown // in replace imports - import { useLoaderData, Outlet } from "@remix-run/react"; + import { useLoaderData, Outlet } from "react-router"; ``` Importing `json` from `@remix-run/node` is not needed in React Router. This will be covered when you update your `loader` functions in step 7. 3. Replace `gadget-server/remix` with `gadget-server/react-router` in `web/root.jsx`: ```markdown // in use gadget-server/react-router - import { ProductionErrorBoundary, DevelopmentErrorBoundary } from "gadget-server/remix"; + import { ProductionErrorBoundary, DevelopmentErrorBoundary } from "gadget-server/react-router"; ``` 4. Add a `web/routes.js` file and add the following content: ```typescript import { flatRoutes } from "@react-router/fs-routes"; import type { RouteConfig } from "@react-router/dev/routes"; export default flatRoutes() satisfies RouteConfig; ``` 5. Add a root level `react-router.config.js` file and add the following content: ```typescript import { reactRouterConfigOptions } from "gadget-server/react-router"; import type { Config } from "@react-router/dev/config"; export default reactRouterConfigOptions satisfies Config; ``` 6. Update `vite.config.mjs` to use the React Router plugin: ```typescript import { defineConfig } from "vite"; import { gadget } from "gadget-server/vite"; import { reactRouter } from "@react-router/dev/vite"; import path from "path"; export default defineConfig({ plugins: [gadget(), reactRouter()], resolve: { alias: { "@": path.resolve(__dirname, "./web"), }, }, }); ``` 7. Update your `loader` functions. The `json()` helper used in Remix is not included with `react-router`. This means you can remove imports of `json` from `@remix-run/node` and update your `loader` functions to return plain objects instead of using `json()`. For TypeScript apps, a `Route` type can be imported from the auto-generated types file for each route, of the format `./+types/path/to/route` ```tsx import type { Route } from "./+types/root"; // use Route.LoaderArgs to type the arguments to the loader function export const loader = async ({ context }: Route.LoaderArgs) => { // return the data you want to pass to the frontend, no json() needed return { gadgetConfig: context.gadgetConfig }; }; // use Route.ComponentProps to type useLoaderData export default function MyPage() { const { gadgetConfig } = useLoaderData(); // ... } ``` Optionally, you can replace the `useLoaderData` hook and use a `loaderData` prop in your component: ```tsx // access the loaderData prop in your component export default function MyPage({ loaderData }: Route.ComponentProps) { const { gadgetConfig } = loaderData; // ... } ```