From c8fd1384fa23d74d7dd72aad4915f20b00d7f35f Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Tue, 19 Nov 2024 11:40:47 -0700 Subject: [PATCH 01/41] docs: adjusted READMEs for the api docs generator --- README.md | 47 ++++++++-------------- packages/create-react-router/README.md | 2 - packages/react-router-architect/README.md | 4 +- packages/react-router-cloudflare/README.md | 4 +- packages/react-router-dev/README.md | 4 +- packages/react-router-dom/README.md | 7 +++- packages/react-router-express/README.md | 4 +- packages/react-router-fs-routes/README.md | 4 +- packages/react-router-node/README.md | 4 +- packages/react-router-serve/README.md | 4 +- packages/react-router/README.md | 8 +++- packages/react-router/tsup.config.ts | 6 ++- 12 files changed, 39 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index ebc5397c2b..1e910f7517 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,28 @@ -# Welcome to React Router · [![npm package][npm-badge]][npm] [![build][build-badge]][build] +[![npm package][npm-badge]][npm] [![build][build-badge]][build] [npm-badge]: https://img.shields.io/npm/v/react-router-dom.svg?style=flat-square [npm]: https://www.npmjs.org/package/react-router-dom [build-badge]: https://img.shields.io/github/actions/workflow/status/remix-run/react-router/test.yml?branch=dev&style=square [build]: https://github.com/remix-run/react-router/actions/workflows/test.yml -React Router is a lightweight, fully-featured routing library for the [React](https://reactjs.org) JavaScript library. React Router runs anywhere React runs; on the web, on the server with node.js, or on any other Javascript platform that supports the [Web Fetch API][fetch-api]. +React Router is a multi-strategy router for React bridging the gap from React 18 to React 19. You can use it maximally as a React framework or minimally as a library with your own architecture. -If you're new to React Router, we recommend you start with [the tutorial](https://reactrouter.com/en/main/start/tutorial). - -If you're migrating to v6 from v5 (or v4, which is the same as v5), check out [the migration guide](/docs/upgrading/v5.md). If you're migrating from Reach Router, check out [the migration guide for Reach Router](/docs/upgrading/reach.md). If you need to find the code for v5, [it is on the `v5` branch](https://github.com/remix-run/react-router/tree/v5). - -Documentation for v6 can be found [on our website](https://reactrouter.com/). - -## Contributing - -There are many different ways to contribute to React Router's development. If you're interested, check out [our contributing guidelines](CONTRIBUTING.md) to learn how you can get involved. +- [Getting Started - Framework](https://reactrouter.com/start/framework/installation) +- [Getting Started - Library](https://react.router.com/start/library/installation) +- [Upgrade from v6](https://reactrouter.com/upgrading/v6) +- [Upgrade from Remix](https://reactrouter.com/upgrading/remix) +- [Changelog](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md) ## Packages -This repository is a monorepo containing the following packages: - -- [`@react-router/dev`](/packages/react-router-dev) -- [`@react-router/express`](/packages/react-router-express) -- [`@react-router/node`](/packages/react-router-node) -- [`@react-router/serve`](/packages/react-router-serve) -- [`react-router`](/packages/react-router) -- [`react-router-dom`](/packages/react-router-dom) - -## Changes - -Detailed release notes for a given version can be found [on our releases page](https://github.com/remix-run/react-router/releases). - -## Funding - -You may provide financial support for this project by donating [via Open Collective](https://opencollective.com/react-router). Thank you for your support! - -## About +- [react-router](./modules/react_router) +- [@react-router/dev](./modules/_react_router_dev) +- [@react-router/node](./modules/_react_router_node) +- [@react-router/cloudflare](./modules/_react_router_cloudflare) +- [@react-router/serve](./modules/_react_router_serve) +- [@react-router/fs-routes](./modules/_react_router_fs_routes) -React Router is developed and maintained by [Remix Software](https://remix.run) and many [amazing contributors](https://github.com/remix-run/react-router/graphs/contributors). +## Previous Versions -[fetch-api]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API +- [v6](https://reactrouter.com/v6) +- [v5](https://v5.reactrouter.com/) diff --git a/packages/create-react-router/README.md b/packages/create-react-router/README.md index 8fcf3653e9..50f449f3b9 100644 --- a/packages/create-react-router/README.md +++ b/packages/create-react-router/README.md @@ -1,5 +1,3 @@ -# create-react-router - Create a new React Router app. ```sh diff --git a/packages/react-router-architect/README.md b/packages/react-router-architect/README.md index 897dbdc8f1..f42337f1fb 100644 --- a/packages/react-router-architect/README.md +++ b/packages/react-router-architect/README.md @@ -1,6 +1,4 @@ -# @react-router/architect - -Architect server request handler for React Router. +[Architect](https://arc.codes) server request handler for React Router. ```bash npm install @react-router/architect diff --git a/packages/react-router-cloudflare/README.md b/packages/react-router-cloudflare/README.md index 9c8b6b051b..6ecbecb41d 100644 --- a/packages/react-router-cloudflare/README.md +++ b/packages/react-router-cloudflare/README.md @@ -1,6 +1,4 @@ -# @react-router/cloudflare - -Cloudflare platform abstractions for [React Router.](https://reactrouter.com) +Cloudflare platform abstractions for React Router ```bash npm install @react-router/cloudflare @cloudflare/workers-types diff --git a/packages/react-router-dev/README.md b/packages/react-router-dev/README.md index 9cb7bfc251..5629001a4e 100644 --- a/packages/react-router-dev/README.md +++ b/packages/react-router-dev/README.md @@ -1,6 +1,4 @@ -# @react-router/dev - -Dev tools and CLI for [React Router.](https://github.com/remix-run/react-router) +Dev tools and CLI for React Router that enables framework features through bundler integration like server rendering, code splitting, HMR, etc. ```sh npm install @react-router/dev diff --git a/packages/react-router-dom/README.md b/packages/react-router-dom/README.md index bd8108f900..d7bfd79188 100644 --- a/packages/react-router-dom/README.md +++ b/packages/react-router-dom/README.md @@ -1,3 +1,6 @@ -# React Router DOM +This package simply re-exports everything from `react-router` to smooth the upgrade path for v6 applications. Once upgraded you can change all of your imports and remove it from your dependencies: -The `react-router-dom` package is deprecated and only kept around for backwards-compatibility. It re-exports everything from the `react-router` package - you should convert your applications to import everything from `react-router` in v7 and beyond. +```diff +-import { Routes } from "react-router-dom" ++import { Routes } from "react-router" +``` diff --git a/packages/react-router-express/README.md b/packages/react-router-express/README.md index b949e57465..63c3de6441 100644 --- a/packages/react-router-express/README.md +++ b/packages/react-router-express/README.md @@ -1,6 +1,4 @@ -# @react-router/express - -[Express](https://expressjs.com) server request handler for [React Router.](https://github.com/remix-run/react-router) +[Express](https://expressjs.com) server request handler for React Router. ```sh npm install @react-router/express diff --git a/packages/react-router-fs-routes/README.md b/packages/react-router-fs-routes/README.md index d8f961b45b..a3fdebfd6e 100644 --- a/packages/react-router-fs-routes/README.md +++ b/packages/react-router-fs-routes/README.md @@ -1,6 +1,4 @@ -# @react-router/fs-routes - -File system routing conventions for [React Router](https://github.com/remix-run/react-router), for use within `routes.ts`. +File system routing conventions for React Router ```sh npm install @react-router/fs-routes diff --git a/packages/react-router-node/README.md b/packages/react-router-node/README.md index 6478831278..f789859826 100644 --- a/packages/react-router-node/README.md +++ b/packages/react-router-node/README.md @@ -1,6 +1,4 @@ -# @react-router/node - -Node.js platform abstractions for [React Router.](https://github.com/remix-run/react-router) +Node.js platform abstractions for React Router ```sh npm install @react-router/node diff --git a/packages/react-router-serve/README.md b/packages/react-router-serve/README.md index 7215d98987..fbee27bc9d 100644 --- a/packages/react-router-serve/README.md +++ b/packages/react-router-serve/README.md @@ -1,6 +1,4 @@ -# @react-router/serve - -Production application server for [React Router.](https://github.com/remix-run/react-router) +Node.js application server for React Router ```sh npm install @react-router/serve diff --git a/packages/react-router/README.md b/packages/react-router/README.md index 33140afe94..b0d8de42df 100644 --- a/packages/react-router/README.md +++ b/packages/react-router/README.md @@ -1,3 +1,7 @@ -# React Router +`react-router` is the primary package in the React Router project. -The `react-router` package is the heart of [React Router](https://github.com/remix-run/react-router) and provides all the core functionality. +## Installation + +```sh +npm i react-router +``` diff --git a/packages/react-router/tsup.config.ts b/packages/react-router/tsup.config.ts index d673ae2657..5d93e3e81b 100644 --- a/packages/react-router/tsup.config.ts +++ b/packages/react-router/tsup.config.ts @@ -40,4 +40,8 @@ const config = (enableDevWarnings: boolean) => }, ]); -export default defineConfig([...config(false), ...config(true)]); +export default defineConfig([ + // @ts-expect-error + ...config(false), + ...config(true), +]); From f664ea894dfaad794c5e98c791b2f8b9c1426424 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Tue, 19 Nov 2024 14:41:23 -0500 Subject: [PATCH 02/41] docs: update remix AppLoadContext migration (#12313) * docs: update remix AppLoadContext migration * tweak AppLoadContext section --------- Co-authored-by: Brooks Lybrand --- docs/upgrading/remix.md | 55 ++++++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/docs/upgrading/remix.md b/docs/upgrading/remix.md index 3e43df5a79..438e05f7d3 100644 --- a/docs/upgrading/remix.md +++ b/docs/upgrading/remix.md @@ -313,29 +313,60 @@ If you were using `remix-serve` you can skip this step. This is only applicable -If you were using `getLoadContext` in your Remix app, then you'll notice that the `LoaderFunctionArgs`/`ActionFunctionArgs` types now type the `context` parameter incorrectly (optional and typed as `any`). These types accept a generic for the `context` type but even that still leaves the property as optional because it does not exist in React Router SPA apps. +Since React Router can be used as both a React framework _and_ a stand-alone routing library, the `context` argument for `LoaderFunctionArgs` and `ActionFunctionArgs` is now optional and typed as `any` by default. You can register types for your load context to get type safety for your loaders and actions. -The proper long term fix is to move to the new [`Route.LoaderArgs`][server-loaders]/[`Route.ActionArgs`][server-actions] types from the [new typegen in React Router v7][type-safety]. +👉 **Register types for your load context** -However, the short-term solution to ease the upgrade is to use TypeScript's [module augmentation][ts-module-augmentation] feature to override the built in `LoaderFunctionArgs`/`ActionFunctionArgs` interfaces. +Before you migrate to the new `Route.LoaderArgs` and `Route.ActionArgs` types, you can temporarily augment `LoaderFunctionArgs` and `ActionFunctionArgs` with your load context type to ease migration. -You can do this with the following code in `react-router.config.ts` (just make sure it gets picked up by your `tsconfig.json`'s `include` paths): +```ts filename=app/env.d.ts +declare module "react-router" { + // Your AppLoadContext used in v2 + interface AppLoadContext { + whatever: string; + } + + // TODO: remove this once we've migrated to `Route.LoaderArgs` instead for our loaders + interface LoaderFunctionArgs { + context: AppLoadContext; + } -```ts filename=react-router.config.ts -// Your AppLoadContext used in v2 -interface AppLoadContext { - whatever: string; + // TODO: remove this once we've migrated to `Route.ActionArgs` instead for our actions + interface ActionFunctionArgs { + context: AppLoadContext; + } } +``` + + -// Tell v7 the type of the context and that it is non-optional +Using `declare module` to register types is a standard TypeScript technique called [module augmentation][ts-module-augmentation]. +You can do this in any TypeScript file covered by your `tsconfig.json`'s `include` field, but we recommend a dedicated `env.d.ts` within your app directory. + + + +👉 **Use the new types** + +Once you adopt the [new type generation][type-safety], you can remove the `LoaderFunctionArgs`/`ActionFunctionArgs` augmentations and use the `context` argument from [`Route.LoaderArgs`][server-loaders] and [`Route.ActionArgs`][server-actions] instead. + +```ts filename=app/env.d.ts declare module "react-router" { - interface LoaderFunctionArgs { - context: AppLoadContext; + // Your AppLoadContext used in v2 + interface AppLoadContext { + whatever: string; } } ``` -This should allow you to upgrade and ship your application on React Router v7, and then you can incrementally migrate routes to the [new typegen approach][type-safety]. +```ts filename=app/routes/my-route.tsx +import type { Route } from "./+types/my-route"; + +export function loader({ context }: Route.LoaderArgs) {} +// { whatever: string } ^^^^^^^ + +export function action({ context }: Route.ActionArgs) {} +// { whatever: string } ^^^^^^^ +``` Congratulations! You are now on React Router v7. Go ahead and run your application to make sure everything is working as expected. From 6819a737619eae9f62bfb6f8257217490cf3c274 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Wed, 20 Nov 2024 09:38:19 +1100 Subject: [PATCH 03/41] Update basic template to 7.0.0-pre.6 (#12316) --- templates/basic/app/routes.ts | 2 +- templates/basic/package.json | 8 ++++---- templates/basic/react-router.config.ts | 5 +++++ 3 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 templates/basic/react-router.config.ts diff --git a/templates/basic/app/routes.ts b/templates/basic/app/routes.ts index a28611ba20..102b402587 100644 --- a/templates/basic/app/routes.ts +++ b/templates/basic/app/routes.ts @@ -1,3 +1,3 @@ import { type RouteConfig, index } from "@react-router/dev/routes"; -export const routes: RouteConfig = [index("routes/home.tsx")]; +export default [index("routes/home.tsx")] satisfies RouteConfig; diff --git a/templates/basic/package.json b/templates/basic/package.json index a22a499ca1..a4bd806fa2 100644 --- a/templates/basic/package.json +++ b/templates/basic/package.json @@ -9,15 +9,15 @@ "typecheck": "react-router typegen && tsc" }, "dependencies": { - "@react-router/node": "7.0.0-pre.5", - "@react-router/serve": "7.0.0-pre.5", + "@react-router/node": "7.0.0-pre.6", + "@react-router/serve": "7.0.0-pre.6", "isbot": "^5.1.17", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router": "7.0.0-pre.5" + "react-router": "7.0.0-pre.6" }, "devDependencies": { - "@react-router/dev": "7.0.0-pre.5", + "@react-router/dev": "7.0.0-pre.6", "@types/react": "^18.3.9", "@types/react-dom": "^18.3.0", "autoprefixer": "^10.4.20", diff --git a/templates/basic/react-router.config.ts b/templates/basic/react-router.config.ts new file mode 100644 index 0000000000..9c6ea11734 --- /dev/null +++ b/templates/basic/react-router.config.ts @@ -0,0 +1,5 @@ +import type { Config } from "@react-router/dev/config"; + +export default { + // Config options... +} satisfies Config; From 50d8bf1e1bca3d38f6f22e63edb0cd00256d0d4c Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Tue, 19 Nov 2024 15:48:49 -0700 Subject: [PATCH 04/41] docs: added SPA how-to --- docs/how-to/spa.md | 74 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 docs/how-to/spa.md diff --git a/docs/how-to/spa.md b/docs/how-to/spa.md new file mode 100644 index 0000000000..3aedcfc44c --- /dev/null +++ b/docs/how-to/spa.md @@ -0,0 +1,74 @@ +--- +title: Single Page App (SPA) +--- + +# Single Page App (SPA) + +There are two ways to ship a single page app with React Router + +- **as a library** - Instead of using React Router's framework features, you can use it as a library in your own SPA architecture. Refer to [React Router as a Library](../start/library/installation) guides. +- **as a framework** - This guide will focus here + +## 1. Disable Server Rendering + +Server rendering and enabled by default. Set the ssr flag to false in `react-router.config.ts` to disable it. + +```ts filename=react-router.config.ts lines=[4] +import { type Config } from "@react-router/dev/config"; + +export default { + ssr: false, +} satisfies Config; +``` + +With this set to false, the server build will no longer be generated. + +## 2. Use client loaders and client actions + +With server rendering disabled, you can still use `clientLoader` and `clientAction` to manage route data and mutations. + +```tsx filename=some-route.tsx +import { Route } from "./+types/some-route"; + +export async function clientLoader( + _: Route.ClientLoaderArgs +) { + let data = await fetch("/some/api/stuff"); + return data; +} + +export async function clientAction({ + request, +}: Route.ClientActionArgs) { + let formData = await request.formData(); + return await processPayment(formData); +} +``` + +## 3. Pre-rendering + +Pre-rendering can be configured for paths with static data known at build time for faster initial page loads. Refer to [Pre-rendering](./pre-rendering) to set it up. + +## 4. Serve all URLs to index.html + +After running `react-router build`, deploy the `build/client` directory to whatever static host you prefer. + +Common to deploying any SPA, you'll need to configure your host to direct all URLs to the `index.html` of the client build. Some hosts do this by default, but others don't. As an example, a host may support a `_redirects` file to do this: + +``` +/* /index.html 200 +``` + +If you're getting 404s at valid routes for your app, it's likely you need to configure your host. + +## Important Note + +Typical Single Pages apps send a mostly blank index.html template with little more than an empty `
`. + +In contrast `react-router build` (with server rendering disabled) pre-renders your root and index routes. This means you can: + +- Send more than an empty div +- Use React components to generate the initial page users see +- Re-enable server rendering later without changing anything about your UI + +This is also why your project still needs a dependency on `@react-router/node`. From 2f45fd13af5a6f3a0cd55a42b9a746405ecfd19b Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Tue, 19 Nov 2024 15:50:08 -0700 Subject: [PATCH 05/41] typo --- docs/how-to/spa.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how-to/spa.md b/docs/how-to/spa.md index 3aedcfc44c..cf8865c188 100644 --- a/docs/how-to/spa.md +++ b/docs/how-to/spa.md @@ -11,7 +11,7 @@ There are two ways to ship a single page app with React Router ## 1. Disable Server Rendering -Server rendering and enabled by default. Set the ssr flag to false in `react-router.config.ts` to disable it. +Server rendering is enabled by default. Set the ssr flag to false in `react-router.config.ts` to disable it. ```ts filename=react-router.config.ts lines=[4] import { type Config } from "@react-router/dev/config"; From 1259f9cae867149b330d2ab23d6123f17390c186 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Tue, 19 Nov 2024 15:51:43 -0700 Subject: [PATCH 06/41] wordsmith --- docs/how-to/spa.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how-to/spa.md b/docs/how-to/spa.md index cf8865c188..1636d3ff19 100644 --- a/docs/how-to/spa.md +++ b/docs/how-to/spa.md @@ -49,7 +49,7 @@ export async function clientAction({ Pre-rendering can be configured for paths with static data known at build time for faster initial page loads. Refer to [Pre-rendering](./pre-rendering) to set it up. -## 4. Serve all URLs to index.html +## 4. Direct all URLs to index.html After running `react-router build`, deploy the `build/client` directory to whatever static host you prefer. From 618da2cf9f7e2fa383958f09fc586dff59d3a0f0 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Tue, 19 Nov 2024 15:54:06 -0700 Subject: [PATCH 07/41] docs updates --- docs/start/framework/rendering.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/start/framework/rendering.md b/docs/start/framework/rendering.md index d5e87bde31..687c701714 100644 --- a/docs/start/framework/rendering.md +++ b/docs/start/framework/rendering.md @@ -11,7 +11,17 @@ There are three rendering strategies in React Router: - Server Side Rendering - Static Pre-rendering -All routes are always client side rendered as the user navigates around the app. However, you can control server rendering and static pre-rendering with the `ssr` and `prerender` config options. +## Client Side Rendering + +All routes are always client side rendered as the user navigates around the app. If you're looking to build a Single Page App, disable server rendering: + +```ts filename=react-router.config.ts +import type { Config } from "@react-router/dev/config"; + +export default { + ssr: false, +} satisfies Config; +``` ## Server Side Rendering From 51421084df14e01343beb83f56df061e2fbc9641 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Tue, 19 Nov 2024 17:28:04 -0700 Subject: [PATCH 08/41] docs docs docs --- docs/how-to/error-boundary.md | 4 ++ docs/how-to/headers.md | 129 +++++++++++++++++++++------------- docs/how-to/status.md | 58 +++++++++++++++ 3 files changed, 143 insertions(+), 48 deletions(-) create mode 100644 docs/how-to/error-boundary.md create mode 100644 docs/how-to/status.md diff --git a/docs/how-to/error-boundary.md b/docs/how-to/error-boundary.md new file mode 100644 index 0000000000..00d6c9dd70 --- /dev/null +++ b/docs/how-to/error-boundary.md @@ -0,0 +1,4 @@ +--- +title: Error Boundaries +hidden: true +--- diff --git a/docs/how-to/headers.md b/docs/how-to/headers.md index 239f926bde..f28746f8e0 100644 --- a/docs/how-to/headers.md +++ b/docs/how-to/headers.md @@ -1,69 +1,102 @@ --- -title: Setting HTTP Headers and Status -hidden: true +title: HTTP Headers --- -# HTTP Headers and Status +# HTTP Headers - +Headers are defined with the route module `headers` export. - +## From Route Modules -If you need to return a custom HTTP status code or custom headers from your `loader`, use [`data`][data]: +```tsx filename=some-route.tsx +import { Route } from "./+types/some-route"; -```tsx filename=app/product.tsx lines=[3,6-8,14,17-21] -// route("products/:pid", "./product.tsx"); -import type { Route } from "./+types/product"; -import { data } from "react-router"; -import { fakeDb } from "../db"; - -export function headers({ loaderHeaders }: HeadersArgs) { - return loaderHeaders; +export function headers(_: Route.HeadersArgs) { + return { + "Content-Security-Policy": "default-src 'self'", + "X-Frame-Options": "DENY", + "X-Content-Type-Options": "nosniff", + "Cache-Control": "max-age=3600, s-maxage=86400", + }; } +``` + +You can return either a [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) instance or `HeadersInit`. -export async function loader({ params }: Route.LoaderArgs) { - const product = await fakeDb.getProduct(params.pid); +## From loaders and actions - if (!product) { - throw data(null, { status: 404 }); - } +When the header is dependent on loader data, loaders and actions can also set headers. + +### 1. Wrap your return value in `data` + +```tsx lines=[1,8] +import { data } from "react-router"; - return data(product, { +export async function loader({ params }: LoaderArgs) { + let [page, ms] = await fakeTimeCall( + await getPage(params.id) + ); + + return data(page, { headers: { - "Cache-Control": "public; max-age=300", + "Server-Timing': `page;dur=${ms};desc=`Page query"', }, }); } ``` - +### 2. Return from `headers` export -If you need to return a custom HTTP status code or custom headers from your `action`, you can do so using the [`data`][data] utility: +Headers from loaders and actions are not sent in a hidden way, you must return them from the `headers` export. -```tsx filename=app/project.tsx lines=[3,11-14,19] -// route('/projects/:projectId', './project.tsx') -import type { Route } from "./+types/project"; -import { data } from "react-router"; -import { fakeDb } from "../db"; - -export async function action({ - request, -}: Route.ActionArgs) { - let formData = await request.formData(); - let title = await formData.get("title"); - if (!title) { - throw data( - { message: "Invalid title" }, - { status: 400 } - ); - } - - if (!projectExists(title)) { - let project = await fakeDb.createProject({ title }); - return data(project, { status: 201 }); - } else { - let project = await fakeDb.updateProject({ title }); - return project; - } +```tsx +export function headers({ + actionHeaders, + loaderHeaders, +}: HeadersArgs) { + return actionHeaders ? actionHeaders : loaderHeaders; +} +``` + +## Merging with parent headers + +Consider these nested routes + +```ts filename=routes.ts +route("pages", "pages-layout-with-nav.tsx", [ + route(":slug", "page.tsx"), +]); +``` + +If both route modules want to set headers, the headers from the deepest matching route will be sent. + +When you need to keep both the parent and the child headers, you need to merge them in the child route. + +### Appending + +The easiest way is to simply append to the parent headers. This avoids overwriting a header the parent may have set and both are important. + +```tsx +export function headers({ parentHeaders }: HeadersArgs) { + parentHeaders.append( + "Permissions-Policy: geolocation=()" + ); + return parentHeaders; } ``` + +### Setting + +Sometimes it's important to overwrite the parent header. Do this with `set` instead of `append`: + +```tsx +export function headers({ parentHeaders }: HeadersArgs) { + parentHeaders.set( + "Cache-Control", + "max-age=3600, s-maxage=86400" + ); + return parentHeaders; +} +``` + +You can avoid the need to merge headers by only defining headers in "leaf routes" (index routes and child routes without children) and not in parent routes. diff --git a/docs/how-to/status.md b/docs/how-to/status.md new file mode 100644 index 0000000000..8d6cff1ca7 --- /dev/null +++ b/docs/how-to/status.md @@ -0,0 +1,58 @@ +--- +title: Status Codes +--- + +# Status Codes + +Set status codes from loaders and actions with `data`. + +```tsx filename=app/project.tsx lines=[3,12-15,20,23] +// route('/projects/:projectId', './project.tsx') +import type { Route } from "./+types/project"; +import { data } from "react-router"; +import { fakeDb } from "../db"; + +export async function action({ + request, +}: Route.ActionArgs) { + let formData = await request.formData(); + let title = await formData.get("title"); + if (!title) { + return data( + { message: "Invalid title" }, + { status: 400 } + ); + } + + if (!projectExists(title)) { + let project = await fakeDb.createProject({ title }); + return data(project, { status: 201 }); + } else { + let project = await fakeDb.updateProject({ title }); + // the default status code is 200, no need for `data` + return project; + } +} +``` + +See [Form Validation](./form-validation) for more information on rendering form errors like this. + +Another common status code is 404: + +```tsx +// route('/projects/:projectId', './project.tsx') +import type { Route } from "./+types/project"; +import { data } from "react-router"; +import { fakeDb } from "../db"; + +export async function loader({ params }: Route.ActionArgs) { + let project = await fakeDb.getProject(params.id); + if (!project) { + // throw to ErrorBoundary + throw data(null, { status: 404 }); + } + return project; +} +``` + +See the [Error Boundaries](./error-boundary) for more information on thrown `data`. From 7f0ef8fe82789a243697d079db2ab3eea6a88953 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Tue, 19 Nov 2024 17:31:03 -0700 Subject: [PATCH 09/41] docs wordsmithing --- docs/how-to/form-validation.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/how-to/form-validation.md b/docs/how-to/form-validation.md index 68c48e7f88..7376c13f19 100644 --- a/docs/how-to/form-validation.md +++ b/docs/how-to/form-validation.md @@ -4,7 +4,7 @@ title: Form Validation # Form Validation -This guide walks you through implementing form validation for a simple signup form. Here, we focus on capturing the fundamentals to help you understand the essential elements of form validation in Remix, including actions, action data, and rendering errors. +This guide walks through a simple signup form implementation. You will likely want to pair these concepts with third-party validation libraries and error components, but this guide only focuses on the moving pieces for React Router. ## 1. Setting Up @@ -113,7 +113,3 @@ export default function Signup(_: Route.ComponentProps) { ); } ``` - -## Conclusion - -And there you have it! You've successfully set up a basic form validation flow in Remix. The beauty of this approach is that the errors will automatically display based on the `action` data, and they will be updated each time the user re-submits the form. This reduces the amount of boilerplate code you have to write, making your development process more efficient. From 06e98c3baf8af79543cfc82fa8978eeedbafdbd1 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Tue, 19 Nov 2024 21:05:16 -0500 Subject: [PATCH 10/41] typegen: headers (#12318) --- packages/react-router-dev/typegen/generate.ts | 3 +++ packages/react-router/lib/types/route-module.ts | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/packages/react-router-dev/typegen/generate.ts b/packages/react-router-dev/typegen/generate.ts index 274dae4f52..bc291a1f38 100644 --- a/packages/react-router-dev/typegen/generate.ts +++ b/packages/react-router-dev/typegen/generate.ts @@ -55,6 +55,9 @@ export function generate(ctx: Context, route: RouteManifestEntry): string { export type MetaDescriptors = T.MetaDescriptors export type MetaFunction = (args: MetaArgs) => MetaDescriptors + export type HeadersArgs = T.HeadersArgs + export type HeadersFunction = (args: HeadersArgs) => Headers | HeadersInit + export type LoaderArgs = T.CreateServerLoaderArgs export type ClientLoaderArgs = T.CreateClientLoaderArgs export type ActionArgs = T.CreateServerActionArgs diff --git a/packages/react-router/lib/types/route-module.ts b/packages/react-router/lib/types/route-module.ts index c801b75ab5..6514ca9778 100644 --- a/packages/react-router/lib/types/route-module.ts +++ b/packages/react-router/lib/types/route-module.ts @@ -10,6 +10,7 @@ type IsDefined = Equal extends true ? false : true; type RouteModule = { meta?: Func; links?: Func; + headers?: Func; loader?: Func; clientLoader?: Func; action?: Func; @@ -58,6 +59,13 @@ export type CreateMetaArgs = { }; export type MetaDescriptors = MetaDescriptor[]; +export type HeadersArgs = { + loaderHeaders: Headers; + parentHeaders: Headers; + actionHeaders: Headers; + errorHeaders: Headers | undefined; +}; + // prettier-ignore type IsHydrate = ClientLoader extends { hydrate: true } ? true : From f41e9d8925499113ec6796f316718476cc4606fc Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Wed, 20 Nov 2024 09:34:07 -0700 Subject: [PATCH 11/41] docs: bit more on headers --- docs/how-to/headers.md | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/how-to/headers.md b/docs/how-to/headers.md index f28746f8e0..dc5bf1f393 100644 --- a/docs/how-to/headers.md +++ b/docs/how-to/headers.md @@ -4,7 +4,7 @@ title: HTTP Headers # HTTP Headers -Headers are defined with the route module `headers` export. +Headers are primarily defined with the route module `headers` export. You can also set headers in `entry.server.tsx`. ## From Route Modules @@ -100,3 +100,34 @@ export function headers({ parentHeaders }: HeadersArgs) { ``` You can avoid the need to merge headers by only defining headers in "leaf routes" (index routes and child routes without children) and not in parent routes. + +## From `entry.server.tsx` + +The `handleRequest` export receives the headers from the route module as an argument. You can append global headers here. + +```tsx +export default function handleRequest( + request, + responseStatusCode, + responseHeaders, + routerContext, + loadContext +) { + // set, append global headers + responseHeaders.set( + "X-App-Version", + routerContext.manifest.version + ); + + return new Response(await getStream(), { + headers: responseHeaders, + status: responseStatusCode, + }); +} +``` + +If you don't have an `entry.server.tsx` run the `reveal` command: + +```shellscript nonumber +react-router reveal +``` From eadc804ba39a6d7809860cb2f2c9249784dd9283 Mon Sep 17 00:00:00 2001 From: Brooks Lybrand Date: Wed, 20 Nov 2024 11:08:28 -0600 Subject: [PATCH 12/41] Framework adoption guides (#12317) * Update component routes guide * Finish code and update language for component routes guide * update route-provider doc to match component-routes doc --- docs/upgrading/component-routes.md | 235 ++++++++++++++++++++--------- docs/upgrading/router-provider.md | 214 +++++++++++++++++++++----- 2 files changed, 335 insertions(+), 114 deletions(-) diff --git a/docs/upgrading/component-routes.md b/docs/upgrading/component-routes.md index 53b7c19665..0cf24714c0 100644 --- a/docs/upgrading/component-routes.md +++ b/docs/upgrading/component-routes.md @@ -2,15 +2,13 @@ title: Framework Adoption from Component Routes --- -This guide is mostly a stub and in active development, it will be wrong about many things before the final v7 release +# Framework Adoption from Component Routes -# Adopting Vite (Routes) - -If you are using `` please see [Adopting Route Modules from RouterProvider](./vite-router-provider) instead. +If you are using `` please see [Framework Adoption from RouterProvider][upgrade-router-provider] instead. If you are using `` this is the right place. -The React Router vite plugin adds framework features to React Router. This document wil help you adopt the plugin in your app if you'd like. +The React Router Vite plugin adds framework features to React Router. This guide will help you adopt the plugin in your app. If you run into any issues, please reach out for help on [Twitter](https://x.com/remix_run) or [Discord](https://rmx.as/discord). ## Features @@ -18,24 +16,37 @@ The Vite plugin adds: - Route loaders, actions, and automatic data revalidation - Typesafe Routes Modules -- Typesafe Route paths across your app - Automatic route code-splitting - Automatic scroll restoration across navigations - Optional Static pre-rendering - Optional Server rendering -- Optional React Server Components -The initial setup will likely be a bit of a pain, but once complete, adopting the new features is incremental, you can do one route at a time. +The initial setup will require the most work, but once complete, adopting the new features is incremental, you can do one route at a time. + +## Prerequisites + +In order to use the Vite plugin, your project needs to be running -## 1. Install Vite +- Node.js 20+ (if using Node as your runtime) +- Vite 5+ -First install the React Router vite plugin: +## 1. Install the Vite plugin + +**👉 Install the React Router Vite plugin** ```shellscript nonumber npm install -D @react-router/dev ``` -Then swap out the React plugin for React Router. +**👉 Install a runtime adapter** + +We will assume you are using Node as your runtime + +```shellscript nonumber +npm install @react-router/node +``` + +**👉 Swap out the React plugin for React Router.** ```diff filename=vite.config.ts -import react from '@vitejs/plugin-react' @@ -51,13 +62,34 @@ export default defineConfig({ }); ``` -## 2. Add the Root entry point +## 2. Add the React Router config + +**👉 Create a `react-router.config.ts` file** + +Add the following to the root of your project. In this config you can tell React Router about your project, like where to find the app directory and to not use SSR (server-side rendering) for now. + +```shellscript nonumber +touch react-router.config.ts +``` + +```ts filename=react-router.config.ts +import type { Config } from "@react-router/dev/config"; + +export default { + appDirectory: "src", + ssr: false, +} satisfies Config; +``` + +## 3. Add the Root entry point In a typical Vite app, the `index.html` file is the entry point for bundling. The React Router Vite plugin moves the entry point to a `root.tsx` file so you can use React to render the shell of your app instead of static HTML, and eventually upgrade to Server Rendering if you want. +**👉 Move your existing `index.html` to `root.tsx`** + For example, if your current `index.html` looks like this: -```html filename="index.html" +```html filename=index.html @@ -77,76 +109,97 @@ For example, if your current `index.html` looks like this: You would move that markup into `src/root.tsx` and delete `index.html`: +```shellscript nonumber +touch src/root.tsx +``` + ```tsx filename=src/root.tsx import { - Scripts, + Links, + Meta, Outlet, + Scripts, ScrollRestoration, } from "react-router"; -export default function Root() { +export function Layout({ + children, +}: { + children: React.ReactNode; +}) { return ( - + My App + + - + {children} ); } + +export default function Root() { + return ; +} ``` -## 3. Add client entry module +## 4. Add client entry module + +In the typical Vite app the `index.html` file points to `src/main.tsx` as the client entry point. React Router uses a file named `src/entry.client.tsx` instead. -In the typical Vite app setup the `index.html` file points to `src/main.tsx` as the client entry point. React Router uses a file named `src/entry.client.tsx` instead. +**👉 Make `src/entry.client.tsx` your entry point** If your current `src/main.tsx` looks like this: ```tsx filename=src/main.tsx -import "./index.css"; import React from "react"; import ReactDOM from "react-dom/client"; -import App from "./App.tsx"; +import { BrowserRouter } from "react-router"; +import "./index.css"; +import App from "./App"; ReactDOM.createRoot( document.getElementById("root")! ).render( - + + + ); ``` -You would rename it to `entry.client.tsx` and have it look like this: +You would rename it to `entry.client.tsx` and change it to this: ```tsx filename=src/entry.client.tsx -import "./index.css"; import React from "react"; import ReactDOM from "react-dom/client"; import { HydratedRouter } from "react-router/dom"; +import "./index.css"; ReactDOM.hydrateRoot( document, - + - + ); ``` - Use `hydrateRoot` instead of `createRoot` - Render a `` instead of your `` component -- Note that we stopped rendering the `` component, it'll come back in a later step, for now we want to simply get the app booting with the new entry points. +- Note: we stopped rendering the `` component. We'll bring it back in a later step, but first we want to get the app to boot with the new entry point. -## 4. Shuffle stuff around +## 5. Shuffle stuff around Between `root.tsx` and `entry.client.tsx`, you may want to shuffle some stuff around between them. @@ -154,41 +207,67 @@ In general: - `root.tsx` contains any rendering things like context providers, layouts, styles, etc. - `entry.client.tsx` should be as minimal as possible -- Remember to _not_ try to render your existing `` component so isolate steps +- Remember to _not_ try to render your existing `` component yet, we'll do that in a later step Note that your `root.tsx` file will be statically generated and served as the entry point of your app, so just that module will need to be compatible with server rendering. This is where most of your trouble will come. -## 5. Boot the app +## 6. Setup your routes -At this point you should be able to to boot the app and see the root layout. +The React Router Vite plugin uses a `routes.ts` file to configure your routes. For now we'll add a simple catchall route to get things going. -```shellscript -npx react-router dev +**👉 Setup a `catchall.tsx` route** + +```shellscript nonumber +touch src/routes.ts src/catchall.tsx ``` -- Search the [Upgrading Discussion](#TODO) category -- Reach out for help on [Twitter](https://x.com/remix_run) or [Discord](https://rmx.as/discord) +```ts filename=src/routes.ts +import { + type RouteConfig, + route, +} from "@react-router/dev/routes"; -Make sure you can boot your app at this point before moving on. +export default [ + // * matches all URLs, the ? makes it optional so it will match / as well + route("*?", "catchall.tsx"), +] satisfies RouteConfig; +``` -## 6. Configure Catchall Route +**👉 Render a placeholder route** -To get back to rendering your app, we'll configure a "catchall" route that matches all URLs so that your existing `` get a chance to render. +Eventually we'll replace this with our original `App` component, but for now we'll just render something simple to make sure we can boot the app. -Create a file at `src/routes.ts` and add this: +```tsx filename=src/catchall.tsx +export default function Component() { + return
Hello, world!
; +} +``` -```ts filename=src/routes.ts -import { type RouteConfig } from "@react-router/dev/routes"; +[View our guide on configuring routes][configuring-routes] to learn more about the `routes.ts` file. -export default [ - { - path: "*", - file: "src/catchall.tsx", - }, -] satisfies RouteConfig; +## 7. Boot the app + +At this point you should be able to to boot the app and see the root layout. + +**👉 Add `dev` script and run the app** + +```json filename=package.json +"scripts": { + "dev": "react-router dev" +} +``` + +Now make sure you can boot your app at this point before moving on: + +```shellscript +npm run dev ``` -And then create the catchall route module and render your existing root App component within it. +## 8. Render your app + +To get back to rendering your app, we'll update the "catchall" route we setup earlier that matches all URLs so that your existing `` get a chance to render. + +**👉 Update the catchall route to render your app** ```tsx filename=src/catchall.tsx import App from "./App"; @@ -200,7 +279,13 @@ export default function Component() { Your app should be back on the screen and working as usual! -## 6. Migrate a route to a Route Module + + +Note: You will get some warnings in your console about descendent routes. You can ignore these for now as it is caused by the temporary catchall route. Once you move all your routes to Route Modules this will go away. + + + +## 9. Migrate a route to a Route Module You can now incrementally migrate your routes to route modules. @@ -208,57 +293,55 @@ Given an existing route like this: ```tsx filename=src/App.tsx // ... -import Page from "./containers/page"; +import About from "./containers/About"; export default function App() { return ( - } /> + } /> ); } ``` -You can move the definition to a `routes.ts` file: +**👉 Add the route definition to `routes.ts`** ```tsx filename=src/routes.ts -import { type RouteConfig } from "@react-router/dev/routes"; +import { + type RouteConfig, + route, +} from "@react-router/dev/routes"; export default [ - { - path: "/pages/:id", - file: "./containers/page.tsx", - }, - { - path: "*", - file: "src/catchall.tsx", - }, + route("/about", "./pages/about.tsx"), + route("*?", "catchall.tsx"), ] satisfies RouteConfig; ``` -And then edit the route module to use the Route Module API: +**👉 Add the route module** -```tsx filename=src/pages/about.tsx -import { useLoaderData } from "react-router"; +Edit the route module to use the [Route Module API][route-modules]: -export async function clientLoader({ params }) { - let page = await getPage(params.id); - return page; +```tsx filename=src/pages/about.tsx +export async function clientLoader() { + // you can now fetch data here + return { + title: "About page", + }; } -export default function Component() { - let data = useLoaderData(); - return

{data.title}

; +export default function Component({ loaderData }) { + return

{loaderData.title}

; } ``` -You'll now get inferred type safety with params, loader data, and more. +See [Type Safety][type-safety] to setup autogenerated type safety for params, loader data, and more. The first few routes you migrate are the hardest because you often have to access various abstractions a bit differently than before (like in a loader instead of from a hook or context). But once the trickiest bits get dealt with, you get into an incremental groove. -## Enable SSR and Pre-rendering +## Enable SSR and/or Pre-rendering -If you want to enable server rendering and static pre-rendering, you can do so with the `ssr` and `prerender` options in the bundler plugin. +If you want to enable server rendering and static pre-rendering, you can do so with the `ssr` and `prerender` options in the bundler plugin. For SSR you'll need to also deploy the server build to a server. See [Deploying][deploying] for more information. ```ts filename=vite.config.ts import type { Config } from "@react-router/dev/config"; @@ -271,6 +354,8 @@ export default { } satisfies Config; ``` -See [Deploying][deploying] for more information on deploying a server. - +[upgrade-router-provider]: ./router-provider [deploying]: ../start/deploying +[configuring-routes]: ../start/framework/routing +[route-modules]: ../start/framework/route-module +[type-safety]: ../how-to/route-module-type-safety diff --git a/docs/upgrading/router-provider.md b/docs/upgrading/router-provider.md index 95b2a21407..06aa951eb5 100644 --- a/docs/upgrading/router-provider.md +++ b/docs/upgrading/router-provider.md @@ -12,11 +12,11 @@ Checkout this @@ -83,43 +117,63 @@ If your current `index.html` looks like this: You would move that markup into `src/root.tsx` and delete `index.html`: +```shellscript nonumber +touch src/root.tsx +``` + ```tsx filename=src/root.tsx import { - Scripts, + Links, + Meta, Outlet, + Scripts, ScrollRestoration, } from "react-router"; -export default function Root() { +export function Layout({ + children, +}: { + children: React.ReactNode; +}) { return ( - + My App + + - + {children} ); } + +export default function Root() { + return ; +} ``` -## 3. Add client entry module +## 4. Add client entry module + + + +In the typical Vite app the `index.html` file points to `src/main.tsx` as the client entry point. React Router uses a file named `src/entry.client.tsx` instead. -In the typical Vite app setup the `index.html` file points to `src/main.tsx` as the client entry point. React Router uses a file named `src/entry.client.tsx`. +**👉 Make `src/entry.client.tsx` your entry point** If your current `src/main.tsx` looks like this: ```tsx filename=src/main.tsx -import * as React from "react"; -import * as ReactDOM from "react-dom"; +import React from "react"; +import ReactDOM from "react-dom/client"; import { createBrowserRouter, RouterProvider, @@ -127,31 +181,36 @@ import { const router = createBrowserRouter(YOUR_ROUTES); -ReactDOM.createRoot(document.getElementById("root")).render( - +ReactDOM.createRoot( + document.getElementById("root")! +).render( + + + ); ``` You would rename it to `entry.client.tsx` and change it to this: ```tsx filename=src/entry.client.tsx -import * as React from "react"; -import * as ReactDOM from "react-dom"; -import { HydratedRouter } from "react-router-dom"; +import React from "react"; +import ReactDOM from "react-dom/client"; +import { HydratedRouter } from "react-router/dom"; +import "./index.css"; ReactDOM.hydrateRoot( document, - - - + + + ); ``` - Use `hydrateRoot` instead of `createRoot` - Use `` instead of `` -- Pass your routes to `` +- Note: we stopped passing the routes prop for now. We'll bring it back in a later step, but first we want to get the app to boot with the new entry point. -## 4. Shuffle stuff around +## 5. Shuffle stuff around Between `root.tsx` and `entry.client.tsx`, you may want to shuffle some stuff around between them. @@ -159,17 +218,84 @@ In general: - `root.tsx` contains any rendering things like context providers, layouts, styles, etc. - `entry.client.tsx` should be as minimal as possible +- Remember to _not_ try to pass your existing routes yet, we'll do that in a later step Note that your `root.tsx` file will be statically generated and served as the entry point of your app, so just that module will need to be compatible with server rendering. This is where most of your trouble will come. -## 5. Boot the app +## 6. Setup your routes -At this point you should be able to to boot the app. +The React Router Vite plugin uses a `routes.ts` file to configure your routes. For now we'll add a simple catchall route to get things going. + +**👉 Setup a `catchall.tsx` route** + +```shellscript nonumber +touch src/routes.ts src/catchall.tsx +``` + +```ts filename=src/routes.ts +import { + type RouteConfig, + route, +} from "@react-router/dev/routes"; + +export default [ + // * matches all URLs, the ? makes it optional so it will match / as well + route("*?", "catchall.tsx"), +] satisfies RouteConfig; +``` + +**👉 Render a placeholder route** + +Eventually we'll replace this with our original routes, but for now we'll just render something simple to make sure we can boot the app. + +```tsx filename=src/catchall.tsx +export default function Component() { + return
Hello, world!
; +} +``` + +[View our guide on configuring routes][configuring-routes] to learn more about the `routes.ts` file. + +## 7. Boot the app + +At this point you should be able to to boot the app and see the root layout. + +**👉 Add `dev` script and run the app** + +```json filename=package.json +"scripts": { + "dev": "react-router dev" +} +``` + +Now make sure you can boot your app at this point before moving on: ```shellscript -npx react-router dev +npm run dev ``` +## 8. Add your routes + +To get back to rendering your routes, we'll update the `routes.ts` file to include your existing routes. + +**👉 Update routes.ts with your existing routes** + +```ts filename=src/routes.ts +import { + type RouteConfig, + route, +} from "@react-router/dev/routes"; + +// Convert your existing routes to the new format +export default [ + route("/", "routes/index.tsx"), + route("/about", "routes/about.tsx"), + // ... other routes +] satisfies RouteConfig; +``` + +Your app should be back on the screen and working as usual! + If you're having trouble - Comment out the `routes` prop to `` to isolate the problem to the new entry points @@ -178,7 +304,7 @@ If you're having trouble Make sure you can boot your app at this point before moving on. -## 6. Migrate a route to a Route Module +## 9. Migrate a route to a Route Module You can now incrementally migrate your routes to route modules. First create a `routes.ts` file that exports your routes. @@ -233,21 +359,31 @@ export default function Component() { } ``` -You'll now get inferred type safety with params, loader data, and more. +See [Type Safety][type-safety] to setup autogenerated type safety for params, loader data, and more. -The first few routes you migrate are the hardest because you often have to access the same abstractions a bit differently than before (like in a loader instead of from a hook or context). But once the trickiest bits get dealt with, you get into an incremental groove. +The first few routes you migrate are the hardest because you often have to access various abstractions a bit differently than before (like in a loader instead of from a hook or context). But once the trickiest bits get dealt with, you get into an incremental groove. ## Enable SSR and/or Pre-rendering -If you want to enable server rendering and static pre-rendering, you can do so with the `ssr` and `prerender` options in the bundler plugin. For SSR you'll need to also deploy the server build to a server. See [Deploying](../start/deploying) for more information. +If you want to enable server rendering and static pre-rendering, you can do so with the `ssr` and `prerender` options in the bundler plugin. For SSR you'll need to also deploy the server build to a server. See [Deploying][deploying] for more information. + +## Enable SSR and Pre-rendering -```ts filename=vite.config.ts +If you want to enable server rendering and static pre-rendering, you can do so with the `ssr` and `prerender` options in the bundler plugin. + +```ts filename=react-router.config.ts import type { Config } from "@react-router/dev/config"; export default { ssr: true, async prerender() { - return ["/", "/pages/about"]; + return ["/", "/about", "/contact"]; }, } satisfies Config; ``` + +[upgrade-component-routes]: ./component-routes +[deploying]: ../start/deploying +[configuring-routes]: ../start/framework/routing +[route-modules]: ../start/framework/route-module +[type-safety]: ../how-to/route-module-type-safety From 39630069d3780897eb04451e8d4f58b075b34462 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Wed, 20 Nov 2024 11:31:13 -0700 Subject: [PATCH 13/41] widen descendant route matching warning (#12324) --- packages/react-router/lib/hooks.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index d8c7771079..13027e2da9 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -479,7 +479,7 @@ export function useRoutesImpl( let parentPath = (parentRoute && parentRoute.path) || ""; warningOnce( parentPathname, - !parentRoute || parentPath.endsWith("*"), + !parentRoute || parentPath.endsWith("*") || parentPath.endsWith("*?"), `You rendered descendant (or called \`useRoutes()\`) at ` + `"${parentPathname}" (under ) but the ` + `parent route path has no trailing "*". This means if you navigate ` + From f79bf3df4d096bbd66ad451b2f3ed61dc7b69637 Mon Sep 17 00:00:00 2001 From: Brooks Lybrand Date: Wed, 20 Nov 2024 12:45:58 -0600 Subject: [PATCH 14/41] docs: remove old routing conventions from file uploads doc (#12323) --- docs/how-to/file-uploads.md | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/docs/how-to/file-uploads.md b/docs/how-to/file-uploads.md index 7a66ccb715..86ffd46ac0 100644 --- a/docs/how-to/file-uploads.md +++ b/docs/how-to/file-uploads.md @@ -1,17 +1,33 @@ --- title: File Uploads -# need to validate this guide and get rid of remixisms like file system routes -hidden: true --- # File Uploads Handle file uploads in your React Router applications. This guide uses some packages from the [Remix The Web][remix-the-web] project to make file uploads easier. -_Thank you to David Adams for [his original guide](https://programmingarehard.com/2024/09/06/remix-file-uploads-updated.html/) on how to implement file uploads in Remix. You can refer to it for even more examples._ +_Thank you to David Adams for [writing an original guide](https://programmingarehard.com/2024/09/06/remix-file-uploads-updated.html/) which this doc is based on. You can refer to it for even more examples._ ## Basic File Upload +👉 **Setup some routes** + +You can setup your routes however you like. This example uses the following structure: + +```ts filename=routes.ts +import { + type RouteConfig, + route, +} from "@react-router/dev/routes"; + +export default [ + // ... other routes + route("user/:id", "pages/user-profile.tsx", [ + route("avatar", "api/upload-avatar.tsx"), + ]), +] satisfies RouteConfig; +``` + 👉 **Add the form data parser** `form-data-parser` is a wrapper around `request.formData()` that provides streaming support for handling file uploads. @@ -32,7 +48,7 @@ You must set the form's `enctype` to `multipart/form-data` for file uploads to w -```tsx filename=routes/user.$id.tsx +```tsx filename=pages/user-profile.tsx import { type FileUpload, parseFormData, @@ -57,10 +73,10 @@ export async function action({ export default function Component() { return ( -
+ -
+ ); } ``` @@ -97,7 +113,7 @@ export function getStorageKey(userId: string) { Update the form's `action` to store files in the `fileStorage` instance. -```tsx filename=routes/user.$id.tsx +```tsx filename=pages/user-profile.tsx import { FileUpload, parseFormData, @@ -106,7 +122,7 @@ import { fileStorage, getStorageKey, } from "~/avatar-storage.server"; -import type { Route } from "./+types/user"; +import type { Route } from "./+types/user-profile"; export async function action({ request, @@ -165,11 +181,12 @@ export default function UserPage({ Create a [resource route][resource-route] that streams the file as a response. -```tsx filename=routes/user.$id.avatar.tsx +```tsx filename=api/upload-avatar.tsx import { fileStorage, getStorageKey, } from "~/avatar-storage.server"; +import type { Route } from "./+types/upload-avatar"; export async function loader({ params }: Route.LoaderArgs) { const storageKey = getStorageKey(params.id); @@ -194,4 +211,4 @@ export async function loader({ params }: Route.LoaderArgs) { [form-data-parser]: https://github.com/mjackson/remix-the-web/tree/main/packages/form-data-parser [file-storage]: https://github.com/mjackson/remix-the-web/tree/main/packages/file-storage [file]: https://developer.mozilla.org/en-US/docs/Web/API/File -[resource-route]: ../how-to/resource-routes.md +[resource-route]: ../how-to/resource-routes From 20899a09995b247dace2d31d6a1d6ddda1109df8 Mon Sep 17 00:00:00 2001 From: Brooks Lybrand Date: Wed, 20 Nov 2024 12:52:58 -0600 Subject: [PATCH 15/41] docs: remove warning from component-routes.md --- docs/upgrading/component-routes.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/upgrading/component-routes.md b/docs/upgrading/component-routes.md index 0cf24714c0..37dcb1ec7b 100644 --- a/docs/upgrading/component-routes.md +++ b/docs/upgrading/component-routes.md @@ -279,12 +279,6 @@ export default function Component() { Your app should be back on the screen and working as usual! - - -Note: You will get some warnings in your console about descendent routes. You can ignore these for now as it is caused by the temporary catchall route. Once you move all your routes to Route Modules this will go away. - - - ## 9. Migrate a route to a Route Module You can now incrementally migrate your routes to route modules. From 811760695c1e682cb9103c6f27bea1be04ef4b31 Mon Sep 17 00:00:00 2001 From: Brooks Lybrand Date: Wed, 20 Nov 2024 13:02:22 -0600 Subject: [PATCH 16/41] move ssr: true from vite.config to react-router.config in basic template --- templates/basic/react-router.config.ts | 2 ++ templates/basic/vite.config.ts | 8 +------- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/templates/basic/react-router.config.ts b/templates/basic/react-router.config.ts index 9c6ea11734..6ff16f9177 100644 --- a/templates/basic/react-router.config.ts +++ b/templates/basic/react-router.config.ts @@ -2,4 +2,6 @@ import type { Config } from "@react-router/dev/config"; export default { // Config options... + // Server-side render by default, to enable SPA mode set this to `false` + ssr: true, } satisfies Config; diff --git a/templates/basic/vite.config.ts b/templates/basic/vite.config.ts index b5a580145a..14fd055e30 100644 --- a/templates/basic/vite.config.ts +++ b/templates/basic/vite.config.ts @@ -3,11 +3,5 @@ import tsconfigPaths from "vite-tsconfig-paths"; import { defineConfig } from "vite"; export default defineConfig({ - plugins: [ - reactRouter({ - // Server-side render by default, to enable SPA mode set this to `false` - ssr: true, - }), - tsconfigPaths(), - ], + plugins: [reactRouter(), tsconfigPaths()], }); From 11ae51284f505bc465fc6c25b0175db71174de73 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Wed, 20 Nov 2024 15:10:19 -0700 Subject: [PATCH 17/41] docs: error boundaries --- docs/how-to/error-boundary.md | 113 ++++++++++++++++++++++++++++++++- docs/how-to/error-reporting.md | 4 ++ 2 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 docs/how-to/error-reporting.md diff --git a/docs/how-to/error-boundary.md b/docs/how-to/error-boundary.md index 00d6c9dd70..296b152237 100644 --- a/docs/how-to/error-boundary.md +++ b/docs/how-to/error-boundary.md @@ -1,4 +1,115 @@ --- title: Error Boundaries -hidden: true --- + +# Error Boundaries + +To avoid rendering an empty page to users, route modules will automatically catch errors in your code and render the closest `ErrorBoundary`. + +Error boundaries are not intended for error reporting or rendering form validation errors. Please see [Form Validation](./form-validation) and [Error Reporting](./error-reporting) instead. + +## 1. Add a root error boundary + +All applications should at a minimum export a root error boundary. This one handles the three main cases: + +- Thrown `data` with a status code and text +- Instances of errors with a stack trace +- Randomly thrown values + +```tsx filename=root.tsx +import { Route } from "./+types/root"; + +export function ErrorBoundary({ + error, +}: Route.ErrorBoundaryProps) { + console.error(error); + if (isRouteErrorResponse(error)) { + return ( + <> +

+ {error.status} {error.statusText} +

+

{error.data}

+ + ); + } else if (error instanceof Error) { + return ( +
+

Error

+

{error.message}

+

The stack trace is:

+
{error.stack}
+
+ ); + } else { + return

Unknown Error

; + } +} +``` + +## 2. Write a bug + +It's not recommended to intentionally throw errors to force the error boundary to render as a means of control flow. Error Boundaries are primarily for catching unintentional errors in your code. + +```tsx +export async function loader() { + return undefined(); +} +``` + +This will render the `instanceof Error` branch of the UI from step 1. + +This is not just for loaders, but for all route module APIs: loaders, actions, components, headers, links, and meta. + +## 3. Throw data in loaders/actions + +There are exceptions to the rule in #2, especially 404s. You can intentionally `throw data()` (with a proper status code) to the closest error boundary when your loader can't find what it needs to render the page. Throw a 404 and move on. + +```tsx +import { data } from "react-router"; + +export async function loader({ params }) { + let record = await fakeDb.getRecord(params.id); + if (!record) { + throw data("Record Not Found", { status: 404 }); + } + return record; +} +``` + +This will render the `isRouteErrorResponse` branch of the UI from step 1. + +## 4. Nested error boundaries + +When an error is thrown, the "closest error boundary" will be rendered. Consider these nested routes: + +```tsx filename="routes.ts" +// ✅ has error boundary +route("/app", "app.tsx", [ + // ❌ no error boundary + route("invoices", "invoices.tsx", [ + // ✅ has error boundary + route("invoices/:id", "invoice-page.tsx", [ + // ❌ no error boundary + route("payments", "payments.tsx"), + ]), + ]), +]); +``` + +The following table shows which error boundary will render given the origin of the error: + +| error origin | rendered boundary | +| ---------------- | ----------------- | +| app.tsx | app.tsx | +| invoices.tsx | app.tsx | +| invoice-page.tsx | invoice-page.tsx | +| payments.tsx | invoice-page.tsx | + +## Error Sanitization + +In production mode, any errors that happen on the server are automatically sanitized before being sent to the browser to prevent leaking any sensitive server information (like stack traces). + +This means that a thrown `Error` will have a generic message and no stack trace in production in the browser. The original error is untouched on the server. + +Also note that data sent with `throw data(yourData)` is not sanitized as the data there is intended to be rendered. diff --git a/docs/how-to/error-reporting.md b/docs/how-to/error-reporting.md new file mode 100644 index 0000000000..9dad8283b7 --- /dev/null +++ b/docs/how-to/error-reporting.md @@ -0,0 +1,4 @@ +--- +title: Error Reporting +hidden: true +--- From 43c53878abb97b9ade10a593f01b20624b503b3d Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Wed, 20 Nov 2024 15:38:07 -0700 Subject: [PATCH 18/41] docs: added error reporting --- docs/how-to/error-boundary.md | 1 - docs/how-to/error-reporting.md | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/docs/how-to/error-boundary.md b/docs/how-to/error-boundary.md index 296b152237..3bd4daa6e8 100644 --- a/docs/how-to/error-boundary.md +++ b/docs/how-to/error-boundary.md @@ -22,7 +22,6 @@ import { Route } from "./+types/root"; export function ErrorBoundary({ error, }: Route.ErrorBoundaryProps) { - console.error(error); if (isRouteErrorResponse(error)) { return ( <> diff --git a/docs/how-to/error-reporting.md b/docs/how-to/error-reporting.md index 9dad8283b7..685d0ea0f6 100644 --- a/docs/how-to/error-reporting.md +++ b/docs/how-to/error-reporting.md @@ -1,4 +1,36 @@ --- title: Error Reporting -hidden: true --- + +# Error Reporting + +React Router catches errors in your route modules and sends them to [error boundaries](./error-boundary) to prevent blank pages when errors occur. However, ErrorBoundary isn't sufficient for logging and reporting errors. To access these caught errors, use the handleError export of the server entry module. + +## 1. Reveal the server entry + +If you don't see `entry.server.tsx` in your app directory, you're using a default entry. Reveal it with this cli command: + +```shellscript nonumber +react-router reveal +``` + +## 2. Export your error handler + +This function is called whenever React Router catches an error in your application on the server. + +```tsx filename=entry.server.tsx +import { type HandleErrorFunction } from "react-router"; + +export const handleError: HandleErrorFunction = ( + error, + { request } +) => { + // React Router may abort some interrupted requests, don't log those + if (!request.signal.aborted) { + myReportError(error); + + // make sure to still log the error so you can see it + console.error(error); + } +}; +``` From 846800adb50e9c8511d76cbe1a13bcf20e5215ef Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Thu, 21 Nov 2024 11:28:01 +1100 Subject: [PATCH 19/41] Add decision doc for `react-router.config.ts` (#12330) --- decisions/0013-react-router-config-ts.md | 64 ++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 decisions/0013-react-router-config-ts.md diff --git a/decisions/0013-react-router-config-ts.md b/decisions/0013-react-router-config-ts.md new file mode 100644 index 0000000000..f2d112cd37 --- /dev/null +++ b/decisions/0013-react-router-config-ts.md @@ -0,0 +1,64 @@ +# `react-router.config.ts` + +Date: 2024-11-21 + +Status: accepted + +## Context + +Previously in Remix and earlier pre-releases of React Router, framework config was passed directly to the Vite plugin as an options object. While this has worked well so far when limited to Vite-specific use cases or simple CLI commands, we've started to run into some limitations as our `react-router` CLI has become more advanced. + +Some key issues with the current approach: + +1. **Tight coupling with Vite** + + Our CLI commands (`react-router routes` and `react-router typegen`) need access to framework config but have nothing to do with Vite. We previously worked around this by using Vite to resolve `vite.config.ts` and then extracting our React Router config from the resolved Vite config object, but this approach proved to be difficult as we added more features to our CLI. + +2. **Limited config watching capabilities** + + The introduction of `react-router typegen --watch` in particular highlighted the limitations of our Vite-coupled approach. We needed to not only resolve our config but also watch for changes. Having this tied to the Vite config made implementing this functionality unnecessarily complex. + +3. **Heavy-handed config updates** + + Changes to Vite plugin options are treated like any other change to the Vite config, triggering a full reload of the dev server. This takes away any ability for us to handle config updates more gracefully. + +4. **Difficulty with config documentation** + + Documentation of our config options was difficult since we either had to show a complete Vite config file with a lot of extra noise, or only show a call to the `reactRouter` plugin which looked a bit confusing since it was labelled as a `vite.config.ts` file. Neither approach was ideal for clearly explaining our config options while keeping code snippets to a minimum. + +## Goals + +1. Decouple framework config from Vite +2. Enable granular config watching for tools like `react-router typegen --watch` +3. Avoid unnecessary dev server reloads when config changes +4. Improve documentation by separating framework config from Vite config + +## Decisions + +### Introduce dedicated `react-router.config.ts` in the root of the project + +We will introduce a dedicated config file, `react-router.config.ts/js`. + +### Config is provided via a default export + +To maintain consistency with other JS build tool configuration patterns, we will export the config object as the default export of the `react-router.config.ts` file. + +### Change `app/routes.ts` API to use a default export rather than a named `routes` export + +Now that we have multiple config files (`react-router.config.ts` and `app/routes.ts`), we should be internally consistent and use default exports for all of our config files. Now is a good time to make this change since the `routes.ts` API hasn't yet had a stable release. + +### Any config APIs should be exported from `@react-router/dev/config` + +The exported config object should satisfy the `Config` type from `@react-router/dev/config`. This follows our established pattern of using `@react-router/dev/*` namespaces for dev-time APIs that are scoped to particular files, e.g. `@react-router/dev/routes` and `@react-router/dev/vite`. + +### Config file is optional but recommended + +While the lack of a config file won't be treated as an error, we should include a blank config file in all official templates to make the config options more discoverable and self-documenting. + +### Remove options from Vite plugin + +The Vite plugin will no longer accept config options. All framework options will be handled through the dedicated config file. + +### Improved config update handling + +Config changes should no longer trigger full dev server reloads. We may re-introduce this behavior in certain cases where it makes sense. From af0acf1e843bcff84949a98c955e5e754b1bd916 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Thu, 21 Nov 2024 15:01:49 +1100 Subject: [PATCH 20/41] Rename `compiler` playgrounds to `framework` (#12329) --- .../.gitignore | 0 .../app/entry.client.tsx | 0 .../app/entry.server.tsx | 0 .../app/root.tsx | 0 .../app/routes.ts | 0 .../app/routes/_index.tsx | 0 .../package.json | 2 +- .../public/favicon.ico | Bin .../server.js | 0 .../tsconfig.json | 0 .../vite.config.ts | 0 .../{compiler-spa => framework-spa}/.gitignore | 0 .../app/entry.client.tsx | 0 .../app/entry.server.tsx | 0 .../{compiler-spa => framework-spa}/app/root.tsx | 0 .../{compiler-spa => framework-spa}/app/routes.ts | 0 .../app/routes/_index.tsx | 0 .../{compiler-spa => framework-spa}/package.json | 2 +- .../public/favicon.ico | Bin .../react-router.config.ts | 0 .../{compiler-spa => framework-spa}/tsconfig.json | 0 .../{compiler-spa => framework-spa}/vite.config.ts | 0 playground/{compiler => framework}/.gitignore | 0 .../{compiler => framework}/app/entry.client.tsx | 0 .../{compiler => framework}/app/entry.server.tsx | 0 playground/{compiler => framework}/app/root.tsx | 0 playground/{compiler => framework}/app/routes.ts | 0 .../{compiler => framework}/app/routes/_index.tsx | 0 .../{compiler => framework}/app/routes/product.tsx | 0 playground/{compiler => framework}/package.json | 2 +- .../{compiler => framework}/public/favicon.ico | Bin playground/{compiler => framework}/tsconfig.json | 0 playground/{compiler => framework}/vite.config.ts | 0 pnpm-lock.yaml | 6 +++--- 34 files changed, 6 insertions(+), 6 deletions(-) rename playground/{compiler-express => framework-express}/.gitignore (100%) rename playground/{compiler-express => framework-express}/app/entry.client.tsx (100%) rename playground/{compiler-express => framework-express}/app/entry.server.tsx (100%) rename playground/{compiler-express => framework-express}/app/root.tsx (100%) rename playground/{compiler-express => framework-express}/app/routes.ts (100%) rename playground/{compiler-express => framework-express}/app/routes/_index.tsx (100%) rename playground/{compiler-express => framework-express}/package.json (95%) rename playground/{compiler-express => framework-express}/public/favicon.ico (100%) rename playground/{compiler-express => framework-express}/server.js (100%) rename playground/{compiler-express => framework-express}/tsconfig.json (100%) rename playground/{compiler-express => framework-express}/vite.config.ts (100%) rename playground/{compiler-spa => framework-spa}/.gitignore (100%) rename playground/{compiler-spa => framework-spa}/app/entry.client.tsx (100%) rename playground/{compiler-spa => framework-spa}/app/entry.server.tsx (100%) rename playground/{compiler-spa => framework-spa}/app/root.tsx (100%) rename playground/{compiler-spa => framework-spa}/app/routes.ts (100%) rename playground/{compiler-spa => framework-spa}/app/routes/_index.tsx (100%) rename playground/{compiler-spa => framework-spa}/package.json (94%) rename playground/{compiler-spa => framework-spa}/public/favicon.ico (100%) rename playground/{compiler-spa => framework-spa}/react-router.config.ts (100%) rename playground/{compiler-spa => framework-spa}/tsconfig.json (100%) rename playground/{compiler-spa => framework-spa}/vite.config.ts (100%) rename playground/{compiler => framework}/.gitignore (100%) rename playground/{compiler => framework}/app/entry.client.tsx (100%) rename playground/{compiler => framework}/app/entry.server.tsx (100%) rename playground/{compiler => framework}/app/root.tsx (100%) rename playground/{compiler => framework}/app/routes.ts (100%) rename playground/{compiler => framework}/app/routes/_index.tsx (100%) rename playground/{compiler => framework}/app/routes/product.tsx (100%) rename playground/{compiler => framework}/package.json (95%) rename playground/{compiler => framework}/public/favicon.ico (100%) rename playground/{compiler => framework}/tsconfig.json (100%) rename playground/{compiler => framework}/vite.config.ts (100%) diff --git a/playground/compiler-express/.gitignore b/playground/framework-express/.gitignore similarity index 100% rename from playground/compiler-express/.gitignore rename to playground/framework-express/.gitignore diff --git a/playground/compiler-express/app/entry.client.tsx b/playground/framework-express/app/entry.client.tsx similarity index 100% rename from playground/compiler-express/app/entry.client.tsx rename to playground/framework-express/app/entry.client.tsx diff --git a/playground/compiler-express/app/entry.server.tsx b/playground/framework-express/app/entry.server.tsx similarity index 100% rename from playground/compiler-express/app/entry.server.tsx rename to playground/framework-express/app/entry.server.tsx diff --git a/playground/compiler-express/app/root.tsx b/playground/framework-express/app/root.tsx similarity index 100% rename from playground/compiler-express/app/root.tsx rename to playground/framework-express/app/root.tsx diff --git a/playground/compiler-express/app/routes.ts b/playground/framework-express/app/routes.ts similarity index 100% rename from playground/compiler-express/app/routes.ts rename to playground/framework-express/app/routes.ts diff --git a/playground/compiler-express/app/routes/_index.tsx b/playground/framework-express/app/routes/_index.tsx similarity index 100% rename from playground/compiler-express/app/routes/_index.tsx rename to playground/framework-express/app/routes/_index.tsx diff --git a/playground/compiler-express/package.json b/playground/framework-express/package.json similarity index 95% rename from playground/compiler-express/package.json rename to playground/framework-express/package.json index 3902c0aafc..65e646efaa 100644 --- a/playground/compiler-express/package.json +++ b/playground/framework-express/package.json @@ -1,5 +1,5 @@ { - "name": "@playground/compiler-express", + "name": "@playground/framework-express", "version": "0.0.0", "private": true, "sideEffects": false, diff --git a/playground/compiler-express/public/favicon.ico b/playground/framework-express/public/favicon.ico similarity index 100% rename from playground/compiler-express/public/favicon.ico rename to playground/framework-express/public/favicon.ico diff --git a/playground/compiler-express/server.js b/playground/framework-express/server.js similarity index 100% rename from playground/compiler-express/server.js rename to playground/framework-express/server.js diff --git a/playground/compiler-express/tsconfig.json b/playground/framework-express/tsconfig.json similarity index 100% rename from playground/compiler-express/tsconfig.json rename to playground/framework-express/tsconfig.json diff --git a/playground/compiler-express/vite.config.ts b/playground/framework-express/vite.config.ts similarity index 100% rename from playground/compiler-express/vite.config.ts rename to playground/framework-express/vite.config.ts diff --git a/playground/compiler-spa/.gitignore b/playground/framework-spa/.gitignore similarity index 100% rename from playground/compiler-spa/.gitignore rename to playground/framework-spa/.gitignore diff --git a/playground/compiler-spa/app/entry.client.tsx b/playground/framework-spa/app/entry.client.tsx similarity index 100% rename from playground/compiler-spa/app/entry.client.tsx rename to playground/framework-spa/app/entry.client.tsx diff --git a/playground/compiler-spa/app/entry.server.tsx b/playground/framework-spa/app/entry.server.tsx similarity index 100% rename from playground/compiler-spa/app/entry.server.tsx rename to playground/framework-spa/app/entry.server.tsx diff --git a/playground/compiler-spa/app/root.tsx b/playground/framework-spa/app/root.tsx similarity index 100% rename from playground/compiler-spa/app/root.tsx rename to playground/framework-spa/app/root.tsx diff --git a/playground/compiler-spa/app/routes.ts b/playground/framework-spa/app/routes.ts similarity index 100% rename from playground/compiler-spa/app/routes.ts rename to playground/framework-spa/app/routes.ts diff --git a/playground/compiler-spa/app/routes/_index.tsx b/playground/framework-spa/app/routes/_index.tsx similarity index 100% rename from playground/compiler-spa/app/routes/_index.tsx rename to playground/framework-spa/app/routes/_index.tsx diff --git a/playground/compiler-spa/package.json b/playground/framework-spa/package.json similarity index 94% rename from playground/compiler-spa/package.json rename to playground/framework-spa/package.json index b42a057d06..c6244af7d5 100644 --- a/playground/compiler-spa/package.json +++ b/playground/framework-spa/package.json @@ -1,5 +1,5 @@ { - "name": "@playground/compiler-spa", + "name": "@playground/framework-spa", "version": "0.0.0", "private": true, "sideEffects": false, diff --git a/playground/compiler-spa/public/favicon.ico b/playground/framework-spa/public/favicon.ico similarity index 100% rename from playground/compiler-spa/public/favicon.ico rename to playground/framework-spa/public/favicon.ico diff --git a/playground/compiler-spa/react-router.config.ts b/playground/framework-spa/react-router.config.ts similarity index 100% rename from playground/compiler-spa/react-router.config.ts rename to playground/framework-spa/react-router.config.ts diff --git a/playground/compiler-spa/tsconfig.json b/playground/framework-spa/tsconfig.json similarity index 100% rename from playground/compiler-spa/tsconfig.json rename to playground/framework-spa/tsconfig.json diff --git a/playground/compiler-spa/vite.config.ts b/playground/framework-spa/vite.config.ts similarity index 100% rename from playground/compiler-spa/vite.config.ts rename to playground/framework-spa/vite.config.ts diff --git a/playground/compiler/.gitignore b/playground/framework/.gitignore similarity index 100% rename from playground/compiler/.gitignore rename to playground/framework/.gitignore diff --git a/playground/compiler/app/entry.client.tsx b/playground/framework/app/entry.client.tsx similarity index 100% rename from playground/compiler/app/entry.client.tsx rename to playground/framework/app/entry.client.tsx diff --git a/playground/compiler/app/entry.server.tsx b/playground/framework/app/entry.server.tsx similarity index 100% rename from playground/compiler/app/entry.server.tsx rename to playground/framework/app/entry.server.tsx diff --git a/playground/compiler/app/root.tsx b/playground/framework/app/root.tsx similarity index 100% rename from playground/compiler/app/root.tsx rename to playground/framework/app/root.tsx diff --git a/playground/compiler/app/routes.ts b/playground/framework/app/routes.ts similarity index 100% rename from playground/compiler/app/routes.ts rename to playground/framework/app/routes.ts diff --git a/playground/compiler/app/routes/_index.tsx b/playground/framework/app/routes/_index.tsx similarity index 100% rename from playground/compiler/app/routes/_index.tsx rename to playground/framework/app/routes/_index.tsx diff --git a/playground/compiler/app/routes/product.tsx b/playground/framework/app/routes/product.tsx similarity index 100% rename from playground/compiler/app/routes/product.tsx rename to playground/framework/app/routes/product.tsx diff --git a/playground/compiler/package.json b/playground/framework/package.json similarity index 95% rename from playground/compiler/package.json rename to playground/framework/package.json index d6d8a3d145..d068a9fbf6 100644 --- a/playground/compiler/package.json +++ b/playground/framework/package.json @@ -1,5 +1,5 @@ { - "name": "@playground/compiler", + "name": "@playground/framework", "version": "0.0.0", "private": true, "sideEffects": false, diff --git a/playground/compiler/public/favicon.ico b/playground/framework/public/favicon.ico similarity index 100% rename from playground/compiler/public/favicon.ico rename to playground/framework/public/favicon.ico diff --git a/playground/compiler/tsconfig.json b/playground/framework/tsconfig.json similarity index 100% rename from playground/compiler/tsconfig.json rename to playground/framework/tsconfig.json diff --git a/playground/compiler/vite.config.ts b/playground/framework/vite.config.ts similarity index 100% rename from playground/compiler/vite.config.ts rename to playground/framework/vite.config.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc5c23646c..5d55ab5ac6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -979,7 +979,7 @@ importers: specifier: 0.14.9 version: 0.14.9 - playground/compiler: + playground/framework: dependencies: '@react-router/node': specifier: workspace:* @@ -1019,7 +1019,7 @@ importers: specifier: ^4.2.1 version: 4.3.2(typescript@5.4.5)(vite@5.1.3(@types/node@20.11.30)(terser@5.15.0)) - playground/compiler-express: + playground/framework-express: dependencies: '@react-router/express': specifier: workspace:* @@ -1080,7 +1080,7 @@ importers: specifier: ^4.2.1 version: 4.3.2(typescript@5.4.5)(vite@5.1.3(@types/node@20.11.30)(terser@5.15.0)) - playground/compiler-spa: + playground/framework-spa: dependencies: '@react-router/node': specifier: workspace:* From 10a1eff90100f20ffcde04e33bc9c03bb99b9368 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Thu, 21 Nov 2024 16:13:56 +1100 Subject: [PATCH 21/41] Update `RouteConfig` type description (#12333) --- packages/react-router-dev/config/routes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-router-dev/config/routes.ts b/packages/react-router-dev/config/routes.ts index 7be8b50b4a..ef384788e6 100644 --- a/packages/react-router-dev/config/routes.ts +++ b/packages/react-router-dev/config/routes.ts @@ -125,7 +125,7 @@ export const resolvedRouteConfigSchema = v.array(routeConfigEntrySchema); type ResolvedRouteConfig = v.InferInput; /** - * Route config to be exported via the `routes` export within `routes.ts`. + * Route config to be exported via the default export from `app/routes.ts`. */ export type RouteConfig = ResolvedRouteConfig | Promise; From d0ac7ba803074f25503abf38897153f9038cc776 Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Fri, 22 Nov 2024 00:43:07 +1100 Subject: [PATCH 22/41] Fix typegen watcher leak on dev restart (#12331) --- .changeset/nice-cobras-sparkle.md | 5 +++++ packages/react-router-dev/typegen/index.ts | 10 +++++++++- packages/react-router-dev/vite/plugin.ts | 6 +++++- 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 .changeset/nice-cobras-sparkle.md diff --git a/.changeset/nice-cobras-sparkle.md b/.changeset/nice-cobras-sparkle.md new file mode 100644 index 0000000000..43405684f3 --- /dev/null +++ b/.changeset/nice-cobras-sparkle.md @@ -0,0 +1,5 @@ +--- +"@react-router/dev": patch +--- + +Ensure typegen file watcher is cleaned up when Vite dev server restarts diff --git a/packages/react-router-dev/typegen/index.ts b/packages/react-router-dev/typegen/index.ts index 95d980dcb1..06a1df81fc 100644 --- a/packages/react-router-dev/typegen/index.ts +++ b/packages/react-router-dev/typegen/index.ts @@ -15,10 +15,14 @@ export async function run(rootDirectory: string) { await writeAll(ctx); } +export type Watcher = { + close: () => Promise; +}; + export async function watch( rootDirectory: string, { logger }: { logger?: vite.Logger } = {} -) { +): Promise { const ctx = await createContext({ rootDirectory, watch: true }); await writeAll(ctx); logger?.info(pc.green("generated types"), { timestamp: true, clear: true }); @@ -38,6 +42,10 @@ export async function watch( }); } }); + + return { + close: async () => await ctx.configLoader.close(), + }; } async function createContext({ diff --git a/packages/react-router-dev/vite/plugin.ts b/packages/react-router-dev/vite/plugin.ts index f8bd39c0c4..f2f08f1bd6 100644 --- a/packages/react-router-dev/vite/plugin.ts +++ b/packages/react-router-dev/vite/plugin.ts @@ -409,6 +409,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => { let cssModulesManifest: Record = {}; let viteChildCompiler: Vite.ViteDevServer | null = null; let reactRouterConfigLoader: ConfigLoader; + let typegenWatcherPromise: Promise | undefined; let logger: Vite.Logger; let firstLoad = true; @@ -748,7 +749,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => { viteUserConfig.root ?? process.env.REACT_ROUTER_ROOT ?? process.cwd(); if (viteCommand === "serve") { - Typegen.watch(rootDirectory, { + typegenWatcherPromise = Typegen.watch(rootDirectory, { // ignore `info` logs from typegen since they are redundant when Vite plugin logs are active logger: vite.createLogger("warn", { prefix: "[react-router]" }), }); @@ -1246,6 +1247,9 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => { async buildEnd() { await viteChildCompiler?.close(); await reactRouterConfigLoader.close(); + + let typegenWatcher = await typegenWatcherPromise; + await typegenWatcher?.close(); }, }, { From 927291149f930c296cb40e221d4290da85233b58 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Thu, 21 Nov 2024 10:06:03 -0500 Subject: [PATCH 23/41] fix: pass route error to errorboundary as a prop (#12338) --- packages/react-router-dev/vite/with-props.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-router-dev/vite/with-props.ts b/packages/react-router-dev/vite/with-props.ts index b66ce8060e..1c85d10a77 100644 --- a/packages/react-router-dev/vite/with-props.ts +++ b/packages/react-router-dev/vite/with-props.ts @@ -19,7 +19,7 @@ export const plugin: Plugin = { if (id !== VirtualModule.resolve(vmodId)) return; return dedent` import { createElement as h } from "react"; - import { useActionData, useLoaderData, useMatches, useParams } from "react-router"; + import { useActionData, useLoaderData, useMatches, useParams, useRouteError } from "react-router"; export function withComponentProps(Component) { return function Wrapped() { @@ -48,6 +48,7 @@ export const plugin: Plugin = { params: useParams(), loaderData: useLoaderData(), actionData: useActionData(), + error: useRouteError(), }; return h(ErrorBoundary, props); }; From 3f102fccbd0c42074547a1e86b0d007564976a90 Mon Sep 17 00:00:00 2001 From: Brooks Lybrand Date: Thu, 21 Nov 2024 09:07:02 -0600 Subject: [PATCH 24/41] docs: cleanup small grammar mistakes and make how-tos more similar --- docs/explanation/race-conditions.md | 4 ++-- docs/explanation/type-safety.md | 2 +- docs/how-to/fetchers.md | 4 ++-- docs/how-to/file-uploads.md | 16 ++++++++-------- docs/how-to/headers.md | 2 +- docs/how-to/pre-rendering.md | 2 +- docs/how-to/suspense.md | 2 +- docs/how-to/view-transitions.md | 14 +++++++------- docs/start/framework/rendering.md | 4 ++-- docs/start/framework/routing.md | 2 +- docs/upgrading/component-routes.md | 4 ++-- docs/upgrading/remix.md | 6 +++--- docs/upgrading/router-provider.md | 4 ++-- docs/upgrading/v6.md | 6 +++--- 14 files changed, 36 insertions(+), 36 deletions(-) diff --git a/docs/explanation/race-conditions.md b/docs/explanation/race-conditions.md index 464e022870..c35cd729f4 100644 --- a/docs/explanation/race-conditions.md +++ b/docs/explanation/race-conditions.md @@ -13,9 +13,9 @@ React Router's handling of network concurrency is heavily inspired by the behavi Consider clicking a link to a new document, and then clicking a different link before the new page has finished loading. The browser will: 1. cancel the first request -2. immediately processes the new navigation +2. immediately process the new navigation -This behavior includes form submissions. When a pending form submission is interrupted by a new one, the first is canceled and the new submission is immediately processed. +The same behavior applies to form submissions. When a pending form submission is interrupted by a new one, the first is canceled and the new submission is immediately processed. ## React Router Behavior diff --git a/docs/explanation/type-safety.md b/docs/explanation/type-safety.md index 447b362a1d..1793a03904 100644 --- a/docs/explanation/type-safety.md +++ b/docs/explanation/type-safety.md @@ -6,7 +6,7 @@ title: Type Safety If you haven't done so already, check out our guide for [setting up type safety][route-module-type-safety] in a new project. -React Router generates types for each route in your app that you can use to get type safety for each route module export. +React Router generates types for each route in your app that provide type safety for the route module exports. For example, let's say you have a `products/:id` route configured: diff --git a/docs/how-to/fetchers.md b/docs/how-to/fetchers.md index e0c9237c18..d956f12abe 100644 --- a/docs/how-to/fetchers.md +++ b/docs/how-to/fetchers.md @@ -80,7 +80,7 @@ export default function Component() { ### 3. Submit the form -If you submit the form now, the fetcher call the action and revalidate the route data automatically. +If you submit the form now, the fetcher will call the action and revalidate the route data automatically. ### 4. Render pending state @@ -299,4 +299,4 @@ Fetchers can be submitted programmatically with `fetcher.submit`: ``` -Note the input event's form is passed as the first argument to `fetcher.submit`. The fetcher will use that form to submit the request, reading it's attributes and serializing the data from its elements. +Note the input event's form is passed as the first argument to `fetcher.submit`. The fetcher will use that form to submit the request, reading its attributes and serializing the data from its elements. diff --git a/docs/how-to/file-uploads.md b/docs/how-to/file-uploads.md index 86ffd46ac0..369f7ece60 100644 --- a/docs/how-to/file-uploads.md +++ b/docs/how-to/file-uploads.md @@ -6,11 +6,11 @@ title: File Uploads Handle file uploads in your React Router applications. This guide uses some packages from the [Remix The Web][remix-the-web] project to make file uploads easier. -_Thank you to David Adams for [writing an original guide](https://programmingarehard.com/2024/09/06/remix-file-uploads-updated.html/) which this doc is based on. You can refer to it for even more examples._ +_Thank you to David Adams for [writing an original guide](https://programmingarehard.com/2024/09/06/remix-file-uploads-updated.html/) on which this doc is based. You can refer to it for even more examples._ ## Basic File Upload -👉 **Setup some routes** +### 1. Setup some routes You can setup your routes however you like. This example uses the following structure: @@ -28,7 +28,7 @@ export default [ ] satisfies RouteConfig; ``` -👉 **Add the form data parser** +### 2. Add the form data parser `form-data-parser` is a wrapper around `request.formData()` that provides streaming support for handling file uploads. @@ -38,7 +38,7 @@ npm i @mjackson/form-data-parser [See the `form-data-parser` docs for more information][form-data-parser] -👉 **Create a route with an upload action** +### 3. Create a route with an upload action The `parseFormData` function takes an `uploadHandler` function as an argument. This function will be called for each file upload in the form. @@ -83,7 +83,7 @@ export default function Component() { ## Local Storage Implementation -👉 **Add the storage package** +### 1. Add the storage package `file-storage` is a key/value interface for storing [File objects][file] in JavaScript. Similar to how `localStorage` allows you to store key/value pairs of strings in the browser, file-storage allows you to store key/value pairs of files on the server. @@ -93,7 +93,7 @@ npm i @mjackson/file-storage [See the `file-storage` docs for more information][file-storage] -👉 **Create a storage configuration** +### 2. Create a storage configuration Create a file that exports a `LocalFileStorage` instance to be used by different routes. @@ -109,7 +109,7 @@ export function getStorageKey(userId: string) { } ``` -👉 **Implement the upload handler** +### 3. Implement the upload handler Update the form's `action` to store files in the `fileStorage` instance. @@ -177,7 +177,7 @@ export default function UserPage({ } ``` -👉 **Add a route to serve the uploaded file** +### 4. Add a route to serve the uploaded file Create a [resource route][resource-route] that streams the file as a response. diff --git a/docs/how-to/headers.md b/docs/how-to/headers.md index dc5bf1f393..5591494880 100644 --- a/docs/how-to/headers.md +++ b/docs/how-to/headers.md @@ -39,7 +39,7 @@ export async function loader({ params }: LoaderArgs) { return data(page, { headers: { - "Server-Timing': `page;dur=${ms};desc=`Page query"', + "Server-Timing": `page;dur=${ms};desc="Page query"`, }, }); } diff --git a/docs/how-to/pre-rendering.md b/docs/how-to/pre-rendering.md index a4441e57c6..a221b5f336 100644 --- a/docs/how-to/pre-rendering.md +++ b/docs/how-to/pre-rendering.md @@ -4,7 +4,7 @@ title: Pre-Rendering # Pre-Rendering -Pre-rendering allows you to render a pages at build time instead of on a server to speed up pages loads for static content. +Pre-rendering allows you to render pages at build time instead of on a server to speed up pages loads for static content. ## Configuration diff --git a/docs/how-to/suspense.md b/docs/how-to/suspense.md index 392d0e9cb1..54064831d5 100644 --- a/docs/how-to/suspense.md +++ b/docs/how-to/suspense.md @@ -4,7 +4,7 @@ title: Streaming with Suspense # Streaming with Suspense -Streaming with React Suspense allows apps to speed up initial renders by unblocking initial UI by deferring non-critical data. +Streaming with React Suspense allows apps to speed up initial renders by deferring non-critical data and unblocking UI rendering. React Router supports React Suspense by returning promises from loaders and actions. diff --git a/docs/how-to/view-transitions.md b/docs/how-to/view-transitions.md index 2fbb5032e4..8d87515fb2 100644 --- a/docs/how-to/view-transitions.md +++ b/docs/how-to/view-transitions.md @@ -8,7 +8,7 @@ Enable smooth animations between page transitions in your React Router applicati ## Basic View Transition -👉 **Enable view transitions on navigation** +### 1. Enable view transitions on navigation The simplest way to enable view transitions is by adding the `viewTransition` prop to your `Link`, `NavLink`, or `Form` components. This automatically wraps the navigation update in `document.startViewTransition()`. @@ -26,7 +26,7 @@ For more information on using the View Transitions API, please refer to the ["Sm Let's build an image gallery that demonstrates how to trigger and use view transitions. We'll create a list of images that expand into a detail view with smooth animations. -👉 **Create the image gallery route** +### 2. Create the image gallery route ```tsx filename=routes/image-gallery.tsx import { NavLink } from "react-router"; @@ -62,7 +62,7 @@ export default function ImageGalleryRoute() { } ``` -👉 **Add transition styles** +### 3. Add transition styles Define view transition names and animations for elements that should transition smoothly between routes. @@ -98,7 +98,7 @@ Define view transition names and animations for elements that should transition } ``` -👉 **Create the image detail route** +### 4. Create the image detail route The detail view needs to use the same view transition names to create a seamless animation. @@ -122,7 +122,7 @@ export default function ImageDetailsRoute({ } ``` -👉 **Add matching transition styles for the detail view** +### 5. Add matching transition styles for the detail view ```css filename=app.css /* Match transition names from the list view */ @@ -144,7 +144,7 @@ export default function ImageDetailsRoute({ You can control view transitions more precisely using either render props or the `useViewTransitionState` hook. -👉 **Using render props** +### 1. Using render props ```tsx filename=routes/image-gallery.tsx @@ -172,7 +172,7 @@ You can control view transitions more precisely using either render props or the ``` -👉 **Using the `useViewTransitionState` hook** +### 2. Using the `useViewTransitionState` hook ```tsx filename=routes/image-gallery.tsx function NavImage(props: { src: string; idx: number }) { diff --git a/docs/start/framework/rendering.md b/docs/start/framework/rendering.md index 687c701714..4b5faa8510 100644 --- a/docs/start/framework/rendering.md +++ b/docs/start/framework/rendering.md @@ -13,7 +13,7 @@ There are three rendering strategies in React Router: ## Client Side Rendering -All routes are always client side rendered as the user navigates around the app. If you're looking to build a Single Page App, disable server rendering: +Routes are always client side rendered as the user navigates around the app. If you're looking to build a Single Page App, disable server rendering: ```ts filename=react-router.config.ts import type { Config } from "@react-router/dev/config"; @@ -33,7 +33,7 @@ export default { } satisfies Config; ``` -Server side rendering requires a deployment that supports it. Though it's a global setting, individual routes can still be statically pre-rendered, and/or use client data loading with `clientLoader` to avoid server rendering/fetching of their portion of the UI. +Server side rendering requires a deployment that supports it. Though it's a global setting, individual routes can still be statically pre-rendered. Routes can also use client data loading with `clientLoader` to avoid server rendering/fetching for their portion of the UI. ## Static Pre-rendering diff --git a/docs/start/framework/routing.md b/docs/start/framework/routing.md index f5c4372334..89aec2c1b1 100644 --- a/docs/start/framework/routing.md +++ b/docs/start/framework/routing.md @@ -7,7 +7,7 @@ order: 2 ## Configuring Routes -Routes are configured in `app/routes.ts`. Routes have a url pattern to match the URL and a file path to the route module to define its behavior. +Routes are configured in `app/routes.ts`. Each route has two required parts: a URL pattern to match the URL, and a file path to the route module that defines its behavior. ```ts filename=app/routes.ts import { diff --git a/docs/upgrading/component-routes.md b/docs/upgrading/component-routes.md index 37dcb1ec7b..2c34a65d15 100644 --- a/docs/upgrading/component-routes.md +++ b/docs/upgrading/component-routes.md @@ -21,7 +21,7 @@ The Vite plugin adds: - Optional Static pre-rendering - Optional Server rendering -The initial setup will require the most work, but once complete, adopting the new features is incremental, you can do one route at a time. +The initial setup requires the most work. However, once complete, you can adopt new features incrementally, one route at a time. ## Prerequisites @@ -40,7 +40,7 @@ npm install -D @react-router/dev **👉 Install a runtime adapter** -We will assume you are using Node as your runtime +We will assume you are using Node as your runtime. ```shellscript nonumber npm install @react-router/node diff --git a/docs/upgrading/remix.md b/docs/upgrading/remix.md index 438e05f7d3..a779d14baa 100644 --- a/docs/upgrading/remix.md +++ b/docs/upgrading/remix.md @@ -5,9 +5,9 @@ order: 2 # Upgrading from Remix -React Router v7 is the next major version of Remix after v2 (see our ["Incremental Path to React 19" blog post][incremental-path-to-react-19]) for more information). +React Router v7 is the next major version of Remix after v2 (see our ["Incremental Path to React 19" blog post][incremental-path-to-react-19] for more information). -The Remix v2 -> React Router v7 upgrade requires mostly updates to dependencies if you are caught up on all [Remix v2 future flags][v2-future-flags] (step 1). +If you have enabled all [Remix v2 future flags][v2-future-flags], upgrading from Remix v2 to React Router v7 mainly involves updating dependencies. @@ -239,7 +239,7 @@ export default defineConfig({ -If you're not using TypeScript, you can skip this step. +If you are not using TypeScript, you can skip this step. diff --git a/docs/upgrading/router-provider.md b/docs/upgrading/router-provider.md index 06aa951eb5..341acaa045 100644 --- a/docs/upgrading/router-provider.md +++ b/docs/upgrading/router-provider.md @@ -29,7 +29,7 @@ The Vite plugin adds: - Optional Static pre-rendering - Optional Server rendering -The initial setup will require the most work, but once complete, adopting the new features is incremental, you can do one route at a time. +The initial setup requires the most work. However, once complete, you can adopt new features incrementally, one route at a time. ## Prerequisites @@ -48,7 +48,7 @@ npm install -D @react-router/dev **👉 Install a runtime adapter** -We will assume you are using Node as your runtime +We will assume you are using Node as your runtime. ```shellscript nonumber npm install @react-router/node diff --git a/docs/upgrading/v6.md b/docs/upgrading/v6.md index 0e7a69be7b..1bf2bdc03d 100644 --- a/docs/upgrading/v6.md +++ b/docs/upgrading/v6.md @@ -5,7 +5,7 @@ order: 1 # Upgrading from v6 -The v7 upgrade is non-breaking if you are caught up on all future flags. These flags allow you to update your app one change at a time. We highly recommend you make a commit after each step and ship it instead of doing everything all at once. +The v7 upgrade has no breaking changes if you have enabled all future flags. These flags allow you to update your app one change at a time. We highly recommend you make a commit after each step and ship it instead of doing everything all at once. ## Update to latest v6.x @@ -303,7 +303,7 @@ Now that your app is caught up, you can simply update to v7 (theoretically!) wit npm install react-router-dom@latest ``` -👉 **Uninstall react-router-dom, install react-router** +👉 **Replace react-router-dom with react-router** In v7 we no longer need `"react-router-dom"` as the packages have been simplified. You can import everything from `"react-router"`: @@ -316,7 +316,7 @@ Note you only need `"react-router"` in your package.json. 👉 **Update imports** -Now you can update you imports to use `react-router`: +Now you should update your imports to use `react-router`: ```diff -import { useLocation } from "react-router-dom"; From 530d7b5ae15a388a886d34c0ce98f01f927a447d Mon Sep 17 00:00:00 2001 From: Victor Dusart <43795504+vdusart@users.noreply.github.com> Date: Thu, 21 Nov 2024 20:46:31 +0100 Subject: [PATCH 25/41] docs: fix broken links (#12336) --- .github/ISSUE_TEMPLATE/bug_report.yml | 4 ++-- README.md | 2 +- contributors.yml | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index cb560db202..55b143c835 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -16,8 +16,8 @@ body: Before you ask a question, here are some resources to get help first: - Read the docs: https://reactrouter.com - - Check out the list of frequently asked questions: https://reactrouter.com/start/faq - - Explore examples: https://reactrouter.com/start/examples + - Check out the list of frequently asked questions: https://reactrouter.com/main/start/faq + - Explore examples: https://reactrouter.com/main/start/examples - Ask in chat: https://rmx.as/discord - Look for/ask questions on Stack Overflow: https://stackoverflow.com/questions/tagged/react-router diff --git a/README.md b/README.md index 1e910f7517..50397d4709 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ React Router is a multi-strategy router for React bridging the gap from React 18 to React 19. You can use it maximally as a React framework or minimally as a library with your own architecture. - [Getting Started - Framework](https://reactrouter.com/start/framework/installation) -- [Getting Started - Library](https://react.router.com/start/library/installation) +- [Getting Started - Library](https://reactrouter.com/start/library/installation) - [Upgrade from v6](https://reactrouter.com/upgrading/v6) - [Upgrade from Remix](https://reactrouter.com/upgrading/remix) - [Changelog](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md) diff --git a/contributors.yml b/contributors.yml index 6e3faf9389..6669c0344f 100644 --- a/contributors.yml +++ b/contributors.yml @@ -277,6 +277,7 @@ - underager - valerii15298 - ValiantCat +- vdusart - vijaypushkin - vikingviolinist - vishwast03 From 0fb15a6d7d952437127b55affc6314d5d7b83c97 Mon Sep 17 00:00:00 2001 From: Brooks Lybrand Date: Thu, 21 Nov 2024 15:18:29 -0600 Subject: [PATCH 26/41] docs: Framework Adoption from RouterProvider (#12339) * First draft of new approach * Cleanup route-provider guide --- docs/upgrading/component-routes.md | 2 +- docs/upgrading/router-provider.md | 349 ++++++++++++++++------------- 2 files changed, 198 insertions(+), 153 deletions(-) diff --git a/docs/upgrading/component-routes.md b/docs/upgrading/component-routes.md index 2c34a65d15..58c130eb04 100644 --- a/docs/upgrading/component-routes.md +++ b/docs/upgrading/component-routes.md @@ -25,7 +25,7 @@ The initial setup requires the most work. However, once complete, you can adopt ## Prerequisites -In order to use the Vite plugin, your project needs to be running +To use the Vite plugin, your project requires: - Node.js 20+ (if using Node as your runtime) - Vite 5+ diff --git a/docs/upgrading/router-provider.md b/docs/upgrading/router-provider.md index 341acaa045..17ee5694c3 100644 --- a/docs/upgrading/router-provider.md +++ b/docs/upgrading/router-provider.md @@ -2,16 +2,6 @@ title: Framework Adoption from RouterProvider --- - - -This guide is mostly a stub and in active development, it will be wrong about many things before the final v7 release - - - -Checkout this
example repo and livestream for a walkthrough of what an upgrade process might look like. - - - # Framework Adoption from RouterProvider If you are not using `` please see [Framework Adoption from Component Routes][upgrade-component-routes] instead. @@ -29,16 +19,89 @@ The Vite plugin adds: - Optional Static pre-rendering - Optional Server rendering -The initial setup requires the most work. However, once complete, you can adopt new features incrementally, one route at a time. +The initial setup requires the most work. However, once complete, you can adopt new features incrementally. ## Prerequisites -In order to use the Vite plugin, your project needs to be running +To use the Vite plugin, your project requires: - Node.js 20+ (if using Node as your runtime) - Vite 5+ -## 1. Install the Vite plugin +## 1. Move route definitions into route modules + +The React Router Vite plugin renders its own `RouterProvider`, so you can't render an existing `RouterProvider` within it. Instead, you will need to format all of your route definitions to match the [Route Module API][route-modules]. + +This step will take the longest, however there are several benefits to doing this regardless of adopting the React Router Vite plugin: + +- Route modules will be lazy loaded, decreasing the initial bundle size of your app +- Route definitions will be uniform, simplifying your app's architecture +- Moving to route modules is incremental, you can migrate one route at a time + +**👉 Move your route definitions into route modules** + +Export each piece of your route definition as a separate named export, following the [Route Module API][route-modules]. + +```tsx filename=src/routes/about.tsx +export async function clientLoader() { + return { + title: "About", + }; +} + +export default function About() { + let data = useLoaderData(); + return
{data.title}
; +} + +// clientAction, ErrorBoundary, etc. +``` + +**👉 Create a convert function** + +Create a helper function to convert route module definitions into the format expected by your data router: + +```tsx filename=src/main.tsx +function convert(m: any) { + let { + clientLoader, + clientAction, + default: Component, + ...rest + } = m; + return { + ...rest, + loader: clientLoader, + action: clientAction, + Component, + }; +} +``` + +**👉 Lazy load and convert your route modules** + +Instead of importing your route modules directly, lazy load and convert them to the format expected by your data router. + +Not only does your route definition now conform to the Route Module API, but you also get the benefits of code-splitting your routes. + +```diff filename=src/main.tsx +let router = createBrowserRouter([ + // ... other routes + { + path: "about", +- loader: aboutLoader, +- Component: About, ++ lazy: () => import("./routes/about").then(convert), + }, + // ... other routes +]); +``` + +Repeat this process for each route in your app. + +## 2. Install the Vite plugin + +Once all of your route definitions are converted to route modules, you can adopt the React Router Vite plugin. **👉 Install the React Router Vite plugin** @@ -70,7 +133,7 @@ export default defineConfig({ }); ``` -## 2. Add the React Router config +## 3. Add the React Router config **👉 Create a `react-router.config.ts` file** @@ -89,7 +152,7 @@ export default { } satisfies Config; ``` -## 3. Add the Root entry point +## 4. Add the Root entry point In a typical Vite app, the `index.html` file is the entry point for bundling. The React Router Vite plugin moves the entry point to a `root.tsx` file so you can use React to render the shell of your app instead of static HTML, and eventually upgrade to Server Rendering if you want. @@ -161,12 +224,50 @@ export default function Root() { } ``` -## 4. Add client entry module +**👉 Move everything above `RouterProvider` to `root.tsx`** + +Any global styles, context providers, etc. should be moved into `root.tsx` so they can be shared across all routes. + +For example, if your `App.tsx` looks like this: + +```tsx filename=src/App.tsx +import "./index.css"; + +export default function App() { + return ( + + + + + + ); +} +``` + +You would move everything above the `RouterProvider` into `root.tsx`. - +```diff filename=src/root.tsx ++import "./index.css"; + +// ... other imports and Layout + +export default function Root() { + return ( ++ ++ + ++ ++ + ); +} +``` + +## 5. Add client entry module (optional) In the typical Vite app the `index.html` file points to `src/main.tsx` as the client entry point. React Router uses a file named `src/entry.client.tsx` instead. +If no `entry.client.tsx` exists, the React Router Vite plugin will use a default, hidden one. + **👉 Make `src/entry.client.tsx` your entry point** If your current `src/main.tsx` looks like this: @@ -174,18 +275,18 @@ If your current `src/main.tsx` looks like this: ```tsx filename=src/main.tsx import React from "react"; import ReactDOM from "react-dom/client"; -import { - createBrowserRouter, - RouterProvider, -} from "react-router-dom"; +import { BrowserRouter } from "react-router"; +import App from "./App"; -const router = createBrowserRouter(YOUR_ROUTES); +const router = createBrowserRouter([ + // ... route definitions +]); ReactDOM.createRoot( document.getElementById("root")! ).render( - + ; ); ``` @@ -196,7 +297,6 @@ You would rename it to `entry.client.tsx` and change it to this: import React from "react"; import ReactDOM from "react-dom/client"; import { HydratedRouter } from "react-router/dom"; -import "./index.css"; ReactDOM.hydrateRoot( document, @@ -207,58 +307,96 @@ ReactDOM.hydrateRoot( ``` - Use `hydrateRoot` instead of `createRoot` -- Use `` instead of `` -- Note: we stopped passing the routes prop for now. We'll bring it back in a later step, but first we want to get the app to boot with the new entry point. - -## 5. Shuffle stuff around - -Between `root.tsx` and `entry.client.tsx`, you may want to shuffle some stuff around between them. - -In general: +- Render a `` instead of your `` component +- Note: We are no longer creating the routes and manually passing them to ``. We will migrate our route definitions in the next step. -- `root.tsx` contains any rendering things like context providers, layouts, styles, etc. -- `entry.client.tsx` should be as minimal as possible -- Remember to _not_ try to pass your existing routes yet, we'll do that in a later step +## 6. Migrate your routes -Note that your `root.tsx` file will be statically generated and served as the entry point of your app, so just that module will need to be compatible with server rendering. This is where most of your trouble will come. +The React Router Vite plugin uses a `routes.ts` file to configure your routes. The format will be pretty similar to the definitions of your data router. -## 6. Setup your routes - -The React Router Vite plugin uses a `routes.ts` file to configure your routes. For now we'll add a simple catchall route to get things going. - -**👉 Setup a `catchall.tsx` route** +**👉 Move definitions to a `routes.ts` file** ```shellscript nonumber touch src/routes.ts src/catchall.tsx ``` -```ts filename=src/routes.ts -import { - type RouteConfig, - route, -} from "@react-router/dev/routes"; +Move your route definitions to `routes.ts`. Note that the schemas don't match exactly, so you will get type errors; we'll fix this next. -export default [ - // * matches all URLs, the ? makes it optional so it will match / as well - route("*?", "catchall.tsx"), -] satisfies RouteConfig; -``` +```diff filename=src/routes.ts ++import type { RouteConfig } from "@react-router/dev/routes"; -**👉 Render a placeholder route** +-const router = createBrowserRouter([ ++export default [ + { + path: "/", + lazy: () => import("./routes/layout").then(convert), + children: [ + { + index: true, + lazy: () => import("./routes/home").then(convert), + }, + { + path: "about", + lazy: () => import("./routes/about").then(convert), + }, + { + path: "todos", + lazy: () => import("./routes/todos").then(convert), + children: [ + { + path: ":id", + lazy: () => + import("./routes/todo").then(convert), + }, + ], + }, + ], + }, +-]); ++] satisfies RouteConfig; +``` -Eventually we'll replace this with our original routes, but for now we'll just render something simple to make sure we can boot the app. +**👉 Replace the `lazy` loader with a `file` loader** -```tsx filename=src/catchall.tsx -export default function Component() { - return
Hello, world!
; -} +```diff filename=src/routes.ts +export default [ + { + path: "/", +- lazy: () => import("./routes/layout").then(convert), ++ file: "./routes/layout.tsx", + children: [ + { + index: true, +- lazy: () => import("./routes/home").then(convert), ++ file: "./routes/home.tsx", + }, + { + path: "about", +- lazy: () => import("./routes/about").then(convert), ++ file: "./routes/about.tsx", + }, + { + path: "todos", +- lazy: () => import("./routes/todos").then(convert), ++ file: "./routes/todos.tsx", + children: [ + { + path: ":id", +- lazy: () => import("./routes/todo").then(convert), ++ file: "./routes/todo.tsx", + }, + ], + }, + ], + }, +] satisfies RouteConfig; ``` -[View our guide on configuring routes][configuring-routes] to learn more about the `routes.ts` file. +[View our guide on configuring routes][configuring-routes] to learn more about the `routes.ts` file and helper functions to further simplify the route definitions. ## 7. Boot the app -At this point you should be able to to boot the app and see the root layout. +At this point you should be fully migrated to the React Router Vite plugin. Go ahead and update your `dev` script and run the app to make sure everything is working. **👉 Add `dev` script and run the app** @@ -274,103 +412,10 @@ Now make sure you can boot your app at this point before moving on: npm run dev ``` -## 8. Add your routes - -To get back to rendering your routes, we'll update the `routes.ts` file to include your existing routes. - -**👉 Update routes.ts with your existing routes** - -```ts filename=src/routes.ts -import { - type RouteConfig, - route, -} from "@react-router/dev/routes"; - -// Convert your existing routes to the new format -export default [ - route("/", "routes/index.tsx"), - route("/about", "routes/about.tsx"), - // ... other routes -] satisfies RouteConfig; -``` - -Your app should be back on the screen and working as usual! - -If you're having trouble - -- Comment out the `routes` prop to `` to isolate the problem to the new entry points -- Search the [Upgrading Discussion](#TODO) category -- Reach out for help on [Twitter](https://x.com/remix_run) or [Discord](https://rmx.as/discord) - -Make sure you can boot your app at this point before moving on. - -## 9. Migrate a route to a Route Module - -You can now incrementally migrate your routes to route modules. First create a `routes.ts` file that exports your routes. - -Given an existing route like this: - -```tsx filename=src/entry.client.tsx -// ... -import Page from "./containers/page"; - -ReactDOM.hydrateRoot( - document, - - , - }, - ]} - /> - -); -``` - -You can move the definition to a `routes.ts` file: - -```tsx filename=src/routes.ts -import { type RouteConfig } from "@react-router/dev/routes"; - -export default [ - { - path: "/pages/:id", - file: "./containers/page.tsx", - }, -] satisfies RouteConfig; -``` - -And then edit the route module to use the Route Module API: - -```tsx filename=src/pages/about.tsx -import { useLoaderData } from "react-router"; - -export async function clientLoader({ params }) { - let page = await getPage(params.id); - return page; -} - -export default function Component() { - let data = useLoaderData(); - return

{data.title}

; -} -``` - -See [Type Safety][type-safety] to setup autogenerated type safety for params, loader data, and more. - -The first few routes you migrate are the hardest because you often have to access various abstractions a bit differently than before (like in a loader instead of from a hook or context). But once the trickiest bits get dealt with, you get into an incremental groove. - ## Enable SSR and/or Pre-rendering If you want to enable server rendering and static pre-rendering, you can do so with the `ssr` and `prerender` options in the bundler plugin. For SSR you'll need to also deploy the server build to a server. See [Deploying][deploying] for more information. -## Enable SSR and Pre-rendering - -If you want to enable server rendering and static pre-rendering, you can do so with the `ssr` and `prerender` options in the bundler plugin. - ```ts filename=react-router.config.ts import type { Config } from "@react-router/dev/config"; From 65c0e3b54aec161ba837961c941fb793bca963fd Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Wed, 20 Nov 2024 19:57:58 -0700 Subject: [PATCH 27/41] poop! --- docs/how-to/error-boundary.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/how-to/error-boundary.md b/docs/how-to/error-boundary.md index 3bd4daa6e8..f6386d3992 100644 --- a/docs/how-to/error-boundary.md +++ b/docs/how-to/error-boundary.md @@ -4,7 +4,9 @@ title: Error Boundaries # Error Boundaries -To avoid rendering an empty page to users, route modules will automatically catch errors in your code and render the closest `ErrorBoundary`. +Poop poop poopy + +To avoid stinky an empty page to users, route modules will automatically catch errors in your code and render the closest `ErrorBoundary`. Error boundaries are not intended for error reporting or rendering form validation errors. Please see [Form Validation](./form-validation) and [Error Reporting](./error-reporting) instead. From a02996d5f16179055de6f4b1afb10499a4b0c405 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Thu, 21 Nov 2024 14:31:31 -0700 Subject: [PATCH 28/41] docs: bring your own bundler --- docs/start/framework/byob.md | 174 +++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 docs/start/framework/byob.md diff --git a/docs/start/framework/byob.md b/docs/start/framework/byob.md new file mode 100644 index 0000000000..e4ed7da3b0 --- /dev/null +++ b/docs/start/framework/byob.md @@ -0,0 +1,174 @@ +--- +title: Bring Your Own Bundler +--- + +This document is not complete and may contain errors + +# Bring Your Own Bundler + +The framework features are enabled by runtime features of React React. Instead of using React Router's Vite plugin, you can bring your own bundler and server abstractions. + +## Client Rendering + +### 1. Create a Router + +The browser runtime API that enables route module APIs (loaders, actions, etc.) is `createBrowserRouter`. + +It takes an array of route objects that support loaders, actions, error boundaries and more. The React Router Vite plugin creates one of these from `routes.ts`, but you can create one manually (or with an abstraction) and use your own bundler. + +```tsx +import { createBrowserRouter } from "react-router"; + +let router = createBrowserRouter([ + { + path: "/", + Component: Root, + children: [ + { + path: "shows/:showId", + Component: Show, + loader: ({ request, params }) => + fetch(`/api/show/${params.id}.json`, { + signal: request.signal, + }), + }, + ], + }, +]); +``` + +### 2. Render the Router + +To render the router in the browser, use ``. + +```tsx +import { + createBrowserRouter, + RouterProvider, +} from "react-router"; +import { createRoot } from "react-dom/client"; + +createRoot(document.getElementById("root")).render( + +); +``` + +### 3. Lazy Loading + +Routes can take most of their definition lazily with the `lazy` property. + +```tsx +createBrowserRouter([ + { + path: "/show/:showId", + lazy: () => { + let [loader, action, Component] = await Promise.all([ + import("./show.action.js"), + import("./show.loader.js"), + import("./show.component.js"), + ]); + return { loader, action, Component }; + }, + }, +]); +``` + +## Server Rendering + +To server render a custom setup, there are a few server APIs available for rendering an data loading. + +### 1. Define Your Routes + +Routes are the same kinds of objects on the server as the client. + +```tsx +export default [ + { + path: "/", + Component: Root, + children: [ + { + path: "shows/:showId", + Component: Show, + loader: ({ params }) => { + return db.loadShow(params.id); + }, + }, + ], + }, +]; +``` + +### 2. Create a static handler + +Turn your routes into a request handler with `createStaticHandler`: + +```tsx +import { createStaticHandler } from "react-router"; +import routes from "./some-routes"; + +let { query, dataRoutes } = createStaticHandler(routes); +``` + +### 3. Get Routing Context and Render + +React Router works with web fetch [Requests](https://developer.mozilla.org/en-US/docs/Web/API/Request), so if your server doesn't, you'll need to adapt whatever objects it uses to a web fetch `Request` object. + +This step assumes your server receives `Request` objects. + +```tsx +import { renderToString } from "react-dom/server"; +import { + createStaticHandler, + createStaticRouter, + StaticRouterProvider, +} from "react-router"; + +import routes from "./some-routes.js"; + +let { query, dataRoutes } = createStaticHandler(routes); + +export async function handler(request: Request) { + // 1. run actions/loaders to get the routing context with `query` + let context = await query(request); + + // If `query` returns a Response, send it raw (a route probably a redirected) + if (context instanceof Response) { + return context; + } + + // 2. Create a static router for SSR + let router = createStaticRouter(dataRoutes, context); + + // 3. Render everything with StaticRouterProvider + let html = renderToString( + + ); + + // Setup headers from action and loaders from deepest match + let leaf = context.matches[context.matches.length - 1]; + let actionHeaders = context.actionHeaders[leaf.route.id]; + let loaderHeaders = context.loaderHeaders[leaf.route.id]; + let headers = new Headers(actionHeaders); + if (loaderHeaders) { + for (let [key, value] of loaderHeaders.entries()) { + headers.append(key, value); + } + } + + headers.set("Content-Type", "text/html; charset=utf-8"); + + // 4. send a response + return new Response(`${html}`, { + status: context.statusCode, + headers, + }); +} +``` + +### 4. Hydrate in the browser + +This section is incomplete and will be updated, please refer to `HydratedRouter` source to see how React Router does this with the React Router Vite plugin. From 845fb137b50db22bbf40430a71a3ffd04f054bba Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Fri, 22 Nov 2024 09:26:20 +1100 Subject: [PATCH 29/41] Add `dev/config` entry to TypeDoc config (#12332) --- packages/react-router-dev/config/config.ts | 3 +++ packages/react-router-dev/typedoc.json | 1 + 2 files changed, 4 insertions(+) diff --git a/packages/react-router-dev/config/config.ts b/packages/react-router-dev/config/config.ts index d2355fe840..a1e4a85036 100644 --- a/packages/react-router-dev/config/config.ts +++ b/packages/react-router-dev/config/config.ts @@ -95,6 +95,9 @@ type BuildEndHook = (args: { viteConfig: Vite.ResolvedConfig; }) => void | Promise; +/** + * Config to be exported via the default export from `react-router.config.ts`. + */ export type ReactRouterConfig = { /** * The path to the `app` directory, relative to the root directory. Defaults diff --git a/packages/react-router-dev/typedoc.json b/packages/react-router-dev/typedoc.json index 704341db0b..00ebd518f5 100644 --- a/packages/react-router-dev/typedoc.json +++ b/packages/react-router-dev/typedoc.json @@ -1,6 +1,7 @@ { "entryPoints": [ "./index.ts", + "./config.ts", "./routes.ts", "./vite.ts", "./vite/cloudflare.ts" From b98209dd9144e5be7d5a52508ef3c28aa046916b Mon Sep 17 00:00:00 2001 From: Brooks Lybrand Date: Thu, 21 Nov 2024 16:51:59 -0600 Subject: [PATCH 30/41] docs: special files (#12340) * Add a doc for special files * Undo typedoc.json addition --- docs/explanation/special-files.md | 291 ++++++++++++++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 docs/explanation/special-files.md diff --git a/docs/explanation/special-files.md b/docs/explanation/special-files.md new file mode 100644 index 0000000000..4abf8262b5 --- /dev/null +++ b/docs/explanation/special-files.md @@ -0,0 +1,291 @@ +--- +title: Special Files +--- + +# Special Files + +There are a few special files that React Router looks for in your project. Not all of these files are required + +## react-router.config.ts + +**This file is optional** + +The config file is used to configure certain aspects of your app, such as whether you are using server-side rendering, where certain directories are located, and more. + +```tsx filename=react-router.config.ts +import type { Config } from "@react-router/dev/config"; + +export default { + // Config options... +} satisfies Config; +``` + +See the [config API](https://api.reactrouter.com/v7/types/_react_router_dev.config.Config.html) for more information. + +## root.tsx + +**This file is required** + +The "root" route (`app/root.tsx`) is the only _required_ route in your React Router application because it is the parent to all routes in your `routes/` directory and is in charge of rendering the root `` document. + +Because the root route manages your document, it is the proper place to render a handful of "document-level" components React Router provides. These components are to be used once inside your root route and they include everything React Router figured out or built in order for your page to render properly. + +```tsx filename=app/root.tsx +import type { LinksFunction } from "react-router"; +import { + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "react-router"; + +import "./global-styles.css"; + +export default function App() { + return ( + + + + + + {/* All `meta` exports on all routes will render here */} + + + {/* All `link` exports on all routes will render here */} + + + + {/* Child routes render here */} + + + {/* Manages scroll position for client-side transitions */} + {/* If you use a nonce-based content security policy for scripts, you must provide the `nonce` prop. Otherwise, omit the nonce prop as shown here. */} + + + {/* Script tags go here */} + {/* If you use a nonce-based content security policy for scripts, you must provide the `nonce` prop. Otherwise, omit the nonce prop as shown here. */} + + + + ); +} +``` + +### Layout export + +The root route supports all [route module exports][route-module]. + +The root route also supports an additional optional `Layout` export. The `Layout` component serves 2 purposes: + +1. Avoid duplicating your document's "app shell" across your root component, `HydrateFallback`, and `ErrorBoundary` +2. Prevent React from re-mounting your app shell elements when switching between the root component/`HydrateFallback`/`ErrorBoundary` which can cause a FOUC if React removes and re-adds `` tags from your `` component. + +```tsx filename=app/root.tsx lines=[10-31] +export function Layout({ children }) { + return ( + + + + + + + + + {/* children will be the root Component, ErrorBoundary, or HydrateFallback */} + {children} + + + + + ); +} + +export default function App() { + return ; +} + +export function ErrorBoundary() {} +``` + +**A note on `useLoaderData`in the `Layout` Component** + +`useLoaderData` is not permitted to be used in `ErrorBoundary` components because it is intended for the happy-path route rendering, and its typings have a built-in assumption that the `loader` ran successfully and returned something. That assumption doesn't hold in an `ErrorBoundary` because it could have been the `loader` that threw and triggered the boundary! In order to access loader data in `ErrorBoundary`'s, you can use `useRouteLoaderData` which accounts for the loader data potentially being `undefined`. + +Because your `Layout` component is used in both success and error flows, this same restriction holds. If you need to fork logic in your `Layout` depending on if it was a successful request or not, you can use `useRouteLoaderData("root")` and `useRouteError()`. + +Because your `` component is used for rendering the `ErrorBoundary`, you should be _very defensive_ to ensure that you can render your `ErrorBoundary` without encountering any render errors. If your `Layout` throws another error trying to render the boundary, then it can't be used and your UI will fall back to the very minimal built-in default `ErrorBoundary`. + +```tsx filename=app/root.tsx lines=[6-7,19-29,32-34] +export function Layout({ + children, +}: { + children: React.ReactNode; +}) { + const data = useRouteLoaderData("root"); + const error = useRouteError(); + + return ( + + + + + + +