Push V1 app

This commit is contained in:
jlacoste
2026-06-26 11:54:29 +02:00
parent 8b7caa1a5a
commit 9d1990523f
3881 changed files with 1291493 additions and 1 deletions
+44
View File
@@ -0,0 +1,44 @@
---
title: Accessibility
---
# Accessibility
Accessibility in a React Router app looks a lot like accessibility on the web in general. Using proper semantic markup and following the [Web Content Accessibility Guidelines (WCAG)][wcag] will get you most of the way there.
React Router makes certain accessibility practices the default where possible and provides APIs to help where it's not.
## Links
[MODES: framework, data, declarative]
<br/>
<br/>
The [`<Link>` component][link] renders a standard anchor tag, meaning that you get its accessibility behaviors from the browser for free!
React Router also provides the [`<NavLink/>`][navlink] which behaves the same as `<Link>`, but it also provides context for assistive technology when the link points to the current page. This is useful for building navigation menus or breadcrumbs.
## Routing
[MODES: framework]
<br/>
<br/>
If you are rendering [`<Scripts>`][scripts] in your app, there are some important things to consider to make client-side routing more accessible for your users.
With a traditional multi-page website we don't have to think about route changes too much. Your app renders an anchor tag, and the browser handles the rest. If your users disable JavaScript, your React Router app should already work this way by default!
When the client scripts in React Router are loaded, React Router takes control of routing and prevents the browser's default behavior. React Router doesn't make any assumptions about your UI as the route changes. There are some important features you'll want to consider as a result, including:
- **Focus management:** What element receives focus when the route changes? This is important for keyboard users and can be helpful for screen-reader users.
- **Live-region announcements:** Screen-reader users also benefit from announcements when a route has changed. You may want to also notify them during certain transition states depending on the nature of the change and how long loading is expected to take.
In 2019, [Marcy Sutton led and published findings from user research][marcy-sutton-led-and-published-findings-from-user-research] to help developers build accessible client-side routing experiences.
[link]: ../api/components/Link
[navlink]: ../api/components/NavLink
[scripts]: ../api/components/Scripts
[wcag]: https://www.w3.org/WAI/standards-guidelines/wcag/
[marcy-sutton-led-and-published-findings-from-user-research]: https://www.gatsbyjs.com/blog/2019-07-11-user-testing-accessible-client-routing
+199
View File
@@ -0,0 +1,199 @@
---
title: Client Data
---
# Client Data
[MODES: framework]
<br/>
<br/>
You can fetch and mutate data directly in the browser using `clientLoader` and `clientAction` functions.
These functions are the primary mechanism for data handling when using [SPA mode][spa]. This guide demonstrates common use cases for leveraging client data in Server-Side Rendering (SSR).
## Skip the Server Hop
When using React Router with a Backend-For-Frontend (BFF) architecture, you might want to bypass the React Router server and communicate directly with your backend API. This approach requires proper authentication handling and assumes no CORS restrictions. Here's how to implement this:
1. Load the data from server `loader` on the document load
2. Load the data from the `clientLoader` on all subsequent loads
In this scenario, React Router will _not_ call the `clientLoader` on hydration - and will only call it on subsequent navigations.
```tsx lines=[4,11]
export async function loader({
request,
}: Route.LoaderArgs) {
const data = await fetchApiFromServer({ request }); // (1)
return data;
}
export async function clientLoader({
request,
}: Route.ClientLoaderArgs) {
const data = await fetchApiFromClient({ request }); // (2)
return data;
}
```
## Fullstack State
Sometimes you need to combine data from both the server and browser (like IndexedDB or browser SDKs) before rendering a component. Here's how to implement this pattern:
1. Load the partial data from server `loader` on the document load
2. Export a [`HydrateFallback`][hydratefallback] component to render during SSR because we don't yet have a full set of data
3. Set `clientLoader.hydrate = true`, this instructs React Router to call the clientLoader as part of initial document hydration
4. Combine the server data with the client data in `clientLoader`
```tsx lines=[4-6,19-20,23,26]
export async function loader({
request,
}: Route.LoaderArgs) {
const partialData = await getPartialDataFromDb({
request,
}); // (1)
return partialData;
}
export async function clientLoader({
request,
serverLoader,
}: Route.ClientLoaderArgs) {
const [serverData, clientData] = await Promise.all([
serverLoader(),
getClientData(request),
]);
return {
...serverData, // (4)
...clientData, // (4)
};
}
clientLoader.hydrate = true as const; // (3)
export function HydrateFallback() {
return <p>Skeleton rendered during SSR</p>; // (2)
}
export default function Component({
// This will always be the combined set of server + client data
loaderData,
}: Route.ComponentProps) {
return <>...</>;
}
```
## Choosing Server or Client Data Loading
You can mix data loading strategies across your application, choosing between server-only or client-only data loading for each route. Here's how to implement both approaches:
1. Export a `loader` when you want to use server data
2. Export `clientLoader` and a `HydrateFallback` when you want to use client data
A route that only depends on a server loader looks like this:
```tsx filename=app/routes/server-data-route.tsx
export async function loader({
request,
}: Route.LoaderArgs) {
const data = await getServerData(request);
return data;
}
export default function Component({
loaderData, // (1) - server data
}: Route.ComponentProps) {
return <>...</>;
}
```
A route that only depends on a client loader looks like this.
```tsx filename=app/routes/client-data-route.tsx
export async function clientLoader({
request,
}: Route.ClientLoaderArgs) {
const clientData = await getClientData(request);
return clientData;
}
// Note: you do not have to set this explicitly - it is implied if there is no `loader`
clientLoader.hydrate = true;
// (2)
export function HydrateFallback() {
return <p>Skeleton rendered during SSR</p>;
}
export default function Component({
loaderData, // (2) - client data
}: Route.ComponentProps) {
return <>...</>;
}
```
## Client-Side Caching
You can implement client-side caching (using memory, localStorage, etc.) to optimize server requests. Here's a pattern that demonstrates cache management:
1. Load the data from server `loader` on the document load
2. Set `clientLoader.hydrate = true` to prime the cache
3. Load subsequent navigations from the cache via `clientLoader`
4. Invalidate the cache in your `clientAction`
Note that since we are not exporting a `HydrateFallback` component, we will SSR the route component and then run the `clientLoader` on hydration, so it's important that your `loader` and `clientLoader` return the same data on initial load to avoid hydration errors.
```tsx lines=[4,26,32,39,46]
export async function loader({
request,
}: Route.LoaderArgs) {
const data = await getDataFromDb({ request }); // (1)
return data;
}
export async function action({
request,
}: Route.ActionArgs) {
await saveDataToDb({ request });
return { ok: true };
}
let isInitialRequest = true;
export async function clientLoader({
request,
serverLoader,
}: Route.ClientLoaderArgs) {
const cacheKey = generateKey(request);
if (isInitialRequest) {
isInitialRequest = false;
const serverData = await serverLoader();
cache.set(cacheKey, serverData); // (2)
return serverData;
}
const cachedData = await cache.get(cacheKey);
if (cachedData) {
return cachedData; // (3)
}
const serverData = await serverLoader();
cache.set(cacheKey, serverData);
return serverData;
}
clientLoader.hydrate = true; // (2)
export async function clientAction({
request,
serverAction,
}: Route.ClientActionArgs) {
const cacheKey = generateKey(request);
cache.delete(cacheKey); // (4)
const serverData = await serverAction();
return serverData;
}
```
[spa]: ../how-to/spa
[hydratefallback]: ../start/framework/route-module#hydratefallback
+317
View File
@@ -0,0 +1,317 @@
---
title: Data Strategy
---
# Data Strategy
[MODES: data]
<br />
<br />
<docs-warning>This is a low-level API intended for advanced use-cases. This overrides React Router's internal handling of `action`/`loader` execution, and if done incorrectly will break your app code. Please use with caution and perform the appropriate testing.</docs-warning>
## Overview
By default, React Router is opinionated about how your data is loaded/submitted - and most notably, executes all of your [`loader`][loader] functions in parallel for optimal data fetching. While we think this is the right behavior for most use-cases, we realize that there is no "one size fits all" solution when it comes to data fetching for the wide landscape of application requirements.
The [`dataStrategy`][data-strategy] option gives you full control over how your [`action`][action]/[`loader`][loader] functions are executed and lays the foundation to build in more advanced APIs such as middleware, context, and caching layers. Over time, we expect that we'll leverage this API internally to bring more first class APIs to React Router, but until then (and beyond), this is your way to add more advanced functionality for your application's data needs.
## Usage
A custom `dataStrategy` receives the `loader`/`action` arguments (`request`, `params`, `context`) plus a few more that allow you to decide how you want to control the executions for your application:
- `matches`: An array of `DataStrategyMatch` instances for the routes matched by the current `request`
- `runClientMiddleware`: A helper function to run the middleware for the matched routes
- `fetcherKey`: The fetcher key if this is for a fetcher request and not a navigation
A `DataStrategyMatch` is a normal route match plus a few additional fields:
- `shouldCallHandler`: A function that tells you whether this routes handler should be called for this request
- `shouldRevalidateArgs`: The arguments that to be passed to the routes `shouldRevalidate` for this request
- ~~`shouldLoad`~~: A boolean field for whether this routes handler should be run for this request
- Deprecated in favor of the more powerful `shouldCallHandler` API
- `resolve`: A function to handle call through to the route handler, and also allow you custom execution of the handler
Here's a basic example that adds logging around the handler executions:
```tsx
let router = createBrowserRouter(routes, {
async dataStrategy({
matches,
request,
runClientMiddleware,
}) {
// Determine which matches are expected to be executed for this request.
// - For loading navigations, this will return true for new routes + existing
// routes requiring revalidation
// - For submission navigations, this will only return true for the action route
// - For fetcher calls, this will only return true for the fetcher route
const matchesToLoad = matches.filter((m) =>
m.shouldCallHandler(),
);
// For each match that we want to execute, call match.resolve() to execute
// the handler and store the result
const results: Record<string, DataStrategyResult> = {};
await runClientMiddleware(() =>
Promise.all(
matchesToLoad.map(async (match) => {
console.log(`Processing ${match.route.id}`);
// The resolve function calls through to the route handler
results[match.route.id] = await match.resolve();
}),
),
);
return results;
},
});
```
The `dataStrategy` function should return a `Record<string, DataStrategyResult>` which contains the result for each handler that was executed. A `DataStrategyResult` is just a wrapper object that indicates if the handler returned or threw:
```ts
interface DataStrategyResult {
type: "data" | "error";
result: unknown; // data, Error, Response, data()
}
```
### Calling Route Middleware
If you are using `middleware` on your routes, you need to leverage the `callClientMiddleware` helper function to execute `middleware` around your handlers:
```tsx
let router = createBrowserRouter(routes, {
async dataStrategy({
matches,
request,
runClientMiddleware,
}) {
const matchesToLoad = matches.filter((m) =>
m.shouldCallHandler(),
);
const results: Record<string, DataStrategyResult> = {};
// Run middleware and execute handlers at the end of the middleware chain
await runClientMiddleware(() =>
Promise.all(
matchesToLoad.map(async (match) => {
results[match.route.id] = await match.resolve();
}),
),
);
return results;
},
});
```
`runClientMiddleware` takes the same arguments as `dataStrategy` so it can also be easily composed with a standalone `dataStrategy` implementation:
```tsx
const loggingDataStrategy: DataStrategyFunction = () => {
/* ... */
};
let router = createBrowserRouter(routes, {
async dataStrategy({ runClientMiddleware }) {
let results = await runClientMiddleware(
loggingDataStrategy,
);
return results;
},
});
```
### Advanced handler execution
If you want more fine-grained control over the execution of the handler, you can pass a callback to `match.resolve()`:
```tsx
// Assume a loader shape such as
function loader({ request }, customContext) {...}
// In your dataStrategy, you can pass this context from inside a resolve callback
await Promise.all(
matchesToLoad.map((match, i) =>
match.resolve((handler) => {
let customContext = getCustomContext();
// Call the handler and p[ass a custom parameter as the handler's second argument
return handler(customContext);
}),
),
);
```
### Custom Revalidation Behavior
If you want to alter the revalidation behavior, you can pass your own `defaultShouldRevalidate` to `match.shouldCallHandler()` which will pass through to any route level `shouldRevalidate` functions. The arguments that would be passed to the route level `shouldRevalidate` are available on `match.shouldRevalidateArgs`:
```tsx
const matchesToLoad = matches.filter((match) => {
let defaultShouldRevalidate = customShouldRevalidate(
match.shouldRevalidateArgs,
);
return m.shouldCallHandler(defaultShouldRevalidate);
});
```
## Migrating away from `shouldLoad`
Now that we have stabilized the new `match.shouldCallHandler()`/`match.shouldRevalidateArgs` fields, it's recommended to move away from the now-deprecated `match.shouldLoad` API. The prior boolean approach did not allow for custom `dataStrategy` functions to alter the default revalidation behavior, so the new function-based APIs were created to allow that.
The major difference between these two APIs is that when using `shouldLoad`, calling `resolve()` would _only_ call the handler if `shouldLoad` was `true`. You could safely call it for all matches even if only a subset needed to have their handlers executed.
With `shouldCallHandler`, you are in charge of which handlers should be called so calling resolve will automatically call the handler. You should only call resolve on the set of matches you wish to run handlers for.
Here's an example change from the prior API to the new API. Note that we pre-filter the `matchesToLoad` before calling `resolve()`:
```diff
let results = {};
+let matchesToLoad = matches.filter(m => m.shouldCallHandler());
await Promise.all(() =>
- matches.map((m) => {
+ matchesToLoad.map((m) => {
results[m.route.id] = await m.resolve();
}),
);
return results;
```
## Advanced Use Cases
### Custom Middleware
<docs-info>This is an unlikely use-case now that React Router has built-in middleware, but if you wish to use a custom middleware you can do so with a `dataStrategy`.</docs-info>
Let's define a middleware on each route via [`handle`](../start/data/route-object#handle)
and call middleware sequentially first, then call all
[`loader`](../start/data/route-object#loader)s in parallel - providing
any data made available via the middleware:
```ts
const routes = [
{
id: "parent",
path: "/parent",
loader({ request }, context) {
// ...
},
handle: {
async middleware({ request }, context) {
context.parent = "PARENT MIDDLEWARE";
},
},
children: [
{
id: "child",
path: "child",
loader({ request }, context) {
// ...
},
handle: {
async middleware({ request }, context) {
context.child = "CHILD MIDDLEWARE";
},
},
},
],
},
];
let router = createBrowserRouter(routes, {
async dataStrategy({ matches, params, request }) {
// Run middleware sequentially and let them add data to `context`
let context = {};
for (const match of matches) {
if (match.route.handle?.middleware) {
await match.route.handle.middleware(
{ request, params },
context,
);
}
}
// Run loaders in parallel with the `context` value
let matchesToLoad = matches.filter((m) =>
m.shouldCallHandler(),
);
let results = await Promise.all(
matchesToLoad.map((match, i) =>
match.resolve((handler) => {
// Whatever you pass to `handler` will be passed as the 2nd parameter
// to your loader/action
return handler(context);
}),
),
);
return results.reduce(
(acc, result, i) =>
Object.assign(acc, {
[matchesToLoad[i].route.id]: result,
}),
{},
);
},
});
```
### Custom Handler
It's also possible you don't even want to define a [`loader`](../start/data/route-object#loader)
implementation at the route level. Maybe you want to just determine the
routes and issue a single GraphQL request for all of your data. You can do
that by setting your `route.loader=true` so it qualifies as "having a
loader", and then store GQL fragments on `route.handle`:
```ts
const routes = [
{
id: "parent",
path: "/parent",
loader: true,
handle: {
gql: gql`
fragment Parent on Whatever {
parentField
}
`,
},
children: [
{
id: "child",
path: "child",
loader: true,
handle: {
gql: gql`
fragment Child on Whatever {
childField
}
`,
},
},
],
},
];
let router = createBrowserRouter(routes, {
async dataStrategy({ matches, params, request }) {
const matchesToLoad = matches.filter((m) =>
m.shouldCallHandler(),
);
// Compose route fragments into a single GQL payload
let gql = getFragmentsFromRouteHandles(matchesToLoad);
let data = await fetchGql(gql);
// Parse results back out into individual route level `DataStrategyResult`'s
// keyed by `routeId`
let results = parseResultsFromGql(matchesToLoad, data);
return results;
},
});
```
Note that we never actually call `match.resolve()` in this scenario since we don't want to call the handlers defined on the routes. We instead make a single GQL call and split the resulting data back out to the proper routes in `results`.
[loader]: ../start/data/route-object#loader
[action]: ../start/data/route-object#action
[data-strategy]: ../api/data-routers/createBrowserRouter#optsdatastrategy
+231
View File
@@ -0,0 +1,231 @@
---
title: Error Boundaries
---
# Error Boundaries
[MODES: framework, data]
<br/>
<br/>
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 rendering form validation errors or error reporting. 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
### Framework Mode
[modes: framework]
In [Framework Mode][picking-a-mode], errors are passed to the route-level error boundary as a prop (see [`Route.ErrorBoundaryProps`][type-safety]), so you don't need to use a hook to grab it:
```tsx filename=root.tsx lines=[1,3-5]
import { Route } from "./+types/root";
export function ErrorBoundary({
error,
}: Route.ErrorBoundaryProps) {
if (isRouteErrorResponse(error)) {
return (
<>
<h1>
{error.status} {error.statusText}
</h1>
<p>{error.data}</p>
</>
);
} else if (error instanceof Error) {
return (
<div>
<h1>Error</h1>
<p>{error.message}</p>
<p>The stack trace is:</p>
<pre>{error.stack}</pre>
</div>
);
} else {
return <h1>Unknown Error</h1>;
}
}
```
### Data Mode
[modes: data]
In [Data Mode][picking-a-mode], the `ErrorBoundary` doesn't receive props, so you can access it via `useRouteError`:
```tsx lines=[1,6,16]
import { useRouteError } from "react-router";
let router = createBrowserRouter([
{
path: "/",
ErrorBoundary: RootErrorBoundary,
Component: Root,
},
]);
function Root() {
/* ... */
}
function RootErrorBoundary() {
let error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<>
<h1>
{error.status} {error.statusText}
</h1>
<p>{error.data}</p>
</>
);
} else if (error instanceof Error) {
return (
<div>
<h1>Error</h1>
<p>{error.message}</p>
<p>The stack trace is:</p>
<pre>{error.stack}</pre>
</div>
);
} else {
return <h1>Unknown Error</h1>;
}
}
```
## 2. Write a bug
[modes: framework,data]
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
[modes: framework,data]
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.
### Framework Mode
[modes: framework]
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 |
### Data Mode
[modes: data]
In Data Mode, the equivalent route tree might look like:
```tsx
let router = createBrowserRouter([
{
path: "/app",
Component: App,
ErrorBoundary: AppErrorBoundary, // ✅ has error boundary
children: [
{
path: "invoices",
Component: Invoices, // ❌ no error boundary
children: [
{
path: ":id",
Component: Invoice,
ErrorBoundary: InvoiceErrorBoundary, // ✅ has error boundary
children: [
{
path: "payments",
Component: Payments, // ❌ no error boundary
},
],
},
],
},
],
},
]);
```
The following table shows which error boundary will render given the origin of the error:
| error origin | rendered boundary |
| ------------ | ---------------------- |
| `App` | `AppErrorBoundary` |
| `Invoices` | `AppErrorBoundary` |
| `Invoice` | `InvoiceErrorBoundary` |
| `Payments` | `InvoiceErrorBoundary` |
## Error Sanitization
[modes: framework]
In Framework Mode when building for production, 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.
[picking-a-mode]: ../start/modes
[type-safety]: ../explanation/type-safety
+142
View File
@@ -0,0 +1,142 @@
---
title: Error Reporting
---
# Error Reporting
[MODES: framework,data]
<br/>
<br/>
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.
## Server Errors
[modes: framework]
To access these caught errors on the server, use the `handleError` export of the server entry module.
### 1. Reveal the server entry
If you don't see [`entry.server.tsx`][entryserver] in your app directory, you're using a default entry. Reveal it with this cli command:
```shellscript nonumber
react-router reveal entry.server
```
### 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);
}
};
```
See also:
- [`handleError`][handleError]
## Client Errors
To access these caught errors on the client, use the `onError` prop on your [`HydratedRouter`][hydratedrouter] or [`RouterProvider`][routerprovider] component.
### Framework Mode
[modes: framework]
#### 1. Reveal the client entry
If you don't see [`entry.client.tsx`][entryclient] in your app directory, you're using a default entry. Reveal it with this cli command:
```shellscript nonumber
react-router reveal entry.client
```
#### 2. Add your error handler
This function is called whenever React Router catches an error in your application on the client.
```tsx filename=entry.client.tsx
import { type ClientOnErrorFunction } from "react-router";
const onError: ClientOnErrorFunction = (
error,
{ location, params, pattern, errorInfo },
) => {
myReportError(error, location, errorInfo);
// make sure to still log the error so you can see it
console.error(error, errorInfo);
};
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter onError={onError} />
</StrictMode>,
);
});
```
See also:
- [`<HydratedRouter onError>`][hydratedrouter-onerror]
### Data Mode
[modes: data]
This function is called whenever React Router catches an error in your application on the client.
```tsx
import {
createBrowserRouter,
type ClientOnErrorFunction,
} from "react-router";
import { RouterProvider } from "react-router/dom";
const onError: ClientOnErrorFunction = (
error,
{ location, params, pattern, errorInfo },
) => {
myReportError(error, location, errorInfo);
// make sure to still log the error so you can see it
console.error(error, errorInfo);
};
const router = createBrowserRouter(routes);
function App() {
return (
<RouterProvider router={router} onError={onError} />
);
}
```
See also:
- [`<RouterProvider onError>`][routerprovider-onerror]
[entryserver]: ../api/framework-conventions/entry.server.tsx
[handleError]: ../api/framework-conventions/entry.server.tsx#handleerror
[entryclient]: ../api/framework-conventions/entry.client.tsx
[hydratedrouter]: ../api/framework-routers/HydratedRouter
[routerprovider]: ../api/data-routers/RouterProvider
[hydratedrouter-onerror]: ../api/framework-routers/HydratedRouter#onError
[routerprovider-onerror]: ../api/data-routers/RouterProvider#onError
+307
View File
@@ -0,0 +1,307 @@
---
title: Using Fetchers
---
# Using Fetchers
[MODES: framework, data]
<br/>
<br/>
Fetchers are useful for creating complex, dynamic user interfaces that require multiple, concurrent data interactions without causing a navigation.
Fetchers track their own, independent state and can be used to load data, mutate data, submit forms, and generally interact with loaders and actions.
## Calling Actions
The most common case for a fetcher is to submit data to an action, triggering a revalidation of route data. Consider the following route module:
```tsx
import { useLoaderData } from "react-router";
export async function clientLoader({ request }) {
let title = localStorage.getItem("title") || "No Title";
return { title };
}
export default function Component() {
let data = useLoaderData();
return (
<div>
<h1>{data.title}</h1>
</div>
);
}
```
### 1. Add an action
First we'll add an action to the route for the fetcher to call:
```tsx lines=[7-11]
import { useLoaderData } from "react-router";
export async function clientLoader({ request }) {
// ...
}
export async function clientAction({ request }) {
await new Promise((res) => setTimeout(res, 1000));
let data = await request.formData();
localStorage.setItem("title", data.get("title"));
return { ok: true };
}
export default function Component() {
let data = useLoaderData();
// ...
}
```
### 2. Create a fetcher
Next create a fetcher and render a form with it:
```tsx lines=[7,12-14]
import { useLoaderData, useFetcher } from "react-router";
// ...
export default function Component() {
let data = useLoaderData();
let fetcher = useFetcher();
return (
<div>
<h1>{data.title}</h1>
<fetcher.Form method="post">
<input type="text" name="title" />
</fetcher.Form>
</div>
);
}
```
### 3. Submit the form
If you submit the form now, the fetcher will call the action and revalidate the route data automatically.
### 4. Render pending state
Fetchers make their state available during the async work so you can render pending UI the moment the user interacts:
```tsx lines=[10]
export default function Component() {
let data = useLoaderData();
let fetcher = useFetcher();
return (
<div>
<h1>{data.title}</h1>
<fetcher.Form method="post">
<input type="text" name="title" />
{fetcher.state !== "idle" && <p>Saving...</p>}
</fetcher.Form>
</div>
);
}
```
### 5. Optimistic UI
Sometimes there's enough information in the form to render the next state immediately. You can access the form data with `fetcher.formData`:
```tsx lines=[3-4,8]
export default function Component() {
let data = useLoaderData();
let fetcher = useFetcher();
let title = fetcher.formData?.get("title") || data.title;
return (
<div>
<h1>{title}</h1>
<fetcher.Form method="post">
<input type="text" name="title" />
{fetcher.state !== "idle" && <p>Saving...</p>}
</fetcher.Form>
</div>
);
}
```
### 6. Fetcher Data and Validation
Data returned from an action is available in the fetcher's `data` property. This is primarily useful for returning error messages to the user for a failed mutation:
```tsx lines=[7-10,28-32]
// ...
export async function clientAction({ request }) {
await new Promise((res) => setTimeout(res, 1000));
let data = await request.formData();
let title = data.get("title") as string;
if (title.trim() === "") {
return { ok: false, error: "Title cannot be empty" };
}
localStorage.setItem("title", title);
return { ok: true, error: null };
}
export default function Component() {
let data = useLoaderData();
let fetcher = useFetcher();
let title = fetcher.formData?.get("title") || data.title;
return (
<div>
<h1>{title}</h1>
<fetcher.Form method="post">
<input type="text" name="title" />
{fetcher.state !== "idle" && <p>Saving...</p>}
{fetcher.data?.error && (
<p style={{ color: "red" }}>
{fetcher.data.error}
</p>
)}
</fetcher.Form>
</div>
);
}
```
## Loading Data
Another common use case for fetchers is to load data from a route for something like a combobox.
### 1. Create a search route
Consider the following route with a very basic search:
```tsx filename=./search-users.tsx
// { path: '/search-users', filename: './search-users.tsx' }
const users = [
{ id: 1, name: "Ryan" },
{ id: 2, name: "Michael" },
// ...
];
export async function loader({ request }) {
await new Promise((res) => setTimeout(res, 300));
let url = new URL(request.url);
let query = url.searchParams.get("q");
return users.filter((user) =>
user.name.toLowerCase().includes(query.toLowerCase()),
);
}
```
### 2. Render a fetcher in a combobox component
```tsx
import { useFetcher } from "react-router";
export function UserSearchCombobox() {
let fetcher = useFetcher();
return (
<div>
<fetcher.Form method="get" action="/search-users">
<input type="text" name="q" />
</fetcher.Form>
</div>
);
}
```
- The action points to the route we created above: "/search-users"
- The name of the input is "q" to match the query parameter
### 3. Add type inference
```tsx lines=[2,5]
import { useFetcher } from "react-router";
import type { loader } from "./search-users";
export function UserSearchCombobox() {
let fetcher = useFetcher<typeof loader>();
// ...
}
```
Ensure you use `import type` so you only import the types.
### 4. Render the data
```tsx lines=[10-16]
import { useFetcher } from "react-router";
export function UserSearchCombobox() {
let fetcher = useFetcher<typeof loader>();
return (
<div>
<fetcher.Form method="get" action="/search-users">
<input type="text" name="q" />
</fetcher.Form>
{fetcher.data && (
<ul>
{fetcher.data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)}
</div>
);
}
```
Note you will need to hit "enter" to submit the form and see the results.
### 5. Render a pending state
```tsx lines=[12-14]
import { useFetcher } from "react-router";
export function UserSearchCombobox() {
let fetcher = useFetcher<typeof loader>();
return (
<div>
<fetcher.Form method="get" action="/search-users">
<input type="text" name="q" />
</fetcher.Form>
{fetcher.data && (
<ul
style={{
opacity: fetcher.state === "idle" ? 1 : 0.25,
}}
>
{fetcher.data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)}
</div>
);
}
```
### 6. Search on user input
Fetchers can be submitted programmatically with `fetcher.submit`:
```tsx lines=[5-7]
<fetcher.Form method="get" action="/search-users">
<input
type="text"
name="q"
onChange={(event) => {
fetcher.submit(event.currentTarget.form);
}}
/>
</fetcher.Form>
```
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.
@@ -0,0 +1,410 @@
---
title: File Route Conventions
---
# File Route Conventions
[MODES: framework]
<br/>
<br/>
The `@react-router/fs-routes` package enables file-convention based route config.
## Setting up
First install the `@react-router/fs-routes` package:
```shellscript nonumber
npm i @react-router/fs-routes
```
Then use it to provide route config in your `app/routes.ts` file:
```tsx filename=app/routes.ts
import { type RouteConfig } from "@react-router/dev/routes";
import { flatRoutes } from "@react-router/fs-routes";
export default flatRoutes() satisfies RouteConfig;
```
Any modules in the `app/routes` directory will become routes in your application by default.
The `ignoredRouteFiles` option allows you to specify files that should not be included as routes:
```tsx filename=app/routes.ts
import { type RouteConfig } from "@react-router/dev/routes";
import { flatRoutes } from "@react-router/fs-routes";
export default flatRoutes({
ignoredRouteFiles: ["home.tsx"],
}) satisfies RouteConfig;
```
This will look for routes in the `app/routes` directory by default, but this can be configured via the `rootDirectory` option which is relative to your app directory:
```tsx filename=app/routes.ts
import { type RouteConfig } from "@react-router/dev/routes";
import { flatRoutes } from "@react-router/fs-routes";
export default flatRoutes({
rootDirectory: "file-routes",
}) satisfies RouteConfig;
```
The rest of this guide will assume you're using the default `app/routes` directory.
## Basic Routes
The filename maps to the route's URL pathname, except for `_index.tsx` which is the [index route][index_route] for the [root route][root_route]. You can use `.js`, `.jsx`, `.ts` or `.tsx` file extensions.
```text lines=[3-4]
app/
├── routes/
│ ├── _index.tsx
│ └── about.tsx
└── root.tsx
```
| URL | Matched Routes |
| -------- | ----------------------- |
| `/` | `app/routes/_index.tsx` |
| `/about` | `app/routes/about.tsx` |
Note that these routes will be rendered in the outlet of `app/root.tsx` because of [nested routing][nested_routing].
## Dot Delimiters
Adding a `.` to a route filename will create a `/` in the URL.
```text lines=[5-7]
app/
├── routes/
│ ├── _index.tsx
│ ├── about.tsx
│ ├── concerts.trending.tsx
│ ├── concerts.salt-lake-city.tsx
│ └── concerts.san-diego.tsx
└── root.tsx
```
| URL | Matched Route |
| -------------------------- | ---------------------------------------- |
| `/` | `app/routes/_index.tsx` |
| `/about` | `app/routes/about.tsx` |
| `/concerts/trending` | `app/routes/concerts.trending.tsx` |
| `/concerts/salt-lake-city` | `app/routes/concerts.salt-lake-city.tsx` |
| `/concerts/san-diego` | `app/routes/concerts.san-diego.tsx` |
The dot delimiter also creates nesting, see the [nesting section][nested_routes] for more information.
## Dynamic Segments
Usually your URLs aren't static but data-driven. Dynamic segments allow you to match segments of the URL and use that value in your code. You create them with the `$` prefix.
```text lines=[5]
app/
├── routes/
│ ├── _index.tsx
│ ├── about.tsx
│ ├── concerts.$city.tsx
│ └── concerts.trending.tsx
└── root.tsx
```
| URL | Matched Route |
| -------------------------- | ---------------------------------- |
| `/` | `app/routes/_index.tsx` |
| `/about` | `app/routes/about.tsx` |
| `/concerts/trending` | `app/routes/concerts.trending.tsx` |
| `/concerts/salt-lake-city` | `app/routes/concerts.$city.tsx` |
| `/concerts/san-diego` | `app/routes/concerts.$city.tsx` |
The value will be parsed from the URL and passed to various APIs. We call these values "URL Parameters". The most useful places to access the URL params are in [loaders] and [actions].
```tsx
export async function loader({ params }) {
return fakeDb.getAllConcertsForCity(params.city);
}
```
You'll note the property name on the `params` object maps directly to the name of your file: `$city.tsx` becomes `params.city`.
Routes can have multiple dynamic segments, like `concerts.$city.$date`, both are accessed on the params object by name:
```tsx
export async function loader({ params }) {
return fake.db.getConcerts({
date: params.date,
city: params.city,
});
}
```
See the [routing guide][routing_guide] for more information.
## Nested Routes
Nested Routing is the general idea of coupling segments of the URL to component hierarchy and data. You can read more about it in the [Routing Guide][nested_routing].
You create nested routes with [dot delimiters][dot_delimiters]. If the filename before the `.` matches another route filename, it automatically becomes a child route to the matching parent. Consider these routes:
```text lines=[5-8]
app/
├── routes/
│ ├── _index.tsx
│ ├── about.tsx
│ ├── concerts._index.tsx
│ ├── concerts.$city.tsx
│ ├── concerts.trending.tsx
│ └── concerts.tsx
└── root.tsx
```
All the routes that start with `app/routes/concerts.` will be child routes of `app/routes/concerts.tsx` and render inside the [parent route's outlet][nested_routing].
| URL | Matched Route | Layout |
| -------------------------- | ---------------------------------- | ------------------------- |
| `/` | `app/routes/_index.tsx` | `app/root.tsx` |
| `/about` | `app/routes/about.tsx` | `app/root.tsx` |
| `/concerts` | `app/routes/concerts._index.tsx` | `app/routes/concerts.tsx` |
| `/concerts/trending` | `app/routes/concerts.trending.tsx` | `app/routes/concerts.tsx` |
| `/concerts/salt-lake-city` | `app/routes/concerts.$city.tsx` | `app/routes/concerts.tsx` |
Note you typically want to add an index route when you add nested routes so that something renders inside the parent's outlet when users visit the parent URL directly.
For example, if the URL is `/concerts/salt-lake-city` then the UI hierarchy will look like this:
```tsx
<Root>
<Concerts>
<City />
</Concerts>
</Root>
```
## Nested URLs without Layout Nesting
Sometimes you want the URL to be nested, but you don't want the automatic layout nesting. You can opt out of nesting with a trailing underscore on the parent segment:
```text lines=[8]
app/
├── routes/
│ ├── _index.tsx
│ ├── about.tsx
│ ├── concerts.$city.tsx
│ ├── concerts.trending.tsx
│ ├── concerts.tsx
│ └── concerts_.mine.tsx
└── root.tsx
```
| URL | Matched Route | Layout |
| -------------------------- | ---------------------------------- | ------------------------- |
| `/` | `app/routes/_index.tsx` | `app/root.tsx` |
| `/about` | `app/routes/about.tsx` | `app/root.tsx` |
| `/concerts/mine` | `app/routes/concerts_.mine.tsx` | `app/root.tsx` |
| `/concerts/trending` | `app/routes/concerts.trending.tsx` | `app/routes/concerts.tsx` |
| `/concerts/salt-lake-city` | `app/routes/concerts.$city.tsx` | `app/routes/concerts.tsx` |
Note that `/concerts/mine` does not nest with `app/routes/concerts.tsx` anymore, but `app/root.tsx`. The `trailing_` underscore creates a path segment, but it does not create layout nesting.
Think of the `trailing_` underscore as the long bit at the end of your parent's signature, writing you out of the will, removing the segment that follows from the layout nesting.
## Nested Layouts without Nested URLs
We call these <a name="pathless-routes"><b>Pathless Routes</b></a>
Sometimes you want to share a layout with a group of routes without adding any path segments to the URL. A common example is a set of authentication routes that have a different header/footer than the public pages or the logged in app experience. You can do this with a `_leading` underscore.
```text lines=[3-5]
app/
├── routes/
│ ├── _auth.login.tsx
│ ├── _auth.register.tsx
│ ├── _auth.tsx
│ ├── _index.tsx
│ ├── concerts.$city.tsx
│ └── concerts.tsx
└── root.tsx
```
| URL | Matched Route | Layout |
| -------------------------- | ------------------------------- | ------------------------- |
| `/` | `app/routes/_index.tsx` | `app/root.tsx` |
| `/login` | `app/routes/_auth.login.tsx` | `app/routes/_auth.tsx` |
| `/register` | `app/routes/_auth.register.tsx` | `app/routes/_auth.tsx` |
| `/concerts` | `app/routes/concerts.tsx` | `app/routes/concerts.tsx` |
| `/concerts/salt-lake-city` | `app/routes/concerts.$city.tsx` | `app/routes/concerts.tsx` |
Think of the `_leading` underscore as a blanket you're pulling over the filename, hiding the filename from the URL.
## Optional Segments
Wrapping a route segment in parentheses will make the segment optional.
```text lines=[3-5]
app/
├── routes/
│ ├── ($lang)._index.tsx
│ ├── ($lang).$productId.tsx
│ └── ($lang).categories.tsx
└── root.tsx
```
| URL | Matched Route |
| -------------------------- | ----------------------------------- |
| `/` | `app/routes/($lang)._index.tsx` |
| `/categories` | `app/routes/($lang).categories.tsx` |
| `/en/categories` | `app/routes/($lang).categories.tsx` |
| `/fr/categories` | `app/routes/($lang).categories.tsx` |
| `/american-flag-speedo` | `app/routes/($lang)._index.tsx` |
| `/en/american-flag-speedo` | `app/routes/($lang).$productId.tsx` |
| `/fr/american-flag-speedo` | `app/routes/($lang).$productId.tsx` |
You may wonder why `/american-flag-speedo` is matching the `($lang)._index.tsx` route instead of `($lang).$productId.tsx`. This is because when you have an optional dynamic param segment followed by another dynamic param, it cannot reliably be determined if a single-segment URL such as `/american-flag-speedo` should match `/:lang` `/:productId`. Optional segments match eagerly and thus it will match `/:lang`. If you have this type of setup it's recommended to look at `params.lang` in the `($lang)._index.tsx` loader and redirect to `/:lang/american-flag-speedo` for the current/default language if `params.lang` is not a valid language code.
## Splat Routes
While [dynamic segments][dynamic_segments] match a single path segment (the stuff between two `/` in a URL), a splat route will match the rest of a URL, including the slashes.
```text lines=[4,6]
app/
├── routes/
│ ├── _index.tsx
│ ├── $.tsx
│ ├── about.tsx
│ └── files.$.tsx
└── root.tsx
```
| URL | Matched Route |
| -------------------------------------------- | ------------------------ |
| `/` | `app/routes/_index.tsx` |
| `/about` | `app/routes/about.tsx` |
| `/beef/and/cheese` | `app/routes/$.tsx` |
| `/files` | `app/routes/files.$.tsx` |
| `/files/talks/react-conf_old.pdf` | `app/routes/files.$.tsx` |
| `/files/talks/react-conf_final.pdf` | `app/routes/files.$.tsx` |
| `/files/talks/react-conf-FINAL-MAY_2024.pdf` | `app/routes/files.$.tsx` |
Similar to dynamic route parameters, you can access the value of the matched path on the splat route's `params` with the `"*"` key.
```tsx filename=app/routes/files.$.tsx
export async function loader({ params }) {
const filePath = params["*"];
return fake.getFileInfo(filePath);
}
```
## Catch-all Route
To create a route that will match any requests that don't match other defined routes (such as a 404 page), create a file named `$.tsx` within your routes directory:
| URL | Matched Route |
| ------------------------------ | ----------------------- |
| `/` | `app/routes/_index.tsx` |
| `/about` | `app/routes/about.tsx` |
| `/any-invalid-path-will-match` | `app/routes/$.tsx` |
By default the matched route will return a 200 response, so be sure to modify your catchall route to return a 404 instead:
```tsx filename=app/routes/$.tsx
export async function loader() {
return data({}, 404);
}
```
## Escaping Special Characters
If you want one of the special characters used for these route conventions to actually be a part of the URL, you can escape the conventions with `[]` characters. This can be especially helpful for [resource routes][resource_routes] that include an extension in the URL.
| Filename | URL |
| ----------------------------------- | ------------------- |
| `app/routes/sitemap[.]xml.tsx` | `/sitemap.xml` |
| `app/routes/[sitemap.xml].tsx` | `/sitemap.xml` |
| `app/routes/weird-url.[_index].tsx` | `/weird-url/_index` |
| `app/routes/dolla-bills-[$].tsx` | `/dolla-bills-$` |
| `app/routes/[[so-weird]].tsx` | `/[so-weird]` |
| `app/routes/reports.$id[.pdf].ts` | `/reports/123.pdf` |
## Folders for Organization
Routes can also be folders with a `route.tsx` file inside defining the route module. The rest of the files in the folder will not become routes. This allows you to organize your code closer to the routes that use them instead of repeating the feature names across other folders.
The files inside a folder have no meaning for the route paths, the route path is completely defined by the folder name.
Consider these routes:
```text
app/
├── routes/
│ ├── _landing._index.tsx
│ ├── _landing.about.tsx
│ ├── _landing.tsx
│ ├── app._index.tsx
│ ├── app.projects.tsx
│ ├── app.tsx
│ └── app_.projects.$id.roadmap.tsx
└── root.tsx
```
Some, or all of them can be folders holding their own `route` module inside.
```text
app/
├── routes/
│ ├── _landing._index/
│ │ ├── route.tsx
│ │ └── scroll-experience.tsx
│ ├── _landing.about/
│ │ ├── employee-profile-card.tsx
│ │ ├── get-employee-data.server.ts
│ │ ├── route.tsx
│ │ └── team-photo.jpg
│ ├── _landing/
│ │ ├── footer.tsx
│ │ ├── header.tsx
│ │ └── route.tsx
│ ├── app._index/
│ │ ├── route.tsx
│ │ └── stats.tsx
│ ├── app.projects/
│ │ ├── get-projects.server.ts
│ │ ├── project-buttons.tsx
│ │ ├── project-card.tsx
│ │ └── route.tsx
│ ├── app/
│ │ ├── footer.tsx
│ │ ├── primary-nav.tsx
│ │ └── route.tsx
│ ├── app_.projects.$id.roadmap/
│ │ ├── chart.tsx
│ │ ├── route.tsx
│ │ └── update-timeline.server.ts
│ └── contact-us.tsx
└── root.tsx
```
Note that when you turn a route module into a folder, the route module becomes `folder/route.tsx`, all other modules in the folder will not become routes. For example:
```
# these are the same route:
app/routes/app.tsx
app/routes/app/route.tsx
# as are these
app/routes/app._index.tsx
app/routes/app._index/route.tsx
```
[route-config-file]: ../start/framework/routing#configuring-routes
[loaders]: ../start/framework/data-loading
[actions]: ../start/framework/actions
[routing_guide]: ../start/framework/routing
[root_route]: ../start/framework/route-module
[index_route]: ../start/framework/routing#index-routes
[nested_routing]: ../start/framework/routing#nested-routes
[nested_routes]: #nested-routes
[dot_delimiters]: #dot-delimiters
[dynamic_segments]: #dynamic-segments
[resource_routes]: ../how-to/resource-routes
+217
View File
@@ -0,0 +1,217 @@
---
title: File Uploads
---
# File Uploads
[MODES: framework]
<br/>
<br/>
_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
### 1. 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/avatar.tsx"),
]),
] satisfies RouteConfig;
```
### 2. Add the form data parser
`form-data-parser` is a wrapper around `request.formData()` that provides streaming support for handling file uploads.
```shellscript
npm i @remix-run/form-data-parser
```
[See the `form-data-parser` docs for more information][form-data-parser]
### 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.
<docs-warning>
You must set the form's `enctype` to `multipart/form-data` for file uploads to work.
</docs-warning>
```tsx filename=pages/user-profile.tsx
import {
type FileUpload,
parseFormData,
} from "@remix-run/form-data-parser";
import type { Route } from "./+types/user-profile";
export async function action({
request,
}: Route.ActionArgs) {
const uploadHandler = async (fileUpload: FileUpload) => {
if (fileUpload.fieldName === "avatar") {
// process the upload and return a File
}
};
const formData = await parseFormData(
request,
uploadHandler,
);
// 'avatar' has already been processed at this point
const file = formData.get("avatar");
}
export default function Component() {
return (
<form method="post" encType="multipart/form-data">
<input type="file" name="avatar" />
<button>Submit</button>
</form>
);
}
```
## Local Storage Implementation
### 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.
```shellscript
npm i @remix-run/file-storage
```
[See the `file-storage` docs for more information][file-storage]
### 2. Create a storage configuration
Create a file that exports a `LocalFileStorage` instance to be used by different routes.
```ts filename=avatar-storage.server.ts
import { LocalFileStorage } from "@remix-run/file-storage/local";
export const fileStorage = new LocalFileStorage(
"./uploads/avatars",
);
export function getStorageKey(userId: string) {
return `user-${userId}-avatar`;
}
```
### 3. Implement the upload handler
Update the form's `action` to store files in the `fileStorage` instance.
```tsx filename=pages/user-profile.tsx
import {
type FileUpload,
parseFormData,
} from "@remix-run/form-data-parser";
import {
fileStorage,
getStorageKey,
} from "~/avatar-storage.server";
import type { Route } from "./+types/user-profile";
export async function action({
request,
params,
}: Route.ActionArgs) {
async function uploadHandler(fileUpload: FileUpload) {
if (
fileUpload.fieldName === "avatar" &&
fileUpload.type.startsWith("image/")
) {
let storageKey = getStorageKey(params.id);
// FileUpload objects are not meant to stick around for very long (they are
// streaming data from the request.body); store them as soon as possible.
await fileStorage.set(storageKey, fileUpload);
// Return a File for the FormData object. This is a LazyFile that knows how
// to access the file's content if needed (using e.g. file.stream()) but
// waits until it is requested to actually read anything.
return fileStorage.get(storageKey);
}
}
const formData = await parseFormData(
request,
uploadHandler,
);
}
export default function UserPage({
actionData,
params,
}: Route.ComponentProps) {
return (
<div>
<h1>User {params.id}</h1>
<form
method="post"
// The form's enctype must be set to "multipart/form-data" for file uploads
encType="multipart/form-data"
>
<input type="file" name="avatar" accept="image/*" />
<button>Submit</button>
</form>
<img
src={`/user/${params.id}/avatar`}
alt="user avatar"
/>
</div>
);
}
```
### 4. Add a route to serve the uploaded file
Create a [resource route][resource-route] that streams the file as a response.
```tsx filename=api/avatar.tsx
import {
fileStorage,
getStorageKey,
} from "~/avatar-storage.server";
import type { Route } from "./+types/avatar";
export async function loader({ params }: Route.LoaderArgs) {
const storageKey = getStorageKey(params.id);
const file = await fileStorage.get(storageKey);
if (!file) {
throw new Response("User avatar not found", {
status: 404,
});
}
return new Response(file.stream(), {
headers: {
"Content-Type": file.type,
"Content-Disposition": `attachment; filename=${file.name}`,
},
});
}
```
[form-data-parser]: https://www.npmjs.com/package/@remix-run/form-data-parser
[file-storage]: https://www.npmjs.com/package/@remix-run/file-storage
[file]: https://developer.mozilla.org/en-US/docs/Web/API/File
[resource-route]: ../how-to/resource-routes
+120
View File
@@ -0,0 +1,120 @@
---
title: Form Validation
---
# Form Validation
[MODES: framework, data]
<br/>
<br/>
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
We'll start by creating a basic signup route with form.
```ts filename=app/routes.ts
import {
type RouteConfig,
route,
} from "@react-router/dev/routes";
export default [
route("signup", "signup.tsx"),
] satisfies RouteConfig;
```
```tsx filename=signup.tsx
import type { Route } from "./+types/signup";
import { useFetcher } from "react-router";
export default function Signup(_: Route.ComponentProps) {
let fetcher = useFetcher();
return (
<fetcher.Form method="post">
<p>
<input type="email" name="email" />
</p>
<p>
<input type="password" name="password" />
</p>
<button type="submit">Sign Up</button>
</fetcher.Form>
);
}
```
## 2. Defining the Action
In this step, we'll define a server `action` in the same file as our `Signup` component. Note that the aim here is to provide a broad overview of the mechanics involved rather than digging deep into form validation rules or error object structures. We'll use rudimentary checks for the email and password to demonstrate the core concepts.
```tsx filename=signup.tsx
import type { Route } from "./+types/signup";
import { redirect, useFetcher, data } from "react-router";
export default function Signup(_: Route.ComponentProps) {
// omitted for brevity
}
export async function action({
request,
}: Route.ActionArgs) {
const formData = await request.formData();
const email = String(formData.get("email"));
const password = String(formData.get("password"));
const errors = {};
if (!email.includes("@")) {
errors.email = "Invalid email address";
}
if (password.length < 12) {
errors.password =
"Password should be at least 12 characters";
}
if (Object.keys(errors).length > 0) {
return data({ errors }, { status: 400 });
}
// Redirect to dashboard if validation is successful
return redirect("/dashboard");
}
```
If any validation errors are found, they are returned from the `action` to the fetcher. This is our way of signaling to the UI that something needs to be corrected, otherwise the user will be redirected to the dashboard.
Note the `data({ errors }, { status: 400 })` call. Setting a 400 status is the web standard way to signal to the client that there was a validation error (Bad Request). In React Router, only 2xx status codes trigger page data revalidation, so sending a 400 status prevents the normal revalidation that would occur after an `action`.
## 3. Displaying Validation Errors
Finally, we'll modify the `Signup` component to display validation errors, if any, from `fetcher.data`.
```tsx filename=signup.tsx lines=[3,8,13-15]
export default function Signup(_: Route.ComponentProps) {
let fetcher = useFetcher();
let errors = fetcher.data?.errors;
return (
<fetcher.Form method="post">
<p>
<input type="email" name="email" />
{errors?.email ? <em>{errors.email}</em> : null}
</p>
<p>
<input type="password" name="password" />
{errors?.password ? (
<em>{errors.password}</em>
) : null}
</p>
<button type="submit">Sign Up</button>
</fetcher.Form>
);
}
```
+165
View File
@@ -0,0 +1,165 @@
---
title: HTTP Headers
---
# HTTP Headers
[MODES: framework]
<br/>
<br/>
## Reading request headers
The `request` sent to route handlers is a standard Web Fetch [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request), so you can read headers directly from the [`request.headers`](https://developer.mozilla.org/en-US/docs/Web/API/Request/headers) property:
```tsx filename=some-route.tsx
export async function loader({
request,
}: Route.LoaderArgs) {
// Standard Headers methods are available
const userAgent = request.headers.get("User-Agent");
const hasCookies = request.headers.has("Cookie");
// ...
}
```
## Setting response headers
Headers are primarily defined with the route module `headers` export. You can also set headers in `entry.server.tsx`.
### From Route Modules
```tsx filename=some-route.tsx
import { Route } from "./+types/some-route";
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`.
### From loaders and actions
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";
export async function loader({ params }: LoaderArgs) {
let [page, ms] = await fakeTimeCall(
await getPage(params.id),
);
return data(page, {
headers: {
"Server-Timing": `page;dur=${ms};desc="Page query"`,
},
});
}
```
**2. Return from `headers` export**
Headers from loaders and actions are not sent automatically. You must explicitly return them from the `headers` export.
```tsx
function hasAnyHeaders(headers: Headers): boolean {
return [...headers].length > 0;
}
export function headers({
actionHeaders,
loaderHeaders,
}: HeadersArgs) {
return hasAnyHeaders(actionHeaders)
? actionHeaders
: loaderHeaders;
}
```
One notable exception is `Set-Cookie` headers, which are automatically preserved from `headers`, `loader`, and `action` in parent routes, even without exporting `headers` from the child route.
### 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.
### 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 async 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
```
+4
View File
@@ -0,0 +1,4 @@
---
title: How-Tos
order: 4
---
+556
View File
@@ -0,0 +1,556 @@
---
title: Instrumentation
---
# Instrumentation
[MODES: framework, data]
<br/>
<br/>
Instrumentation allows you to add logging, error reporting, and performance tracing to your React Router application without modifying your actual route handlers. This enables comprehensive observability solutions for production applications on both the server and client.
## Overview
With the React Router Instrumentation APIs, you provide "wrapper" functions that execute around your request handlers, router operations, route middlewares, and/or route handlers. This allows you to:
- Monitor application performance
- Add logging
- Integrate with observability platforms (Sentry, DataDog, New Relic, etc.)
- Implement OpenTelemetry tracing
- Track user behavior and navigation patterns
A key design principle is that instrumentation is **read-only** - you can observe what's happening but cannot modify runtime application behavior by modifying the arguments passed to, or data returned from your route handlers.
<docs-info>
As with any instrumentation approach, adding additional code execution at runtime may alter the performance characteristics compared to an uninstrumented application. Keep this in mind and perform appropriate testing and/or leverage conditional instrumentation to avoid a negative UX impact in production.
</docs-info>
## Quick Start (Framework Mode)
[modes: framework]
### 1. Server-side Instrumentation
Add instrumentations to your `entry.server.tsx`:
```tsx filename=app/entry.server.tsx
export const instrumentations = [
{
// Instrument the server handler
handler(handler) {
handler.instrument({
async request(handleRequest, { request }) {
let url = `${request.method} ${request.url}`;
console.log(`Request start: ${url}`);
await handleRequest();
console.log(`Request end: ${url}`);
},
});
},
// Instrument individual routes
route(route) {
// Skip instrumentation for specific routes if needed
if (route.id === "root") return;
route.instrument({
async loader(callLoader, { request }) {
let url = `${request.method} ${request.url}`;
console.log(`Loader start: ${url} - ${route.id}`);
await callLoader();
console.log(`Loader end: ${url} - ${route.id}`);
},
// Other available instrumentations:
// async action() { /* ... */ },
// async middleware() { /* ... */ },
// async lazy() { /* ... */ },
});
},
},
];
export default function handleRequest(/* ... */) {
// Your existing handleRequest implementation
}
```
### 2. Client-side Instrumentation
Add instrumentations to your `entry.client.tsx`:
```tsx filename=app/entry.client.tsx
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
const instrumentations = [
{
// Instrument router operations
router(router) {
router.instrument({
// Instrument navigations
async navigate(callNavigate, { currentUrl, to }) {
let nav = `${currentUrl} → ${to}`;
console.log(`Navigation start: ${nav}`);
await callNavigate();
console.log(`Navigation end: ${nav}`);
},
// Instrument fetcher calls
async fetch(
callFetch,
{ href, currentUrl, fetcherKey },
) {
let fetch = `${fetcherKey} → ${href}`;
console.log(`Fetcher start: ${fetch}`);
await callFetch();
console.log(`Fetcher end: ${fetch}`);
},
});
},
// Instrument individual routes (same as server-side)
route(route) {
// Skip instrumentation for specific routes if needed
if (route.id === "root") return;
route.instrument({
async loader(callLoader, { request }) {
let url = `${request.method} ${request.url}`;
console.log(`Loader start: ${url} - ${route.id}`);
await callLoader();
console.log(`Loader end: ${url} - ${route.id}`);
},
// Other available instrumentations:
// async action() { /* ... */ },
// async middleware() { /* ... */ },
// async lazy() { /* ... */ },
});
},
},
];
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter instrumentations={instrumentations} />
</StrictMode>,
);
});
```
## Quick Start (Data Mode)
[modes: data]
In Data Mode, you add instrumentations when creating your router:
```tsx
import {
createBrowserRouter,
RouterProvider,
} from "react-router";
const instrumentations = [
{
// Instrument router operations
router(router) {
router.instrument({
// Instrument navigations
async navigate(callNavigate, { currentUrl, to }) {
let nav = `${currentUrl} → ${to}`;
console.log(`Navigation start: ${nav}`);
await callNavigate();
console.log(`Navigation end: ${nav}`);
},
// Instrument fetcher calls
async fetch(
callFetch,
{ href, currentUrl, fetcherKey },
) {
let fetch = `${fetcherKey} → ${href}`;
console.log(`Fetcher start: ${fetch}`);
await callFetch();
console.log(`Fetcher end: ${fetch}`);
},
});
},
// Instrument individual routes (same as server-side)
route(route) {
// Skip instrumentation for specific routes if needed
if (route.id === "root") return;
route.instrument({
async loader(callLoader, { request }) {
let url = `${request.method} ${request.url}`;
console.log(`Loader start: ${url} - ${route.id}`);
await callLoader();
console.log(`Loader end: ${url} - ${route.id}`);
},
// Other available instrumentations:
// async action() { /* ... */ },
// async middleware() { /* ... */ },
// async lazy() { /* ... */ },
});
},
},
];
const router = createBrowserRouter(routes, {
instrumentations,
});
function App() {
return <RouterProvider router={router} />;
}
```
## Core Concepts
### Instrumentation Levels
There are different levels at which you can instrument your application. Each instrumentation function receives a second "info" parameter containing relevant contextual information for the specific aspect being instrumented.
#### 1. Handler Level (Server)
[modes: framework]
Instruments the top-level request handler that processes all requests to your server:
```tsx filename=entry.server.tsx
export const instrumentations = [
{
handler(handler) {
handler.instrument({
async request(handleRequest, { request, context }) {
// Runs around ALL requests to your app
await handleRequest();
},
});
},
},
];
```
#### 2. Router Level (Client)
[modes: framework,data]
Instruments client-side router operations like navigations and fetcher calls:
```tsx
export const instrumentations = [
{
router(router) {
router.instrument({
async navigate(callNavigate, { to, currentUrl }) {
// Runs around navigation operations
await callNavigate();
},
async fetch(
callFetch,
{ href, currentUrl, fetcherKey },
) {
// Runs around fetcher operations
await callFetch();
},
});
},
},
];
// Framework Mode (entry.client.tsx)
<HydratedRouter instrumentations={instrumentations} />;
// Data Mode
const router = createBrowserRouter(routes, {
instrumentations,
});
```
#### 3. Route Level (Server + Client)
[modes: framework,data]
Instruments individual route handlers:
```tsx
const instrumentations = [
{
route(route) {
route.instrument({
async loader(
callLoader,
{ params, request, context, pattern },
) {
// Runs around loader execution
await callLoader();
},
async action(
callAction,
{ params, request, context, pattern },
) {
// Runs around action execution
await callAction();
},
async middleware(
callMiddleware,
{ params, request, context, pattern },
) {
// Runs around middleware execution
await callMiddleware();
},
async lazy(callLazy) {
// Runs around lazy route loading
await callLazy();
},
});
},
},
];
```
### Read-only Design
Instrumentations are designed to be **observational only**. You cannot:
- Modify arguments passed to handlers
- Change return values from handlers
- Alter application behavior
This ensures that instrumentation is safe to add to production applications and cannot introduce bugs in your route logic.
### Error Handling
To ensure that instrumentation code doesn't impact the runtime application, errors are caught internally and prevented from propagating outward. This design choice shows up in 2 aspects.
First, if a "handler" function (loader, action, request handler, navigation, etc.) throws an error, that error will not bubble out of the `callHandler` function invoked from your instrumentation. Instead, the `callHandler` function returns a discriminated union result of type `{ type: "success", error: undefined } | { type: "error", error: unknown }`. This ensures your entire instrumentation function runs without needing any try/catch/finally logic to handle application errors.
```tsx
export const instrumentations = [
{
route(route) {
route.instrument({
async loader(callLoader) {
let { status, error } = await callLoader();
if (status === "error") {
// error case - `error` is defined
} else {
// success case - `error` is undefined
}
},
});
},
},
];
```
Second, if your instrumentation function throws an error, React Router will gracefully swallow that so that it does not bubble outward and impact other instrumentations or application behavior. In both of these examples, the handlers and all other instrumentation functions will still run:
```tsx
export const instrumentations = [
{
route(route) {
route.instrument({
// Throwing before calling the handler - RR will
// catch the error and still call the loader
async loader(callLoader) {
somethingThatThrows();
await callLoader();
},
// Throwing after calling the handler - RR will
// catch the error internally
async action(callAction) {
await callAction();
somethingThatThrows();
},
});
},
},
];
```
### Composition
You can compose multiple instrumentations by providing an array:
```tsx
export const instrumentations = [
loggingInstrumentation,
performanceInstrumentation,
errorReportingInstrumentation,
];
```
Each instrumentation wraps the previous one, creating a nested execution chain.
### Conditional Instrumentation
You can enable instrumentation conditionally based on environment or other factors:
```tsx
export const instrumentations =
process.env.NODE_ENV === "production"
? [productionInstrumentation]
: [developmentInstrumentation];
```
```tsx
// Or conditionally within an instrumentation
export const instrumentations = [
{
route(route) {
// Only instrument specific routes
if (!route.id?.startsWith("routes/admin")) return;
// Or, only instrument if a query parameter is present
let sp = new URL(request.url).searchParams;
if (!sp.has("DEBUG")) return;
route.instrument({
async loader() {
/* ... */
},
});
},
},
];
```
## Common Patterns
### Request logging (server)
```tsx
const logging: ServerInstrumentation = {
handler({ instrument }) {
instrument({
request: (fn, { request }) =>
log(`request ${request.url}`, fn),
});
},
route({ instrument, id }) {
instrument({
middleware: (fn) => log(` middleware (${id})`, fn),
loader: (fn) => log(` loader (${id})`, fn),
action: (fn) => log(` action (${id})`, fn),
});
},
};
async function log(
label: string,
cb: () => Promise<InstrumentationHandlerResult>,
) {
let start = Date.now();
console.log(`➡️ ${label}`);
await cb();
console.log(`⬅️ ${label} (${Date.now() - start}ms)`);
}
export const instrumentations = [logging];
```
### OpenTelemetry Integration
```tsx
import { trace, SpanStatusCode } from "@opentelemetry/api";
const tracer = trace.getTracer("my-app");
const otel: ServerInstrumentation = {
handler({ instrument }) {
instrument({
request: (fn, { request }) =>
otelSpan(`request`, { url: request.url }, fn),
});
},
route({ instrument, id }) {
instrument({
middleware: (fn, { pattern }) =>
otelSpan(
"middleware",
{ routeId: id, pattern: pattern },
fn,
),
loader: (fn, { pattern }) =>
otelSpan(
"loader",
{ routeId: id, pattern: pattern },
fn,
),
action: (fn, { pattern }) =>
otelSpan(
"action",
{ routeId: id, pattern: pattern },
fn,
),
});
},
};
async function otelSpan(
label: string,
attributes: Record<string, string>,
cb: () => Promise<InstrumentationHandlerResult>,
) {
return tracer.startActiveSpan(
label,
{ attributes },
async (span) => {
let { error } = await cb();
if (error) {
span.recordException(error);
span.setStatus({
code: SpanStatusCode.ERROR,
});
}
span.end();
},
);
}
export const instrumentations = [otel];
```
### Client-side Performance Tracking
```tsx
const windowPerf: ClientInstrumentation = {
router({ instrument }) {
instrument({
navigate: (fn, { to, currentUrl }) =>
measure(`navigation:${currentUrl}->${to}`, fn),
fetch: (fn, { href }) =>
measure(`fetcher:${href}`, fn),
});
},
route({ instrument, id }) {
instrument({
middleware: (fn) => measure(`middleware:${id}`, fn),
loader: (fn) => measure(`loader:${id}`, fn),
action: (fn) => measure(`action:${id}`, fn),
});
},
};
async function measure(
label: string,
cb: () => Promise<InstrumentationHandlerResult>,
) {
performance.mark(`start:${label}`);
await cb();
performance.mark(`end:${label}`);
performance.measure(
label,
`start:${label}`,
`end:${label}`,
);
}
<HydratedRouter instrumentations={[windowPerf]} />;
```
+40
View File
@@ -0,0 +1,40 @@
---
title: Meta Tags and SEO
hidden: true
---
[copy pasted from route module doc]
By default, meta descriptors will render a [`<meta>` tag][meta-element] in most cases. The two exceptions are:
- `{ title }` renders a `<title>` tag
- `{ "script:ld+json" }` renders a `<script type="application/ld+json">` tag, and its value should be a serializable object that is stringified and injected into the tag.
```tsx
export function meta() {
return [
{
"script:ld+json": {
"@context": "https://schema.org",
"@type": "Organization",
name: "React Router",
url: "https://reactrouter.com",
},
},
];
}
```
A meta descriptor can also render a [`<link>` tag][link-element] by setting the `tagName` property to `"link"`. This is useful for `<link>` tags associated with SEO like `canonical` URLs. For asset links like stylesheets and favicons, you should use the [`links` export][links] instead.
```tsx
export function meta() {
return [
{
tagName: "link",
rel: "canonical",
href: "https://reactrouter.com",
},
];
}
```
+763
View File
@@ -0,0 +1,763 @@
---
title: Middleware
---
# Middleware
[MODES: framework, data]
<br/>
<br/>
Middleware allows you to run code before and after the [`Response`][Response] generation for the matched path. This enables [common patterns][common-patterns] like authentication, logging, error handling, and data preprocessing in a reusable way.
Middleware runs in a nested chain, executing from parent routes to child routes on the way "down" to your route handlers, then from child routes back to parent routes on the way "up" after a [`Response`][Response] is generated.
For example, on a `GET /parent/child` request, the middleware would run in the following order:
```text
- Root middleware start
- Parent middleware start
- Child middleware start
- Run loaders, generate HTML Response
- Child middleware end
- Parent middleware end
- Root middleware end
```
<docs-info>There are some slight differences between middleware on the server (framework mode) versus the client (framework/data mode). For the purposes of this document, we'll be referring to Server Middleware in most of our examples as it's the most familiar to users who've used middleware in other HTTP servers in the past. Please refer to the [Server vs Client Middleware][server-client] section below for more information.</docs-info>
## Quick Start (Framework mode)
### 1. Enable the middleware flag
First, enable middleware in your [React Router config][rr-config]:
```ts filename=react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
future: {
v8_middleware: true,
},
} satisfies Config;
```
<docs-warning>By enabling the middleware feature, you change the type of the `context` parameter to your [`action`][framework-action]s and [`loader`][framework-loader]s. Please pay attention to the section on [`getLoadContext`][getloadcontext] below if you are actively using `context` today.</docs-warning>
### 2. Create a context
Middleware uses a `context` provider instance to provide data down the middleware chain.
You can create type-safe context objects using [`createContext`][createContext]:
```ts filename=app/context.ts
import { createContext } from "react-router";
import type { User } from "~/types";
export const userContext = createContext<User | null>(null);
```
### 3. Export middleware from your routes
```tsx filename=app/routes/dashboard.tsx
import { redirect } from "react-router";
import { userContext } from "~/context";
// Server-side Authentication Middleware
async function authMiddleware({ request, context }) {
const user = await getUserFromSession(request);
if (!user) {
throw redirect("/login");
}
context.set(userContext, user);
}
export const middleware: Route.MiddlewareFunction[] = [
authMiddleware,
];
// Client-side timing middleware
async function timingMiddleware({ context }, next) {
const start = performance.now();
await next();
const duration = performance.now() - start;
console.log(`Navigation took ${duration}ms`);
}
export const clientMiddleware: Route.ClientMiddlewareFunction[] =
[timingMiddleware];
export async function loader({
context,
}: Route.LoaderArgs) {
const user = context.get(userContext);
const profile = await getProfile(user);
return { profile };
}
export default function Dashboard({
loaderData,
}: Route.ComponentProps) {
return (
<div>
<h1>Welcome {loaderData.profile.fullName}!</h1>
<Profile profile={loaderData.profile} />
</div>
);
}
```
### 4. Update your `getLoadContext` function (if applicable)
If you're using a custom server and a `getLoadContext` function, you will need to update your implementation to return an instance of [`RouterContextProvider`][RouterContextProvider], instead of a JavaScript object:
```diff
+import {
+ createContext,
+ RouterContextProvider,
+} from "react-router";
import { createDb } from "./db";
+const dbContext = createContext<Database>();
function getLoadContext(req, res) {
- return { db: createDb() };
+ const context = new RouterContextProvider();
+ context.set(dbContext, createDb());
+ return context;
}
```
## Quick Start (Data Mode)
### 1. TypeScript: augment `Future` for loader/action `context`
In order to properly type your `context` param in your `loader`/`action`/`middleware` functions, you will need a small module augmentation to override the default context type of `any`:
```ts
// src/react-router.d.ts
import "react-router";
declare module "react-router" {
interface Future {
v8_middleware: true;
}
}
```
Without this, `context` stays loosely typed even when middleware is enabled at runtime.
### 2. Create a context
Middleware uses a `context` provider to pass data through the middleware chain into loaders and actions. Create typed context with [`createContext`][createContext]:
```ts
import { createContext } from "react-router";
import type { User } from "~/types";
export const userContext = createContext<User | null>(null);
```
### 3. Add `middleware` to route objects
Attach `middleware` arrays to your route objects:
```tsx
import {
redirect,
useLoaderData,
type LoaderFunctionArgs,
} from "react-router";
import { userContext } from "~/context";
const routes = [
{
path: "/",
middleware: [timingMiddleware], // 👈
Component: Root,
children: [
{
path: "dashboard",
middleware: [authMiddleware], // 👈
loader: dashboardLoader,
Component: Dashboard,
},
{
path: "login",
Component: Login,
},
],
},
];
async function timingMiddleware({ context }, next) {
const start = performance.now();
await next();
const duration = performance.now() - start;
console.log(`Navigation took ${duration}ms`);
}
async function authMiddleware({ context }) {
const user = await getUser();
if (!user) {
throw redirect("/login");
}
context.set(userContext, user);
}
export async function dashboardLoader({
context,
}: LoaderFunctionArgs) {
const user = context.get(userContext);
const profile = await getProfile(user);
return { profile };
}
export default function Dashboard() {
let loaderData = useLoaderData();
return (
<div>
<h1>Welcome {loaderData.profile.fullName}!</h1>
<Profile profile={loaderData.profile} />
</div>
);
}
```
### 4. Add a `getContext` function (optional)
To seed every navigation or fetcher call with shared values, pass [`getContext`][getContext] when creating the router:
```tsx
let sessionContext = createContext();
const router = createBrowserRouter(routes, {
getContext() {
let context = new RouterContextProvider();
context.set(sessionContext, getSession());
return context;
},
});
```
<docs-info>This mirrors Framework modes server-side [`getLoadContext`][getloadcontext]. In the browser, root `middleware` can often do the same job, but `getContext` is available when you want to seed every request up front.</docs-info>
## Core Concepts
### Server vs Client Middleware
Server middleware runs on the server in Framework mode for HTML Document requests and `.data` requests for subsequent navigations and fetcher calls. Because server middleware runs on the server in response to an HTTP [`Request`][request], it returns an HTTP [`Response`][Response] back up the middleware chain via the `next` function:
```ts
async function serverMiddleware({ request }, next) {
console.log(request.method, request.url);
let response = await next();
console.log(response.status, request.method, request.url);
return response;
}
// Framework mode only
export const middleware: Route.MiddlewareFunction[] = [
serverMiddleware,
];
```
Client middleware runs in the browser in framework and data mode for client-side navigations and fetcher calls. Client middleware differs from server middleware because there's no HTTP Request, so it doesn't have a `Response` to bubble up. In most cases, you can just ignore the return value from `next` and return nothing from your middleware on the client:
```ts
async function clientMiddleware({ request }, next) {
console.log(request.method, request.url);
await next();
console.log(`Finished ${request.method} ${request.url}`);
}
// Framework mode
export const clientMiddleware: Route.ClientMiddlewareFunction[] =
[clientMiddleware];
// Or, Data mode
const route = {
path: "/",
middleware: [clientMiddleware],
loader: rootLoader,
Component: Root,
};
```
There may be _some_ cases where you want to do some post-processing based on the result of the loaders/action. In lieu of a `Response`, client middleware bubbles up the value returned from the active [`dataStrategy`][datastrategy] (`Record<string, DataStrategyResult>` - keyed by route id). This allows you to take conditional action in your middleware based on the outcome of the executed `loader`/`action` functions.
Here's an example of the [CMS Redirect on 404][cms-redirect] use case implemented as a client side middleware:
```tsx
async function cmsFallbackMiddleware({ request }, next) {
const results = await next();
// Check if we got a 404 from any of our routes and if so, look for a
// redirect in our CMS
const found404 = Object.values(results).some(
(r) =>
isRouteErrorResponse(r.result) &&
r.result.status === 404,
);
if (found404) {
const cmsRedirect = await checkCMSRedirects(
request.url,
);
if (cmsRedirect) {
throw redirect(cmsRedirect, 302);
}
}
}
```
<docs-warning>In a server middleware, you shouldn't be messing with the `Response` body and should only be reading status/headers and setting headers. Similarly, this value should be considered read-only in client middleware because it represents the "body" or "data" for the resulting navigation which should be driven by loaders/actions - not middleware. This also means that in client middleware, there's usually no need to return the results even if you needed to capture it from `await next()`;</docs-warning>
### When Middleware Runs
It is very important to understand _when_ your middlewares will run to make sure your application is behaving as you intend.
#### Server Middleware
In a hydrated Framework Mode app, server middleware is designed such that it prioritizes SPA behavior and does not create new network activity by default. Middleware wraps _existing_ requests and only runs when you _need_ to hit the server.
This raises the question of what is a "handler" in React Router? Is it the route? Or the `loader`? We think "it depends":
- On document requests (`GET /route`), the handler is the route — because the response encompasses both the `loader` and the route component
- On data requests (`GET /route.data`) for client-side navigations, the handler is the [`action`][data-action]/[`loader`][data-loader], because that's all that is included in the response
Therefore:
- Document requests run server middleware whether `loader`s exist or not because we're still in a "handler" to render the UI
- Client-side navigations will only run server middleware if a `.data` request is made to the server for a [`action`][framework-action]/[`loader`][framework-loader]
This is important behavior for request-annotation middlewares such as logging request durations, checking/setting sessions, setting outgoing caching headers, etc. It would be useless to go to the server and run those types of middlewares when there was no reason to go to the server in the first place. This would result in increased server load and noisy server logs.
```tsx filename=app/root.tsx
// This middleware won't run on client-side navigations without a `.data` request
async function loggingMiddleware({ request }, next) {
console.log(`Request: ${request.method} ${request.url}`);
let response = await next();
console.log(
`Response: ${response.status} ${request.method} ${request.url}`,
);
return response;
}
export const middleware: Route.MiddlewareFunction[] = [
loggingMiddleware,
];
```
However, there may be cases where you _want_ to run certain server middlewares on _every_ client-navigation - even if no `loader` exists. For example, a form in the authenticated section of your site that doesn't require a `loader` but you'd rather use auth middleware to redirect users away before they fill out the form — rather than when they submit to the `action`. If your middleware meets these criteria, then you can put a `loader` on the route that contains the middleware to force it to always call the server for client-side navigations involving that route.
```tsx filename=app/_auth.tsx
function authMiddleware({ request }, next) {
if (!isLoggedIn(request)) {
throw redirect("/login");
}
}
export const middleware: Route.MiddlewareFunction[] = [
authMiddleware,
];
// By adding a `loader`, we force the `authMiddleware` to run on every
// client-side navigation involving this route.
export async function loader() {
return null;
}
```
#### Client Middleware
Client middleware is simpler because since we are already on the client and are always making a "request" to the router when navigating. Client middlewares will run on every client navigation, regardless of whether there are `loader`s to run.
### Context API
The new context system provides type safety and prevents naming conflicts and allows you to provide data to nested middlewares and `action`/`loader` functions. In Framework Mode, this replaces the previous `AppLoadContext` API.
```ts
// ✅ Type-safe
import { createContext } from "react-router";
const userContext = createContext<User>();
// Later in middleware/`loader`s
context.set(userContext, user); // Must be `User` type
const user = context.get(userContext); // Returns `User` type
// ❌ Old way (no type safety)
context.user = user; // Could be anything
```
#### `Context` and `AsyncLocalStorage`
Node provides an [`AsyncLocalStorage`][asynclocalstorage] API which gives you a way to provide values through asynchronous execution contexts. While this is a Node API, most modern runtimes have made it (mostly) available (i.e., [Cloudflare][cloudflare], [Bun][bun], [Deno][deno]).
In theory, we could have leveraged [`AsyncLocalStorage`][asynclocalstorage] directly as the way to pass values from middlewares to child routes, but the lack of 100% cross-platform compatibility was concerning enough that we wanted to still ship a first-class `context` API so there would be a way to publish reusable middleware packages guaranteed to work in a runtime-agnostic manner.
That said, this API still works great with React Router middleware and can be used in place of, or alongside of the `context` API:
<docs-info>[`AsyncLocalStorage`][asynclocalstorage] is _especially_ powerful when using [React Server Components](../how-to/react-server-components) because it allows you to provide information from `middleware` to your Server Components and Server Actions because they run in the same server execution context 🤯</docs-info>
```tsx filename=app/user-context.ts
import { AsyncLocalStorage } from "node:async_hooks";
const USER = new AsyncLocalStorage<User>();
export async function provideUser(
request: Request,
cb: () => Promise<Response>,
) {
let user = await getUser(request);
return USER.run(user, cb);
}
export function getUser() {
return USER.getStore();
}
```
```tsx filename=app/root.tsx
import { provideUser } from "./user-context";
export const middleware: Route.MiddlewareFunction[] = [
async ({ request, context }, next) => {
return provideUser(request, async () => {
let res = await next();
return res;
});
},
];
```
```tsx filename=app/routes/_index.tsx
import { getUser } from "../user-context";
export async function loader() {
let user = getUser();
//...
}
```
### The `next` function
The `next` function logic depends on which route middleware it's being called from:
- When called from a non-leaf middleware, it runs the next middleware in the chain
- When called from the leaf middleware, it executes any route handlers and generates the resulting [`Response`][Response] for the request
```ts
const middleware = async ({ context }, next) => {
// Code here runs BEFORE handlers
console.log("Before");
const response = await next();
// Code here runs AFTER handlers
console.log("After");
return response; // Optional on client, required on server
};
```
<docs-warning>You can only call `next()` once per middleware. Calling it multiple times will throw an error</docs-warning>
### Skipping `next()`
If you don't need to run code after your handlers, you can skip calling `next()`:
```ts
const authMiddleware = async ({ request, context }) => {
const user = await getUser(request);
if (!user) {
throw redirect("/login");
}
context.set(userContext, user);
// next() is called automatically
};
```
### `next()` and Error Handling
React Router contains built-in error handling via the route [`ErrorBoundary`][ErrorBoundary] export. Just like when a `action`/`loader` throws, if a `middleware` throws it will be caught and handled at the appropriate [`ErrorBoundary`][ErrorBoundary] and a [`Response`][Response] will be returned through the ancestor `next()` call. This means that the `next()` function should never throw and should always return a [`Response`][Response], so you don't need to worry about wrapping it in a try/catch.
This behavior is important to allow middleware patterns such as automatically setting required headers on outgoing responses (i.e., committing a session) from a root `middleware`. If any error from a `middleware` caused `next()` to `throw`, we'd miss the execution of ancestor middlewares on the way out and those required headers wouldn't be set.
```tsx filename=routes/parent.tsx
export const middleware: Route.MiddlewareFunction[] = [
async (_, next) => {
let res = await next();
// ^ res.status = 500
// This response contains the ErrorBoundary
return res;
},
];
```
```tsx filename=routes/parent.child.tsx
export const middleware: Route.MiddlewareFunction[] = [
async (_, next) => {
let res = await next();
// ^ res.status = 200
// This response contains the successful UI render
throw new Error("Uh oh, something went wrong!");
},
];
```
Which `ErrorBoundary` is rendered will differ based on whether your middleware threw _before_ or _after_ calling then `next()` function. If it throws _after_ then it will bubble up from the throwing route just like a normal loader error because we've already run the loaders and have the appropriate `loaderData` to render in the route components. However, if an error is thrown _before_ calling `next()`, then we haven't called any loaders yet and there is no `loaderData` available. When this happens, we must bubble up to the highest route with a `loader` and start looking for an `ErrorBoundary` there. We cannot render any route components at that level or below without any `loaderData`.
## Changes to `getLoadContext`/`AppLoadContext`
<docs-info>This only applies if you are using a custom server and a custom `getLoadContext` function</docs-info>
Middleware introduces a breaking change to the `context` parameter generated by `getLoadContext` and passed to your `action`s and `loader`s. The current approach of a module-augmented `AppLoadContext` isn't really type-safe and instead just sort of tells TypeScript to "trust me".
Middleware needs an equivalent `context` on the client for `clientMiddleware`, but we didn't want to duplicate this pattern from the server that we already weren't thrilled with, so we decided to introduce a new API where we could tackle type-safety.
When opting into middleware, the `context` parameter changes to an instance of [`RouterContextProvider`][RouterContextProvider]:
```ts
let dbContext = createContext<Database>();
let context = new RouterContextProvider();
context.set(dbContext, getDb());
// ^ type-safe
let db = context.get(dbContext);
// ^ Database
```
If you're using a custom server and a `getLoadContext` function, you will need to update your implementation to return an instance of [`RouterContextProvider`][RouterContextProvider], instead of a plain JavaScript object:
```diff
+import {
+ createContext,
+ RouterContextProvider,
+} from "react-router";
import { createDb } from "./db";
+const dbContext = createContext<Database>();
function getLoadContext(req, res) {
- return { db: createDb() };
+ const context = new RouterContextProvider();
+ context.set(dbContext, createDb());
+ return context;
}
```
### Migration from `AppLoadContext`
If you're currently using `AppLoadContext`, you can migrate incrementally by using your existing module augmentation to augment [`RouterContextProvider`][RouterContextProvider] instead of `AppLoadContext`. Then, update your `getLoadContext` function to return an instance of [`RouterContextProvider`][RouterContextProvider]:
```diff
declare module "react-router" {
- interface AppLoadContext {
+ interface RouterContextProvider {
db: Database;
user: User;
}
}
function getLoadContext() {
const loadContext = {...};
- return loadContext;
+ let context = new RouterContextProvider();
+ Object.assign(context, loadContext);
+ return context;
}
```
This allows you to leave your `action`s/`loader`s untouched during initial adoption of middleware, since they can still read values directly (i.e., `context.db`).
<docs-warning>This approach is only intended to be used as a migration strategy when adopting middleware in React Router v7, allowing you to incrementally migrate to `context.set`/`context.get`. It is not safe to assume this approach will work in the next major version of React Router.</docs-warning>
<docs-warning>The [`RouterContextProvider`][RouterContextProvider] class is also used for the client-side `context` parameter via `<HydratedRouter getContext>` and `<RouterProvider getContext>`. Since `AppLoadContext` is primarily intended as a hand-off from your HTTP server into the React Router handlers, you need to be aware that these augmented fields will not be available in `clientMiddleware`, `clientLoader`, or `clientAction` functions even thought TypeScript will tell you they are (unless, of course, you provide the fields via `getContext` on the client).</docs-warning>
## Common Patterns
### Authentication
```tsx filename=app/middleware/auth.ts
import { redirect } from "react-router";
import { userContext } from "~/context";
import { getSession } from "~/sessions.server";
export const authMiddleware = async ({
request,
context,
}) => {
const session = await getSession(request);
const userId = session.get("userId");
if (!userId) {
throw redirect("/login");
}
const user = await getUserById(userId);
context.set(userContext, user);
};
```
```tsx filename=app/routes/protected.tsx
import { authMiddleware } from "~/middleware/auth";
export const middleware: Route.MiddlewareFunction[] = [
authMiddleware,
];
export async function loader({
context,
}: Route.LoaderArgs) {
const user = context.get(userContext); // Guaranteed to exist
return { user };
}
```
### Logging
```tsx filename=app/middleware/logging.ts
import { requestIdContext } from "~/context";
export const loggingMiddleware = async (
{ request, context },
next,
) => {
const requestId = crypto.randomUUID();
context.set(requestIdContext, requestId);
console.log(
`[${requestId}] ${request.method} ${request.url}`,
);
const start = performance.now();
const response = await next();
const duration = performance.now() - start;
console.log(
`[${requestId}] Response ${response.status} (${duration}ms)`,
);
return response;
};
```
### CMS Redirect on 404
```tsx filename=app/middleware/cms-fallback.ts
export const cmsFallbackMiddleware = async (
{ request },
next,
) => {
const response = await next();
// Check if we got a 404
if (response.status === 404) {
// Check CMS for a redirect
const cmsRedirect = await checkCMSRedirects(
request.url,
);
if (cmsRedirect) {
throw redirect(cmsRedirect, 302);
}
}
return response;
};
```
### Response Headers
```tsx filename=app/middleware/headers.ts
export const headersMiddleware = async (
{ context },
next,
) => {
const response = await next();
// Add security headers
response.headers.set("X-Frame-Options", "DENY");
response.headers.set("X-Content-Type-Options", "nosniff");
return response;
};
```
### Conditional Middleware
```tsx
export const middleware: Route.MiddlewareFunction[] = [
async ({ request, context }, next) => {
// Only run auth for POST requests
if (request.method === "POST") {
await ensureAuthenticated(request, context);
}
return next();
},
];
```
### Sharing Context Between `action` and `loader`
<docs-info>On the server, this approach only works for document POST requests because `context` is scoped to a request. SPA navigation submissions use separate POST/GET requests so you cannot share `context` between them. This pattern always works in `clientMiddleware`/`clientLoader`/`clientAction` because there's no separate HTTP requests.</docs-info>
```tsx
const sharedDataContext = createContext<any>();
export const middleware: Route.MiddlewareFunction[] = [
async ({ request, context }, next) => {
// Set data if it doesn't exist
// This will only run once for document requests
// It will run twice (action request + loader request) in SPA submissions
if (!context.get(sharedDataContext)) {
context.set(
sharedDataContext,
await getExpensiveData(),
);
}
return next();
},
];
export async function action({
context,
}: Route.ActionArgs) {
const data = context.get(sharedDataContext);
// Use the data...
}
export async function loader({
context,
}: Route.LoaderArgs) {
const data = context.get(sharedDataContext);
// Same data is available here
}
```
[future-flags]: ../upgrading/future
[Response]: https://developer.mozilla.org/en-US/docs/Web/API/Response
[common-patterns]: #common-patterns
[server-client]: #server-vs-client-middleware
[rr-config]: ../api/framework-conventions/react-router.config.ts
[create-browser-router]: ../api/data-routers/createBrowserRouter
[create-hash-router]: ../api/data-routers/createHashRouter
[create-memory-router]: ../api/data-routers/createMemoryRouter
[create-static-handler]: ../api/data-routers/createStaticHandler
[framework-action]: ../start/framework/route-module#action
[framework-loader]: ../start/framework/route-module#loader
[getloadcontext]: #changes-to-getloadcontextapploadcontext
[datastrategy]: ../api/data-routers/createBrowserRouter#optsdatastrategy
[cms-redirect]: #cms-redirect-on-404
[createContext]: ../api/utils/createContext
[RouterContextProvider]: ../api/utils/RouterContextProvider
[getContext]: ../api/data-routers/createBrowserRouter#optsgetContext
[window]: https://developer.mozilla.org/en-US/docs/Web/API/Window
[document]: https://developer.mozilla.org/en-US/docs/Web/API/Document
[request]: https://developer.mozilla.org/en-US/docs/Web/API/Request
[data-action]: ../start/data/route-object#action
[data-loader]: ../start/data/route-object#loader
[asynclocalstorage]: https://nodejs.org/api/async_context.html#class-asynclocalstorage
[cloudflare]: https://developers.cloudflare.com/workers/runtime-apis/nodejs/asynclocalstorage/
[bun]: https://bun.sh/blog/bun-v0.7.0#asynclocalstorage-support
[deno]: https://docs.deno.com/api/node/async_hooks/~/AsyncLocalStorage
[ErrorBoundary]: ../start/framework/route-module#errorboundary
+233
View File
@@ -0,0 +1,233 @@
---
title: Navigation Blocking
---
# Navigation Blocking
[MODES: framework, data]
<br/>
<br/>
When users are in the middle of a workflow, like filling out an important form, you may want to prevent them from navigating away from the page.
This example will show:
- Setting up a route with a form and action called with a fetcher
- Blocking navigation when the form is dirty
- Showing a confirmation when the user tries to leave the page
## 1. Set up a route with a form
Add a route with the form, we'll use a "contact" route for this example:
```ts filename=routes.ts
import {
type RouteConfig,
index,
route,
} from "@react-router/dev/routes";
export default [
index("routes/home.tsx"),
route("contact", "routes/contact.tsx"),
] satisfies RouteConfig;
```
Add the form to the contact route module:
```tsx filename=routes/contact.tsx
import { useFetcher } from "react-router";
import type { Route } from "./+types/contact";
export async function action({
request,
}: Route.ActionArgs) {
let formData = await request.formData();
let email = formData.get("email");
let message = formData.get("message");
console.log(email, message);
return { ok: true };
}
export default function Contact() {
let fetcher = useFetcher();
return (
<fetcher.Form method="post">
<p>
<label>
Email: <input name="email" type="email" />
</label>
</p>
<p>
<textarea name="message" />
</p>
<p>
<button type="submit">
{fetcher.state === "idle" ? "Send" : "Sending..."}
</button>
</p>
</fetcher.Form>
);
}
```
## 2. Add dirty state and onChange handler
To track the dirty state of the form, we'll use a single boolean and a quick form onChange handler. You may want to track the dirty state differently but this works for this guide.
```tsx filename=routes/contact.tsx lines=[2,8-12]
export default function Contact() {
let [isDirty, setIsDirty] = useState(false);
let fetcher = useFetcher();
return (
<fetcher.Form
method="post"
onChange={(event) => {
let email = event.currentTarget.email.value;
let message = event.currentTarget.message.value;
setIsDirty(Boolean(email || message));
}}
>
{/* existing code */}
</fetcher.Form>
);
}
```
## 3. Block navigation when the form is dirty
```tsx filename=routes/contact.tsx lines=[1,6-8]
import { useBlocker } from "react-router";
export default function Contact() {
let [isDirty, setIsDirty] = useState(false);
let fetcher = useFetcher();
let blocker = useBlocker(
useCallback(() => isDirty, [isDirty]),
);
// ... existing code
}
```
While this will now block a navigation, there's no way for the user to confirm it.
## 4. Show confirmation UI
This uses a simple div, but you may want to use a modal dialog.
```tsx filename=routes/contact.tsx lines=[19-41]
export default function Contact() {
let [isDirty, setIsDirty] = useState(false);
let fetcher = useFetcher();
let blocker = useBlocker(
useCallback(() => isDirty, [isDirty]),
);
return (
<fetcher.Form
method="post"
onChange={(event) => {
let email = event.currentTarget.email.value;
let message = event.currentTarget.message.value;
setIsDirty(Boolean(email || message));
}}
>
{/* existing code */}
{blocker.state === "blocked" && (
<div>
<p>Wait! You didn't send the message yet:</p>
<p>
<button
type="button"
onClick={() => blocker.proceed()}
>
Leave
</button>{" "}
<button
type="button"
onClick={() => blocker.reset()}
>
Stay here
</button>
</p>
</div>
)}
</fetcher.Form>
);
}
```
If the user clicks "leave" then `blocker.proceed()` will proceed with the navigation. If they click "stay here" then `blocker.reset()` will clear the blocker and keep them on the current page.
## 5. Reset the blocker when the action resolves
If the user doesn't click either "leave" or "stay here", then submits the form, the blocker will still be active. Let's reset the blocker when the action resolves with an effect.
```tsx filename=routes/contact.tsx
useEffect(() => {
if (fetcher.data?.ok) {
if (blocker.state === "blocked") {
blocker.reset();
}
}
}, [fetcher.data]);
```
## 6. Clear the form when the action resolves
While unrelated to navigation blocking, let's clear the form when the action resolves with a ref.
```tsx
let formRef = useRef<HTMLFormElement>(null);
// put it on the form
<fetcher.Form
ref={formRef}
method="post"
onChange={(event) => {
// ... existing code
}}
>
{/* existing code */}
</fetcher.Form>;
```
```tsx
useEffect(() => {
if (fetcher.data?.ok) {
// clear the form in the effect
formRef.current?.reset();
if (blocker.state === "blocked") {
blocker.reset();
}
}
}, [fetcher.data]);
```
Alternatively, if a navigation is currently blocked, instead of resetting the blocker, you can proceed through to the blocked navigation.
```tsx
useEffect(() => {
if (fetcher.data?.ok) {
if (blocker.state === "blocked") {
// proceed with the blocked navigation
blocker.proceed();
} else {
formRef.current?.reset();
}
}
}, [fetcher.data]);
```
In this case the user flow is:
- User fills out the form
- User forgets to click "send" and clicks a link instead
- The navigation is blocked, and the confirmation message is shown
- Instead of clicking "leave" or "stay here", the user submits the form
- The user is taken to the requested page
@@ -0,0 +1,12 @@
---
title: Revalidation Optimization
hidden: true
---
[copy pasted]
During client-side transitions, React Router will optimize reloading of routes that are already rendering, like not reloading layout routes that aren't changing. In other cases, like form submissions or search param changes, React Router doesn't know which routes need to be reloaded, so it reloads them all to be safe. This ensures your UI always stays in sync with the state on your server.
This function lets apps further optimize by returning `false` when React Router is about to reload a route. If you define this function on a route module, React Router will defer to your function on every navigation and every revalidation after an action is called. Again, this makes it possible for your UI to get out of sync with your server if you do it wrong, so be careful.
`fetcher.load` calls also revalidate, but because they load a specific URL, they don't have to worry about route param or URL search param revalidations. `fetcher.load`'s only revalidate by default after action submissions and explicit revalidation requests via [`useRevalidator`][use-revalidator].
+225
View File
@@ -0,0 +1,225 @@
---
title: Pre-Rendering
---
# Pre-Rendering
[MODES: framework]
<br/>
<br/>
Pre-Rendering allows you to speed up page loads for static content by rendering pages at build time instead of at runtime.
## Configuration
Pre-rendering is enabled via the `prerender` config in `react-router.config.ts`.
The simplest configuration is a boolean `true` which will pre-render all off the applications static paths based on `routes.ts`:
```ts filename=react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
prerender: true,
} satisfies Config;
```
The boolean `true` will not include any dynamic paths (i.e., `/blog/:slug`) because the parameter values are unknown.
To configure specific paths including dynamic values, you can specify an array of paths:
```ts filename=react-router.config.ts
import type { Config } from "@react-router/dev/config";
let slugs = getPostSlugs();
export default {
prerender: [
"/",
"/blog",
...slugs.map((s) => `/blog/${s}`),
],
} satisfies Config;
```
If you need to perform more complex and/or asynchronous logic to determine the paths, you can also provide a function that returns an array of paths. This function provides you with a `getStaticPaths` method you can use to avoid manually adding all of the static paths in your application:
```ts filename=react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
async prerender({ getStaticPaths }) {
let slugs = await getPostSlugsFromCMS();
return [
...getStaticPaths(), // "/" and "/blog"
...slugs.map((s) => `/blog/${s}`),
];
},
} satisfies Config;
```
### Concurrency
By default, pages are pre-rendered one path at a time. You can enable concurrency to pre-render multiple paths in parallel which can speed up build times in many cases. You should experiment with the value that provides the best performance for your app.
To specify concurrency, move your `prerender` config down into a `prerender.paths` field and you can specify the concurrency in `prerender.concurrency`:
```ts filename=react-router.config.ts
import type { Config } from "@react-router/dev/config";
let slugs = getPostSlugs();
export default {
prerender: {
paths: [
"/",
"/blog",
...slugs.map((s) => `/blog/${s}`),
],
concurrency: 4,
},
} satisfies Config;
```
## Pre-Rendering with/without a Runtime Server
Pre-Rendering can be used in two ways based on the `ssr` config value:
- Alongside a runtime SSR server with `ssr:true` (the default value)
- Deployed to a static file server with `ssr:false`
### Pre-rendering with `ssr:true`
When pre-rendering with `ssr:true`, you're indicating you will still have a runtime server but you are choosing to pre-render certain paths for quicker Response times.
```ts filename=react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
// Can be omitted - defaults to true
ssr: true,
prerender: ["/", "/blog", "/blog/popular-post"],
} satisfies Config;
```
#### Data Loading and Pre-rendering
There is no extra application API for pre-rendering. Routes being pre-rendered use the same route `loader` functions as server rendering:
```tsx
export async function loader({ request, params }) {
let post = await getPost(params.slug);
return post;
}
export function Post({ loaderData }) {
return <div>{loaderData.title}</div>;
}
```
Instead of a request coming to your route on a deployed server, the build creates a `new Request()` and runs it through your app just like a server would.
When server rendering, requests to paths that have not been pre-rendered will be server rendered as usual.
#### Static File Output
The rendered result will be written out to your `build/client` directory. You'll notice two files for each path:
- `[url].html` HTML file for initial document requests
- `[url].data` file for client side navigation browser requests
The output of your build will indicate what files were pre-rendered:
```sh
> react-router build
vite v5.2.11 building for production...
...
vite v5.2.11 building SSR bundle for production...
...
Prerender: Generated build/client/index.html
Prerender: Generated build/client/blog.data
Prerender: Generated build/client/blog/index.html
Prerender: Generated build/client/blog/my-first-post.data
Prerender: Generated build/client/blog/my-first-post/index.html
...
```
During development, pre-rendering doesn't save the rendered results to the public directory, this only happens for `react-router build`.
### Pre-rendering with `ssr:false`
The above examples assume you are deploying a runtime server but are pre-rendering some static pages to avoid hitting the server, resulting in faster loads.
To disable runtime SSR and configure pre-rendering to be served from a static file server, you can set the `ssr:false` config flag:
```ts filename=react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
ssr: false, // disable runtime server rendering
prerender: true, // pre-render all static routes
} satisfies Config;
```
If you specify `ssr:false` without a `prerender` config, React Router refers to that as [SPA Mode](./spa). In SPA Mode, we render a single HTML file that is capable of hydrating for _any_ of your application paths. It can do this because it only renders the `root` route into the HTML file and then determines which child routes to load based on the browser URL during hydration. This means you can use a `loader` on the root route, but not on any other routes because we don't know which routes to load until hydration in the browser.
If you want to pre-render paths with `ssr:false`, those matched routes _can_ have loaders because we'll pre-render all of the matched routes for those paths, not just the root. You cannot include `actions` or `headers` functions in any routes when `ssr:false` is set because there will be no runtime server to run them on.
#### Pre-rendering with a SPA Fallback
If you want `ssr:false` but don't want to pre-render _all_ of your routes - that's fine too! You may have some paths where you need the performance/SEO benefits of pre-rendering, but other pages where a SPA would be fine.
You can do this using the combination of config options as well - just limit your `prerender` config to the paths that you want to pre-render and React Router will also output a "SPA Fallback" HTML file that can be served to hydrate any other paths (using the same approach as [SPA Mode](./spa)).
This will be written to one of the following paths:
- `build/client/index.html` - If the `/` path is not pre-rendered
- `build/client/__spa-fallback.html` - If the `/` path is pre-rendered
```ts filename=react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
ssr: false,
// SPA fallback will be written to build/client/index.html
prerender: ["/about-us"],
// SPA fallback will be written to build/client/__spa-fallback.html
prerender: ["/", "/about-us"],
} satisfies Config;
```
You can configure your deployment server to serve this file for any path that otherwise would 404. Some hosts do this by default, but others don't. As an example, a host may support a `_redirects` file to do this:
```
# If you did not pre-render the `/` route
/* /index.html 200
# If you pre-rendered the `/` route
/* /__spa-fallback.html 200
```
If you're getting 404s at valid routes for your app, it's likely you need to configure your host.
Here's another example of how you can do this with the [`sirv-cli`](https://www.npmjs.com/package/sirv-cli#user-content-single-page-applications) tool:
```sh
# If you did not pre-render the `/` route
sirv-cli build/client --single index.html
# If you pre-rendered the `/` route
sirv-cli build/client --single __spa-fallback.html
```
#### Invalid Exports
When pre-rendering with `ssr:false`, React Router will error at build time if you have invalid exports to help prevent some mistakes that can be easily overlooked.
- `headers`/`action` functions are prohibited in all routes because there will be no runtime server on which to run them
- When using `ssr:false` without a `prerender` config (SPA Mode), a `loader` is permitted on the root route only
- When using `ssr:false` with a `prerender` config, a `loader` is permitted on any route matched by a `prerender` path
- If you are using a `loader` on a pre-rendered route that has child routes, you will need to make sure the parent `loaderData` can be determined at run-time properly by either:
- Pre-rendering all child routes so that the parent `loader` can be called at build-time for each child route path and rendered into a `.data` file, or
- Use a `clientLoader` on the parent that can be called at run-time for non-pre-rendered child paths
+103
View File
@@ -0,0 +1,103 @@
---
title: Presets
---
# Presets
[MODES: framework]
<br/>
<br/>
The [React Router config][react-router-config] supports a `presets` option to ease integration with other tools and hosting providers.
[Presets][preset-type] can only do two things:
- Configure React Router config options on your behalf
- Validate the resolved config
The config returned by each preset is merged in the order the presets were defined. Any config directly specified in your React Router config will be merged last. This means that your config will always take precedence over any presets.
## Defining preset config
As a basic example, let's create a preset that configures a [server bundles function][server-bundles]:
```ts filename=my-cool-preset.ts
import type { Preset } from "@react-router/dev/config";
export function myCoolPreset(): Preset {
return {
name: "my-cool-preset",
reactRouterConfig: () => ({
serverBundles: ({ branch }) => {
const isAuthenticatedRoute = branch.some((route) =>
route.id.split("/").includes("_authenticated"),
);
return isAuthenticatedRoute
? "authenticated"
: "unauthenticated";
},
}),
};
}
```
## Validating config
Keep in mind that other presets and user config can still override the values returned from your preset.
In our example preset, the `serverBundles` function could be overridden with a different, conflicting implementation. If we want to validate that the final resolved config contains the `serverBundles` function from our preset, we can use the `reactRouterConfigResolved` hook:
```ts filename=my-cool-preset.ts lines=[22-27]
import type {
Preset,
ServerBundlesFunction,
} from "@react-router/dev/config";
const serverBundles: ServerBundlesFunction = ({
branch,
}) => {
const isAuthenticatedRoute = branch.some((route) =>
route.id.split("/").includes("_authenticated"),
);
return isAuthenticatedRoute
? "authenticated"
: "unauthenticated";
};
export function myCoolPreset(): Preset {
return {
name: "my-cool-preset",
reactRouterConfig: () => ({ serverBundles }),
reactRouterConfigResolved: ({ reactRouterConfig }) => {
if (
reactRouterConfig.serverBundles !== serverBundles
) {
throw new Error("`serverBundles` was overridden!");
}
},
};
}
```
The `reactRouterConfigResolved` hook should only be used when it would be an error to merge or override your preset's config.
## Using a preset
Presets are designed to be published to npm and used within your React Router config.
```ts filename=react-router.config.ts lines=[6]
import type { Config } from "@react-router/dev/config";
import { myCoolPreset } from "react-router-preset-cool";
export default {
// ...
presets: [myCoolPreset()],
} satisfies Config;
```
[react-router-config]: https://api.reactrouter.com/v7/types/_react-router_dev.config.Config.html
[preset-type]: https://api.reactrouter.com/v7/types/_react-router_dev.config.Preset.html
[server-bundles]: ./server-bundles
@@ -0,0 +1,899 @@
---
title: React Server Components
unstable: true
---
# React Server Components
[MODES: framework, data]
<br/>
<br/>
<docs-warning>React Server Components support is experimental and subject to breaking changes in
minor/patch releases. Please use with caution and pay **very** close attention
to release notes for relevant changes.</docs-warning>
React Server Components (RSC) refers generally to an architecture and set of APIs provided by React since version 19.
From the docs:
> Server Components are a new type of Component that renders ahead of time, before bundling, in an environment separate from your client app or SSR server.
>
> <cite>- [React "Server Components" docs][react-server-components-doc]</cite>
React Router provides a set of APIs for integrating with RSC-compatible bundlers, allowing you to leverage [Server Components][react-server-components-doc] and [Server Functions][react-server-functions-doc] in your React Router applications.
If you're unfamiliar with these React features, we recommend reading the official [Server Components documentation][react-server-components-doc] before using React Router's RSC APIs.
RSC support is available in both Framework and Data Modes. For more information on the conceptual difference between these, see ["Picking a Mode"][picking-a-mode]. However, note that the APIs and features differ between RSC and non-RSC modes in ways that this guide will cover in more detail.
## Quick Start
The quickest way to get started is with one of our templates.
These templates come with React Router RSC APIs already configured, offering you out of the box features such as:
- Server Side Rendering (SSR)
- Server Components
- Client Components (via [`"use client"`][use-client-docs] directive)
- Server Functions (via [`"use server"`][use-server-docs] directive)
### RSC Framework Mode Template
The [RSC Framework Mode template][framework-rsc-template] uses the unstable React Router RSC Vite plugin along with the experimental [`@vitejs/plugin-rsc` plugin][vite-plugin-rsc].
```shellscript
npx create-react-router@latest --template remix-run/react-router-templates/unstable_rsc-framework-mode
```
### RSC Data Mode Templates
The [Vite RSC Data Mode template][vite-rsc-template] uses the experimental Vite `@vitejs/plugin-rsc` plugin.
```shellscript
npx create-react-router@latest --template remix-run/react-router-templates/unstable_rsc-data-mode-vite
```
## RSC Framework Mode
Most APIs and features in RSC Framework Mode are the same as non-RSC Framework Mode, so this guide will focus on the differences.
### New React Router RSC Vite Plugin
RSC Framework Mode uses a different Vite plugin than non-RSC Framework Mode, currently exported as `unstable_reactRouterRSC`.
This new Vite plugin also has a peer dependency on the experimental `@vitejs/plugin-rsc` plugin. Note that the `@vitejs/plugin-rsc` plugin should be placed after the React Router RSC plugin in your Vite config.
```tsx filename=vite.config.ts
import { defineConfig } from "vite";
import { unstable_reactRouterRSC as reactRouterRSC } from "@react-router/dev/vite";
import rsc from "@vitejs/plugin-rsc";
export default defineConfig({
plugins: [reactRouterRSC(), rsc()],
});
```
### Build Output
The RSC Framework Mode server build file (`build/server/index.js`) exports a `default` request handler function (`(request: Request) => Promise<Response>`) for document/data requests.
If needed, you can convert this into a [standard Node.js request listener][node-request-listener] for use with Node's built-in `http.createServer` function (or anything that supports it, e.g. [Express][express]) by using the `createRequestListener` function from [@remix-run/node-fetch-server][node-fetch-server].
For example, in Express:
```tsx filename=start.js
import express from "express";
import requestHandler from "./build/server/index.js";
import { createRequestListener } from "@remix-run/node-fetch-server";
const app = express();
app.use(
"/assets",
express.static("build/client/assets", {
immutable: true,
maxAge: "1y",
}),
);
app.use(express.static("build/client"));
app.use(createRequestListener(requestHandler));
app.listen(3000);
```
### React Elements From Loaders/Actions
In RSC Framework Mode, loaders and actions can return React elements along with other data. These elements will only ever be rendered on the server.
```tsx
import type { Route } from "./+types/route";
export async function loader() {
return {
message: "Message from the server!",
element: <p>Element from the server!</p>,
};
}
export default function Route({
loaderData,
}: Route.ComponentProps) {
return (
<>
<h1>{loaderData.message}</h1>
{loaderData.element}
</>
);
}
```
If you need to use client-only features (e.g. [Hooks][hooks], event handlers) within React elements returned from loaders/actions, you'll need to extract components using these features into a [client module][use-client-docs]:
```tsx filename=src/routes/counter/counter.tsx
"use client";
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
```
```tsx filename=src/routes/counter/route.tsx
import type { Route } from "./+types/route";
import { Counter } from "./counter";
export async function loader() {
return {
message: "Message from the server!",
element: (
<>
<p>Element from the server!</p>
<Counter />
</>
),
};
}
export default function Route({
loaderData,
}: Route.ComponentProps) {
return (
<>
<h1>{loaderData.message}</h1>
{loaderData.element}
</>
);
}
```
### Route Server Components
If a route exports a `ServerComponent` instead of the typical `default` component export, the route renders on the server instead of the client. A route module cannot export both `default` and `ServerComponent`.
You can still export client-only annotations like `clientLoader` and `clientAction` alongside a `ServerComponent`. The other route module component exports follow the same client/server split: `ErrorBoundary`, `Layout`, and `HydrateFallback` are client components, while `ServerErrorBoundary`, `ServerLayout`, and `ServerHydrateFallback` render on the server.
The following route module components have their own mutually exclusive server component counterparts:
| Server Component Export | Client Component |
| ----------------------- | ----------------- |
| `ServerComponent` | `default` |
| `ServerErrorBoundary` | `ErrorBoundary` |
| `ServerLayout` | `Layout` |
| `ServerHydrateFallback` | `HydrateFallback` |
```tsx
import type { Route } from "./+types/route";
import { Outlet } from "react-router";
import { getMessage } from "./message";
export async function loader() {
return {
message: await getMessage(),
};
}
export function ServerComponent({
loaderData,
}: Route.ServerComponentProps) {
return (
<>
<h1>Server Component Route</h1>
<p>Message from the server: {loaderData.message}</p>
<Outlet />
</>
);
}
```
If you need to use client-only features (e.g. [Hooks][hooks], event handlers) within a server-first route, you'll need to extract components using these features into a [client module][use-client-docs]:
```tsx filename=src/routes/counter/counter.tsx
"use client";
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
```
```tsx filename=src/routes/counter/route.tsx
import { Counter } from "./counter";
export function ServerComponent() {
return (
<>
<h1>Counter</h1>
<Counter />
</>
);
}
```
### `.server`/`.client` Modules
To avoid confusion with RSC's `"use server"` and `"use client"` directives, support for [`.server` modules][server-modules] and [`.client` modules][client-modules] is no longer built-in when using RSC Framework Mode.
As an alternative solution that doesn't rely on file naming conventions, we recommend using the `"server-only"` and `"client-only"` imports provided by [`@vitejs/plugin-rsc`][vite-plugin-rsc]. For example, to ensure a module is never accidentally included in the client build, simply import from `"server-only"` as a side effect within your server-only module.
```ts filename=app/utils/db.ts
import "server-only";
// Rest of the module...
```
Note that while there are official npm packages [`server-only`][server-only-package] and [`client-only`][client-only-package] created by the React team, they don't need to be installed. `@vitejs/plugin-rsc` internally handles these imports and provides build-time validation instead of runtime errors.
If you'd like to quickly migrate existing code that relies on the `.server` and `.client` file naming conventions, we recommend using the [`vite-env-only` plugin][vite-env-only] directly. For example, to ensure `.server` modules aren't accidentally included in the client build:
```tsx filename=vite.config.ts
import { defineConfig } from "vite";
import { denyImports } from "vite-env-only";
import { unstable_reactRouterRSC as reactRouterRSC } from "@react-router/dev/vite";
import rsc from "@vitejs/plugin-rsc";
export default defineConfig({
plugins: [
denyImports({
client: { files: ["**/.server/*", "**/*.server.*"] },
}),
reactRouterRSC(),
rsc(),
],
});
```
### MDX Route Support
MDX routes are supported in RSC Framework Mode when using `@mdx-js/rollup` v3.1.1+.
Note that any components exported from an MDX route must also be valid in RSC environments, meaning that they cannot use client-only features like [Hooks][hooks]. Any components that need to use these features should be extracted into a [client module][use-client-docs].
### Custom Entry Files
RSC Framework Mode supports custom entry files, allowing you to customize the behavior of the RSC server, SSR server, and client entry points.
The plugin will automatically detect custom entry files in your `app` directory:
- `app/entry.rsc.ts` (or `.tsx`) - Custom RSC server entry
- `app/entry.ssr.ts` (or `.tsx`) - Custom SSR server entry
- `app/entry.client.tsx` - Custom client entry
If these files are not found, React Router will use the default entries provided by the framework.
If you want to inspect the generated defaults before overriding them, you can also use `react-router reveal entry.client`, `react-router reveal entry.rsc`, and `react-router reveal entry.ssr`.
#### Basic Override Pattern
You can create a custom entry file that wraps or extends the default behavior. For example, to add custom logging to the RSC entry:
```ts filename=app/entry.rsc.ts
import defaultEntry from "@react-router/dev/config/default-rsc-entries/entry.rsc";
import { RouterContextProvider } from "react-router";
export default {
fetch(request: Request): Promise<Response> {
console.log(
"Custom RSC entry handling request:",
request.url,
);
const requestContext = new RouterContextProvider();
return defaultEntry.fetch(request, requestContext);
},
};
if (import.meta.hot) {
import.meta.hot.accept();
}
```
Similarly, you can customize the SSR entry:
```ts filename=app/entry.ssr.ts
import { generateHTML as defaultGenerateHTML } from "@react-router/dev/config/default-rsc-entries/entry.ssr";
export function generateHTML(
request: Request,
serverResponse: Response,
): Promise<Response> {
console.log(
"Custom SSR entry generating HTML for:",
request.url,
);
return defaultGenerateHTML(request, serverResponse);
}
```
And for the client:
```ts filename=app/entry.client.ts
import "@react-router/dev/config/default-rsc-entries/entry.client";
```
#### Copying Default Entries
For more advanced customization, you can copy the default entries and modify them as needed. To find the default entries:
1. In your IDE, use "Go to Definition" (or Cmd/Ctrl+Click) on the default entry import:
```ts
import defaultEntry from "@react-router/dev/config/default-rsc-entries/entry.rsc";
```
2. Copy the default entry code into your custom file
3. Modify it to suit your needs
The default entries are located at:
- [`@react-router/dev/config/default-rsc-entries/entry.rsc`][entry-rsc-source]
- [`@react-router/dev/config/default-rsc-entries/entry.ssr`][entry-ssr-source]
- [`@react-router/dev/config/default-rsc-entries/entry.client`][entry-client-source]
You can view the source code on GitHub using the links above, or navigate directly to these files in `node_modules/@react-router/dev/dist/config/default-rsc-entries/`.
<docs-info>
When copying default entries, make sure to maintain the required exports:
- `entry.rsc.ts` must export a default object with a `fetch` method
- `entry.ssr.ts` must export a `generateHTML` function
- `entry.client.tsx` should handle client-side hydration
</docs-info>
### Unsupported Config Options
The following options from `react-router.config.ts` are not currently supported in RSC Framework Mode:
- `buildEnd`
- `presets`
- `serverBundles`
- `future.v8_splitRouteModules`
- `subResourceIntegrity`
## RSC Data Mode
The RSC Framework Mode APIs described above are built on top of lower-level RSC Data Mode APIs.
RSC Data Mode is missing some of the features of RSC Framework Mode (e.g. `routes.ts` config and file system routing, HMR and Hot Data Revalidation), but is more flexible and allows you to integrate with your own bundler and server abstractions.
### Configuring Routes
Routes are configured as an argument to [`matchRSCServerRequest`][match-rsc-server-request]. At a minimum, you need a path and component:
```tsx
function Root() {
return <h1>Hello world</h1>;
}
matchRSCServerRequest({
// ...other options
routes: [{ path: "/", Component: Root }],
});
```
While you can define components inline, we recommend using the `lazy()` option and defining [Route Modules][route-module] for both startup performance and code organization.
<docs-info>
The `lazy` field of the RSC route config expects the same exports as the [Route Module API][route-module], which keeps the route-module shape consistent across [Framework Mode][framework-mode] and RSC Data Mode.
That includes exports like `loader`, `action`, `meta`, `links`, `headers`, `ErrorBoundary`, `HydrateFallback`, and the client annotations.
</docs-info>
```tsx filename=app/routes.ts
import type { unstable_RSCRouteConfig as RSCRouteConfig } from "react-router";
export function routes() {
return [
{
id: "root",
path: "",
lazy: () => import("./root/route"),
children: [
{
id: "home",
index: true,
lazy: () => import("./home/route"),
},
{
id: "about",
path: "about",
lazy: () => import("./about/route"),
},
],
},
] satisfies RSCRouteConfig;
}
```
### Server Component Routes
By default each route's `default` export renders a Server Component
```tsx
export default function Home() {
return (
<main>
<article>
<h1>Welcome to React Router RSC</h1>
<p>
You won't find me running any JavaScript in the
browser!
</p>
</article>
</main>
);
}
```
A nice feature of Server Components is that you can fetch data directly from your component by making it asynchronous.
```tsx
export default async function Home() {
let user = await getUserData();
return (
<main>
<article>
<h1>Welcome to React Router RSC</h1>
<p>
You won't find me running any JavaScript in the
browser!
</p>
<p>
Hello, {user ? user.name : "anonymous person"}!
</p>
</article>
</main>
);
}
```
<docs-info>
Server Components can also be returned from your loaders and actions. In general, if you are using RSC to build your application, loaders are primarily useful for things like setting `status` codes or returning a `redirect`.
Using Server Components in loaders can be helpful for incremental adoption of RSC.
</docs-info>
### Server Functions
[Server Functions][react-server-functions-doc] are a React feature that allow you to call async functions executed on the server. They're defined with the [`"use server"`][use-server-docs] directive.
```tsx
"use server";
export async function updateFavorite(formData: FormData) {
let movieId = formData.get("id");
let intent = formData.get("intent");
if (intent === "add") {
await addFavorite(Number(movieId));
} else {
await removeFavorite(Number(movieId));
}
}
```
```tsx
import { updateFavorite } from "./action.ts";
export async function AddToFavoritesForm({
movieId,
}: {
movieId: number;
}) {
let isFav = await isFavorite(movieId);
return (
<form action={updateFavorite}>
<input type="hidden" name="id" value={movieId} />
<input
type="hidden"
name="intent"
value={isFav ? "remove" : "add"}
/>
<AddToFavoritesButton isFav={isFav} />
</form>
);
}
```
Note that after server functions are called, React Router will automatically revalidate the route and update the UI with the new server content. You don't have to mess around with any cache invalidation.
### Client Properties
Routes are defined on the server at runtime, but we can still provide `clientLoader`, `clientAction`, and `shouldRevalidate` through the utilization of client references and `"use client"`.
```tsx filename=src/routes/root/client.tsx
"use client";
export function clientAction() {}
export function clientLoader() {}
export function shouldRevalidate() {}
export default function ClientRoot() {
return <p>Client route</p>;
}
```
We can then re-export these from our lazy loaded route module:
```tsx filename=src/routes/root/route.tsx
export {
clientAction,
clientLoader,
shouldRevalidate,
} from "./client";
export default function Root() {
// ...
}
```
This is also the way we would make an entire route a Client Component.
```tsx filename=src/routes/root/route.tsx lines=[1,11]
import { default as ClientRoot } from "./route.client";
export {
clientAction,
clientLoader,
shouldRevalidate,
} from "./client";
export default function Root() {
// Adding a Server Component at the root is required by bundlers
// if you're using css side-effects imports.
return <ClientRoot />;
}
```
### Bundler Configuration
React Router provides several APIs that allow you to easily integrate with RSC-compatible bundlers, useful if you are using React Router Data Mode to make your own [custom framework][custom-framework].
The following steps show how to setup a React Router application to use Server Components (RSC) to server-render (SSR) pages and hydrate them for single-page app (SPA) navigations. You don't have to use SSR (or even client-side hydration) if you don't want to. You can also leverage the HTML generation for Static Site Generation (SSG) or Incremental Static Regeneration (ISR) if you prefer. This guide is meant merely to explain how to wire up all the different APIs for a typically RSC-based application.
### Entry points
Besides our [route definitions](#configuring-routes), we will need to configure the following:
1. A server to handle the incoming request, fetch the RSC payload, and convert it into HTML
2. A React server to generate RSC payloads
3. A browser handler to hydrate the generated HTML and set the `callServer` function to support post-hydration server actions
The following naming conventions have been chosen for familiarity and simplicity. Feel free to name and configure your entry points as you see fit.
See the relevant bundler documentation below for specific code examples for each of the following entry points.
These examples all use [express][express] and [@remix-run/node-fetch-server][node-fetch-server] for the server and request handling.
**Routes**
See [Configuring Routes](#configuring-routes).
**Server**
<docs-info>
You don't have to use SSR at all. You can choose to use RSC to "prerender" HTML for Static Site Generation (SSG) or something like Incremental Static Regeneration (ISR).
</docs-info>
`entry.ssr.tsx` is the entry point for the server. It is responsible for handling the request, calling the RSC server, and converting the RSC payload into HTML on document requests (server-side rendering).
Relevant APIs:
- [`routeRSCServerRequest`][route-rsc-server-request]
- [`RSCStaticRouter`][rsc-static-router]
**RSC Server**
<docs-info>
Even though you have a "React Server" and a server responsible for request handling/SSR, you don't actually need to have 2 separate servers. You can simply have 2 separate module graphs within the same server. This is important because React behaves differently when generating RSC payloads vs. when generating HTML to be hydrated on the client.
</docs-info>
`entry.rsc.tsx` is the entry point for the React Server. It is responsible for matching the request to a route and generating RSC payloads.
Relevant APIs:
- [`matchRSCServerRequest`][match-rsc-server-request]
**Browser**
`entry.browser.tsx` is the entry point for the client. It is responsible for hydrating the generated HTML and setting the `callServer` function to support post-hydration server actions.
Relevant APIs:
- [`createCallServer`][create-call-server]
- [`getRSCStream`][get-rsc-stream]
- [`RSCHydratedRouter`][rsc-hydrated-router]
### Vite
See the [@vitejs/plugin-rsc docs][vite-plugin-rsc] for more information. You can also refer to our [Vite RSC Data Mode template][vite-rsc-template] to see a working version.
In addition to `react`, `react-dom`, and `react-router`, you'll need the following dependencies:
```shellscript
npm i -D vite @vitejs/plugin-react @vitejs/plugin-rsc
```
#### `vite.config.ts`
To configure Vite, add the following to your `vite.config.ts`:
```ts filename=vite.config.ts
import rsc from "@vitejs/plugin-rsc/plugin";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
react(),
rsc({
entries: {
client: "src/entry.browser.tsx",
rsc: "src/entry.rsc.tsx",
ssr: "src/entry.ssr.tsx",
},
}),
],
});
```
```tsx filename=src/routes/config.ts
import type { unstable_RSCRouteConfig as RSCRouteConfig } from "react-router";
export function routes() {
return [
{
id: "root",
path: "",
lazy: () => import("./root/route"),
children: [
{
id: "home",
index: true,
lazy: () => import("./home/route"),
},
{
id: "about",
path: "about",
lazy: () => import("./about/route"),
},
],
},
] satisfies RSCRouteConfig;
}
```
#### `entry.ssr.tsx`
The following is a simplified example of a Vite SSR Server.
```tsx filename=src/entry.ssr.tsx
import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr";
import { renderToReadableStream as renderHTMLToReadableStream } from "react-dom/server.edge";
import {
unstable_routeRSCServerRequest as routeRSCServerRequest,
unstable_RSCStaticRouter as RSCStaticRouter,
} from "react-router";
export async function generateHTML(
request: Request,
serverResponse: Response,
): Promise<Response> {
return await routeRSCServerRequest({
// The incoming request.
request,
// The React Server response
serverResponse,
// Provide the React Server touchpoints.
createFromReadableStream,
// Render the router to HTML.
async renderHTML(getPayload, options) {
const payload = await getPayload();
const formState =
payload.type === "render"
? await payload.formState
: undefined;
const bootstrapScriptContent =
await import.meta.viteRsc.loadBootstrapScriptContent(
"index",
);
return await renderHTMLToReadableStream(
<RSCStaticRouter getPayload={getPayload} />,
{
...options,
bootstrapScriptContent,
formState,
signal: request.signal,
},
);
},
});
}
```
#### `entry.rsc.tsx`
The following is a simplified example of a Vite RSC Server.
```tsx filename=src/entry.rsc.tsx
import {
createTemporaryReferenceSet,
decodeAction,
decodeFormState,
decodeReply,
loadServerAction,
renderToReadableStream,
} from "@vitejs/plugin-rsc/rsc";
import { unstable_matchRSCServerRequest as matchRSCServerRequest } from "react-router";
import { routes } from "./routes/config";
function fetchServer(request: Request) {
return matchRSCServerRequest({
// Provide the React Server touchpoints.
createTemporaryReferenceSet,
decodeAction,
decodeFormState,
decodeReply,
loadServerAction,
// The incoming request.
request,
// The app routes.
routes: routes(),
// Encode the match with the React Server implementation.
generateResponse(match, options) {
return new Response(
renderToReadableStream(match.payload, options),
{
status: match.statusCode,
headers: match.headers,
},
);
},
});
}
export default async function handler(request: Request) {
// Import the generateHTML function from the client environment
const ssr = await import.meta.viteRsc.loadModule<
typeof import("./entry.ssr")
>("ssr", "index");
return ssr.generateHTML(
request,
await fetchServer(request),
);
}
```
#### `entry.browser.tsx`
```tsx filename=src/entry.browser.tsx
import {
createFromReadableStream,
createTemporaryReferenceSet,
encodeReply,
setServerCallback,
} from "@vitejs/plugin-rsc/browser";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import {
unstable_createCallServer as createCallServer,
unstable_getRSCStream as getRSCStream,
unstable_RSCHydratedRouter as RSCHydratedRouter,
type unstable_RSCPayload as RSCPayload,
} from "react-router/dom";
// Create and set the callServer function to support post-hydration server actions.
setServerCallback(
createCallServer({
createFromReadableStream,
createTemporaryReferenceSet,
encodeReply,
}),
);
// Get and decode the initial server payload.
createFromReadableStream<RSCPayload>(getRSCStream()).then(
(payload) => {
startTransition(async () => {
const formState =
payload.type === "render"
? await payload.formState
: undefined;
hydrateRoot(
document,
<StrictMode>
<RSCHydratedRouter
createFromReadableStream={
createFromReadableStream
}
payload={payload}
/>
</StrictMode>,
{
formState,
},
);
});
},
);
```
[picking-a-mode]: ../start/modes
[react-server-components-doc]: https://react.dev/reference/rsc/server-components
[react-server-functions-doc]: https://react.dev/reference/rsc/server-functions
[use-client-docs]: https://react.dev/reference/rsc/use-client
[use-server-docs]: https://react.dev/reference/rsc/use-server
[route-module]: ../start/framework/route-module
[framework-mode]: ../start/modes#framework
[custom-framework]: ../start/data/custom
[vite-plugin-rsc]: https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-rsc
[match-rsc-server-request]: ../api/rsc/matchRSCServerRequest
[route-rsc-server-request]: ../api/rsc/routeRSCServerRequest
[rsc-static-router]: ../api/rsc/RSCStaticRouter
[create-call-server]: ../api/rsc/createCallServer
[get-rsc-stream]: ../api/rsc/getRSCStream
[rsc-hydrated-router]: ../api/rsc/RSCHydratedRouter
[express]: https://expressjs.com/
[node-fetch-server]: https://www.npmjs.com/package/@remix-run/node-fetch-server
[framework-rsc-template]: https://github.com/remix-run/react-router-templates/tree/main/unstable_rsc-framework-mode
[vite-rsc-template]: https://github.com/remix-run/react-router-templates/tree/main/unstable_rsc-data-mode-vite
[node-request-listener]: https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener
[hooks]: https://react.dev/reference/react/hooks
[vite-env-only]: https://github.com/pcattori/vite-env-only
[server-modules]: ../api/framework-conventions/server-modules
[client-modules]: ../api/framework-conventions/client-modules
[server-only-package]: https://www.npmjs.com/package/server-only
[client-only-package]: https://www.npmjs.com/package/client-only
[entry-rsc-source]: https://github.com/remix-run/react-router/blob/main/packages/react-router-dev/config/default-rsc-entries/entry.rsc.tsx
[entry-ssr-source]: https://github.com/remix-run/react-router/blob/main/packages/react-router-dev/config/default-rsc-entries/entry.ssr.tsx
[entry-client-source]: https://github.com/remix-run/react-router/blob/main/packages/react-router-dev/config/default-rsc-entries/entry.client.tsx
+126
View File
@@ -0,0 +1,126 @@
---
title: Resource Routes
---
# Resource Routes
[MODES: framework, data]
<br/>
<br/>
When server rendering, routes can serve "resources" instead of rendering components, like images, PDFs, JSON payloads, webhooks, etc.
## Defining a Resource Route
A route becomes a resource route by convention when its module exports a loader or action but does not export a default component.
Consider a route that serves a PDF instead of UI:
```ts
route("/reports/pdf/:id", "pdf-report.ts");
```
```tsx filename=pdf-report.ts
import type { Route } from "./+types/pdf-report";
export async function loader({ params }: Route.LoaderArgs) {
const report = await getReport(params.id);
const pdf = await generateReportPDF(report);
return new Response(pdf, {
status: 200,
headers: {
"Content-Type": "application/pdf",
},
});
}
```
Note there is no default export. That makes this route a resource route.
## Linking to Resource Routes
When linking to resource routes, use `<a>` or `<Link reloadDocument>`, otherwise React Router will attempt to use client side routing and fetching the payload (you'll get a helpful error message if you make this mistake).
```tsx
<Link reloadDocument to="/reports/pdf/123">
View as PDF
</Link>
```
## Handling different request methods
GET requests are handled by the `loader`, while POST, PUT, PATCH, and DELETE are handled by the `action`:
```tsx
import type { Route } from "./+types/resource";
export function loader(_: Route.LoaderArgs) {
return Response.json({ message: "I handle GET" });
}
export function action(_: Route.ActionArgs) {
return Response.json({
message: "I handle everything else",
});
}
```
## Return Types
Resource Routes are flexible when it comes to the return type - you can return [`Response`][Response] instances or [`data()`][data] objects. A good general rule of thumb when deciding which type to use is:
- If you're using resource routes intended for external consumption, return `Response` instances
- Keeps the resulting response encoding explicit in your code rather than having to wonder how React Router might convert `data() -> Response` under the hood
- If you're accessing resource routes from [fetchers][fetcher] or [`<Form>`][form] submissions, return `data()`
- Keeps things consistent with the loaders/actions in your UI routes
- Allows you to stream promises down to your UI through `data()`/[`Await`][await]
## Error Handling
Throwing an `Error` from Resource route (or anything other than a `Response`/`data()`) will trigger [`handleError`][handleError] and result in a 500 HTTP Response:
```tsx
export function action() {
let db = await getDb();
if (!db) {
// Fatal error - return a 500 response and trigger `handleError`
throw new Error("Could not connect to DB");
}
// ...
}
```
If a resource route generates a `Response` (via `new Response()` or `data()`), it is considered a successful execution and will not trigger `handleError` because the API has successfully produced a Response for the HTTP request. This applies to thrown responses as well as returned responses with a 4xx/5xx status code. This behavior aligns with `fetch()` which does not return a rejected promise on 4xx/5xx Responses.
```tsx
export function action() {
// Non-fatal error - don't trigger `handleError`:
throw new Response(
{ error: "Unauthorized" },
{ status: 401 },
);
// These 3 are equivalent to the above
return new Response(
{ error: "Unauthorized" },
{ status: 401 },
);
throw data({ error: "Unauthorized" }, { status: 401 });
return data({ error: "Unauthorized" }, { status: 401 });
}
```
### Error Boundaries
[Error Boundaries][error-boundary] are only applicable when a resource route is accessed from a UI, such as from a [`fetcher`][fetcher] call or a [`<Form>`][form] submission. If you `throw` from your resource route in these cases, it will bubble to the nearest `ErrorBoundary` in the UI.
[handleError]: ../api/framework-conventions/entry.server.tsx#handleerror
[data]: ../api/utils/data
[Response]: https://developer.mozilla.org/en-US/docs/Web/API/Response
[fetcher]: ../api/hooks/useFetcher
[form]: ../api/components/Form
[await]: ../api/components/Await
[error-boundary]: ../start/framework/route-module#errorboundary
@@ -0,0 +1,100 @@
---
title: Route Module Type Safety
---
# Route Module Type Safety
[MODES: framework]
<br/>
<br/>
React Router generates route-specific types to power type inference for URL params, loader data, and more.
This guide will help you set it up if you didn't start with a template.
To learn more about how type safety works in React Router, check out [Type Safety Explanation](../explanation/type-safety).
## 1. Add `.react-router/` to `.gitignore`
React Router generates types into a `.react-router/` directory at the root of your app. This directory is fully managed by React Router and should be gitignore'd.
```txt
.react-router/
```
## 2. Include the generated types in tsconfig
Edit your tsconfig to get TypeScript to use the generated types. Additionally, `rootDirs` needs to be configured so the types can be imported as relative siblings to route modules.
```json filename=tsconfig.json
{
"include": [".react-router/types/**/*"],
"compilerOptions": {
"rootDirs": [".", "./.react-router/types"]
}
}
```
If you are using multiple `tsconfig` files for your app, you'll need to make these changes in whichever one `include`s your app directory.
For example, the [`node-custom-server` template](https://github.com/remix-run/react-router-templates/tree/390fcec476dd336c810280479688fe893da38713/node-custom-server) contains `tsconfig.json`, `tsconfig.node.json`, and `tsconfig.vite.json`. Since `tsconfig.vite.json` is the one that [includes the app directory](https://github.com/remix-run/react-router-templates/blob/390fcec476dd336c810280479688fe893da38713/node-custom-server/tsconfig.vite.json#L4-L6), that's the one that sets up `.react-router/types` for route module type safety.
## 3. Generate types before type checking
If you want to run type checking as its own command — for example, as part of your Continuous Integration pipeline — you'll need to make sure to generate types _before_ running typechecking:
```json
{
"scripts": {
"typecheck": "react-router typegen && tsc"
}
}
```
## 4. Typing `AppLoadContext`
## Extending app `Context` types
To define your app's `context` type, add the following in a `.ts` or `.d.ts` file within your project:
```typescript
import "react-router";
declare module "react-router" {
interface AppLoadContext {
// add context properties here
}
}
```
## 5. Type-only auto-imports (optional)
When auto-importing the `Route` type helper, TypeScript will generate:
```ts filename=app/routes/my-route.tsx
import { Route } from "./+types/my-route";
```
But if you enable [verbatimModuleSyntax](https://www.typescriptlang.org/tsconfig/#verbatimModuleSyntax):
```json filename=tsconfig.json
{
"compilerOptions": {
"verbatimModuleSyntax": true
}
}
```
Then, you will get the `type` modifier for the import automatically as well:
```ts filename=app/routes/my-route.tsx
import type { Route } from "./+types/my-route";
// ^^^^
```
This helps tools like bundlers to detect type-only module that can be safely excluded from the bundle.
## Conclusion
React Router's Vite plugin should be automatically generating types into `.react-router/types/` anytime you edit your route config (`routes.ts`).
That means all you need to do is run `react-router dev` (or your custom dev server) to get to up-to-date types in your routes.
Check out our [Type Safety Explanation](../explanation/type-safety) for an example of how to pull in those types into your routes.
+4
View File
@@ -0,0 +1,4 @@
---
title: Using Search Params
hidden: true
---
+32
View File
@@ -0,0 +1,32 @@
---
title: Security
---
# Security
[MODES: framework]
<br/>
<br/>
This is by no means a comprehensive guide, but React Router provides features to help address a few aspects under the _very large_ umbrella that is _Security_.
## `Content-Security-Policy`
If you are implementing a [Content-Security-Policy (CSP)][csp] in your application, specifically one using the `unsafe-inline` directive, you will need to specify a [`nonce`][nonce] attribute on the inline `<script>` elements rendered in your HTML.
Add a nonce to these two spots in [`entry.server.tsx`][entryserver]:
- The [`<ServerRouter nonce>`][serverrouter] prop
- This will be proxied along through React Context and used for other Framework Mode components that output `nonce`-aware elements, including [`<Scripts>`][scripts], [`<ScrollRestoration>`][scrollrestoration]
- If those components specify their own `nonce` prop, it will override the `ServerRouter` value
- The `nonce` options of [`renderToPipeableStream`][renderToPipeableStream]/[`renderToReadableStream`][renderToReadableStream]
[csp]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP
[entryserver]: ../api/framework-conventions/entry.server.tsx
[nonce]: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce
[renderToPipeableStream]: https://react.dev/reference/react-dom/server/renderToPipeableStream
[renderToReadableStream]: https://react.dev/reference/react-dom/server/renderToReadableStream
[scripts]: ../api/components/Scripts
[scrollrestoration]: ../api/components/ScrollRestoration
[serverrouter]: ../api/framework-routers/ServerRouter
+66
View File
@@ -0,0 +1,66 @@
---
title: Server Bundles
---
# Server Bundles
[MODES: framework]
<br/>
<br/>
<docs-warning>This is an advanced feature designed for hosting provider integrations. When compiling your app into multiple server bundles, there will need to be a custom routing layer in front of your app directing requests to the correct bundle.</docs-warning>
React Router typically builds your server code into a single bundle that exports a request handler function. However, there are scenarios where you might want to split your route tree into multiple server bundles, each exposing a request handler function for a subset of routes. To provide this flexibility, [`react-router.config.ts`][react-router-config] supports a `serverBundles` option, which is a function for assigning routes to different server bundles.
The [`serverBundles` function][server-bundles-function] is called for each route in the tree (except for routes that aren't addressable, e.g., pathless layout routes) and returns a server bundle ID that you'd like to assign that route to. These bundle IDs will be used as directory names in your server build directory.
For each route, this function receives an array of routes leading to and including that route, referred to as the route `branch`. This allows you to create server bundles for different portions of the route tree. For example, you could use this to create a separate server bundle containing all routes within a particular layout route:
```ts filename=react-router.config.ts lines=[5-13]
import type { Config } from "@react-router/dev/config";
export default {
// ...
serverBundles: ({ branch }) => {
const isAuthenticatedRoute = branch.some((route) =>
route.id.split("/").includes("_authenticated"),
);
return isAuthenticatedRoute
? "authenticated"
: "unauthenticated";
},
} satisfies Config;
```
Each `route` in the `branch` array contains the following properties:
- `id` — The unique ID for this route, named like its `file` but relative to the app directory and without the extension, e.g., `app/routes/gists.$username.tsx` will have an `id` of `routes/gists.$username`
- `path` — The path this route uses to match the URL pathname
- `file` — The absolute path to the entry point for this route
- `index` — Whether this route is an index route
## Build manifest
When the build is complete, React Router will call the `buildEnd` hook, passing a `buildManifest` object. This is useful if you need to inspect the build manifest to determine how to route requests to the correct server bundle.
```ts filename=react-router.config.ts lines=[5-7]
import type { Config } from "@react-router/dev/config";
export default {
// ...
buildEnd: async ({ buildManifest }) => {
// ...
},
} satisfies Config;
```
When using server bundles, the build manifest contains the following properties:
- `serverBundles` — An object that maps bundle IDs to the bundle's `id` and `file`
- `routeIdToServerBundleId` — An object that maps route IDs to their server bundle ID
- `routes` — A route manifest that maps route IDs to route metadata. This can be used to drive a custom routing layer in front of your React Router request handlers
[react-router-config]: https://api.reactrouter.com/v7/types/_react-router_dev.config.Config.html
[server-bundles-function]: https://api.reactrouter.com/v7/types/_react-router_dev.config.ServerBundlesFunction.html
+120
View File
@@ -0,0 +1,120 @@
---
title: Single Page App (SPA)
---
# Single Page App (SPA)
[MODES: framework]
<br/>
<br/>
<docs-info>This guide focuses on how to build Single Page Apps with React Router Framework mode. If you're using React Router in declarative or data mode, you can design your own SPA architecture.</docs-info>
When using React Router as a framework, you can enable "SPA Mode" by setting `ssr:false` in your `react-router.config.ts` file. This will disable runtime server rendering and generate an `index.html` at build time that you can serve and hydrate as a SPA.
Typical Single Page apps send a mostly blank `index.html` template with little more than an empty `<div id="root"></div>`. In contrast, `react-router build` (in SPA Mode) pre-renders your root route at build time into an `index.html` file. This means you can:
- Send more than an empty `<div>`
- Use a root `loader` to load data for your application shell
- Use React components to generate the initial page users see (root `HydrateFallback`)
- Re-enable server rendering later without changing anything about your UI
<docs-info>SPA Mode is a special form of "Pre-Rendering" that allows you to serve all paths in your application from the same HTML file. Please refer to the [Pre-Rendering](./pre-rendering) guide if you want to do more extensive pre-rendering.</docs-info>
## 1. Disable Runtime Server Rendering
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";
export default {
ssr: false,
} satisfies Config;
```
With this set to false, the server build will no longer be generated.
<docs-info>It's important to note that setting `ssr:false` only disables _runtime server rendering_. React Router will still server render your root route at _build time_ to generate the `index.html` file. This is why your project still needs a dependency on `@react-router/node` and your routes need to be SSR-safe. That means you can't call `window` or other browser-only APIs during the initial render, even when server rendering is disabled.</docs-info>
## 2. Add a `HydrateFallback` and optional `loader` to your root route
SPA Mode will generate an `index.html` file at build-time that you can serve as the entry point for your SPA. This will only render the root route so that it is capable of hydrating at runtime for any path in your application.
To provide a better loading UI than an empty `<div>`, you can add a `HydrateFallback` component to your root route to render your loading UI into the `index.html` at build time. This way, it will be shown to users immediately while the SPA is loading/hydrating.
```tsx filename=root.tsx lines=[7-9]
import LoadingScreen from "./components/loading-screen";
export function Layout() {
return <html>{/*...*/}</html>;
}
export function HydrateFallback() {
return <LoadingScreen />;
}
export default function App() {
return <Outlet />;
}
```
Because the root route is server-rendered at build time, you can also use a `loader` in your root route if you choose. This `loader` will be called at build time and the data will be available via the optional `HydrateFallback` `loaderData` prop.
```tsx filename=root.tsx lines=[5,10,14]
import { Route } from "./+types/root";
export async function loader() {
return {
version: await getVersion(),
};
}
export function HydrateFallback({
loaderData,
}: Route.ComponentProps) {
return (
<div>
<h1>Loading version {loaderData.version}...</h1>
<AwesomeSpinner />
</div>
);
}
```
You cannot include a `loader` in any other routes in your app when using SPA Mode unless you are [pre-rendering those pages](./pre-rendering).
## 3. 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({
params,
}: Route.ClientLoaderArgs) {
let data = await fetch(`/some/api/stuff/${params.id}`);
return data;
}
export async function clientAction({
request,
}: Route.ClientActionArgs) {
let formData = await request.formData();
return await processPayment(formData);
}
```
## 4. Direct 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.
+63
View File
@@ -0,0 +1,63 @@
---
title: Status Codes
---
# Status Codes
[MODES: framework ,data]
<br/>
<br/>
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 = 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`.
+132
View File
@@ -0,0 +1,132 @@
---
title: Streaming with Suspense
---
# Streaming with Suspense
[MODES: framework, data]
<br/>
<br/>
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.
## 1. Return a promise from loader
React Router awaits route loaders before rendering route components. To unblock the loader for non-critical data, return the promise instead of awaiting it in the loader.
```tsx
import type { Route } from "./+types/my-route";
export async function loader({}: Route.LoaderArgs) {
// note this is NOT awaited
let nonCriticalData = new Promise((res) =>
setTimeout(() => res("non-critical"), 5000),
);
let criticalData = await new Promise((res) =>
setTimeout(() => res("critical"), 300),
);
return { nonCriticalData, criticalData };
}
```
Note you can't return a single promise, it must be an object with keys.
## 2. Render the fallback and resolved UI
The promise will be available on `loaderData`, `<Await>` will await the promise and trigger `<Suspense>` to render the fallback UI.
```tsx
import * as React from "react";
import { Await } from "react-router";
// [previous code]
export default function MyComponent({
loaderData,
}: Route.ComponentProps) {
let { criticalData, nonCriticalData } = loaderData;
return (
<div>
<h1>Streaming example</h1>
<h2>Critical data value: {criticalData}</h2>
<React.Suspense fallback={<div>Loading...</div>}>
<Await resolve={nonCriticalData}>
{(value) => <h3>Non critical value: {value}</h3>}
</Await>
</React.Suspense>
</div>
);
}
```
## With React 19
If you're using React 19, you can use `React.use` instead of `Await`, but you'll need to create a new component and pass the promise down to trigger the suspense fallback.
```tsx
<React.Suspense fallback={<div>Loading...</div>}>
<NonCriticalUI p={nonCriticalData} />
</React.Suspense>
```
```tsx
function NonCriticalUI({ p }: { p: Promise<string> }) {
let value = React.use(p);
return <h3>Non critical value {value}</h3>;
}
```
## Timeouts
By default, loaders and actions reject any outstanding promises after 4950ms. You can control this by exporting a `streamTimeout` numerical value from your `entry.server.tsx`.
```ts filename=entry.server.tsx
// Reject all pending promises from handler functions after 10 seconds
export const streamTimeout = 10_000;
```
## Handling early rejections (Node)
React Router waits for all loaders to settle (via `Promise.all`) before it begins streaming the response. Once streaming has started, React Router catches subsequent rejections of your streamed promises and surfaces them to your `<Await>` (or React 19 `React.use`) error UI.
However, if a streamed promise rejects _before_ all of the route's loaders have settled, React Router has not yet been able to attach a handler to it. In Node, an unhandled promise rejection will crash the process unless you have a top-level handler registered.
For example, this can happen if a parent route's loader takes longer to resolve than a child route's streamed promise takes to reject:
```tsx
// parent.tsx — slow loader
export async function loader() {
await new Promise((r) => setTimeout(r, 1000));
return { parent: "data" };
}
// child.tsx — fast-rejecting streamed promise
export async function loader() {
let lazy = new Promise((_, reject) =>
setTimeout(() => reject(new Error("boom")), 100),
);
return { lazy };
}
```
When `lazy` rejects before the parent loader resolves, the rejection bubbles to the node process as an unhandled rejection, which will crash the process without a user-defined handler.
To prevent this, register a process-level `unhandledRejection` handler in your server entry:
```ts filename=entry.server.ts
process.on("unhandledRejection", (reason, promise) => {
console.error(
"Unhandled Rejection at:",
promise,
"reason:",
reason,
);
});
```
+117
View File
@@ -0,0 +1,117 @@
---
title: Using handle
---
# Using `handle`
[MODES: framework]
<br/>
<br/>
You can build dynamic UI elements like breadcrumbs based on your route hierarchy using the [`useMatches`][use-matches] hook and [`handle`][handle] route exports.
## Understanding the Basics
React Router provides access to all route matches and their data throughout your component tree. This allows routes to contribute metadata through the `handle` export that can be rendered by ancestor components.
The `useMatches` hook combined with `handle` exports enables routes to contribute to rendering processes higher up the component tree than their actual render point. While we'll use breadcrumbs as an example, this pattern works for any scenario where you need routes to provide additional information to their ancestors.
## Defining Route `handle`s
We'll use a route structure like the following:
```ts filename=app/routes.ts
import { route } from "@react-router/dev/routes";
export default [
route("parent", "./routes/parent.tsx", [
route("child", "./routes/child.tsx"),
]),
] satisfies RouteConfig;
```
Add a `breadcrumb` property to the "parent" route's `handle` export. You can name this property whatever makes sense for your use case.
```tsx filename=app/routes/parent.tsx
import { Link } from "react-router";
export const handle = {
breadcrumb: () => <Link to="/parent">Some Route</Link>,
};
```
You can define breadcrumbs for child routes as well:
```tsx filename=app/routes/child.tsx
import { Link } from "react-router";
export const handle = {
breadcrumb: () => (
<Link to="/parent/child">Child Route</Link>
),
};
```
## Using Route `handle`s
Use the `useMatches` hook in your root layout or any ancestor component to collect and render the components defined in the `handle` export(s):
```tsx filename=app/root.tsx lines=[7,11,22-31]
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useMatches,
} from "react-router";
export function Layout({ children }) {
const matches = useMatches();
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<header>
<ol>
{matches
.filter(
(match) =>
match.handle && match.handle.breadcrumb,
)
.map((match, index) => (
<li key={index}>
{match.handle.breadcrumb(match)}
</li>
))}
</ol>
</header>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}
```
The `match` object is passed to each breadcrumb function, giving you access to `match.data` (from loaders) and other route information to create dynamic breadcrumbs based on your route's data.
This pattern provides a clean way for routes to contribute metadata that can be consumed and rendered by ancestor components.
## Additional Resources
- [`useMatches`][use-matches]
- [`handle`][handle]
[use-matches]: ../api/hooks/useMatches
[handle]: ../start/framework/route-module#handle
+237
View File
@@ -0,0 +1,237 @@
---
title: View Transitions
---
# View Transitions
[MODES: framework, data]
<br/>
<br/>
Enable smooth animations between page transitions in your React Router applications using the [View Transitions API][view-transitions-api]. This feature allows you to create seamless visual transitions during client-side navigation.
## Basic View Transition
### 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()`.
```tsx
<Link to="/about" viewTransition>
About
</Link>
```
Without any additional CSS, this provides a basic cross-fade animation between pages.
### 2. Enable view transitions with programmatic navigation
When using programmatic navigation with the `useNavigate` hook, you can enable view transitions by passing the `viewTransition: true` option:
```tsx
import { useNavigate } from "react-router";
function NavigationButton() {
const navigate = useNavigate();
return (
<button
onClick={() =>
navigate("/about", { viewTransition: true })
}
>
About
</button>
);
}
```
This provides the same cross-fade animation as using the `viewTransition` prop on Link components.
For more information on using the View Transitions API, please refer to the ["Smooth transitions with the View Transition API" guide][view-transitions-guide] from the Google Chrome team.
## Image Gallery Example
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.
### 1. Create the image gallery route
```tsx filename=routes/image-gallery.tsx
import { NavLink } from "react-router";
export const images = [
"https://remix.run/blog-images/headers/the-future-is-now.jpg",
"https://remix.run/blog-images/headers/waterfall.jpg",
"https://remix.run/blog-images/headers/webpack.png",
// ... more images ...
];
export default function ImageGalleryRoute() {
return (
<div className="image-list">
<h1>Image List</h1>
<div>
{images.map((src, idx) => (
<NavLink
key={src}
to={`/image/${idx}`}
viewTransition // Enable view transitions for this link
>
<p>Image Number {idx}</p>
<img
className="max-w-full contain-layout"
src={src}
/>
</NavLink>
))}
</div>
</div>
);
}
```
### 2. Add transition styles
Define view transition names and animations for elements that should transition smoothly between routes.
```css filename=app.css
/* Layout styles for the image grid */
.image-list > div {
display: grid;
grid-template-columns: repeat(4, 1fr);
column-gap: 10px;
}
.image-list h1 {
font-size: 2rem;
font-weight: 600;
}
.image-list img {
max-width: 100%;
contain: layout;
}
.image-list p {
width: fit-content;
}
/* Assign transition names to elements during navigation */
.image-list a.transitioning img {
view-transition-name: image-expand;
}
.image-list a.transitioning p {
view-transition-name: image-title;
}
```
### 3. Create the image detail route
The detail view needs to use the same view transition names to create a seamless animation.
```tsx filename=routes/image-details.tsx
import { Link } from "react-router";
import { images } from "./home";
import type { Route } from "./+types/image-details";
export default function ImageDetailsRoute({
params,
}: Route.ComponentProps) {
return (
<div className="image-detail">
<Link to="/" viewTransition>
Back
</Link>
<h1>Image Number {params.id}</h1>
<img src={images[Number(params.id)]} />
</div>
);
}
```
### 4. Add matching transition styles for the detail view
```css filename=app.css
/* Match transition names from the list view */
.image-detail h1 {
font-size: 2rem;
font-weight: 600;
width: fit-content;
view-transition-name: image-title;
}
.image-detail img {
max-width: 100%;
contain: layout;
view-transition-name: image-expand;
}
```
## Advanced Usage
You can control view transitions more precisely using either render props or the `useViewTransitionState` hook.
### 1. Using render props
```tsx filename=routes/image-gallery.tsx
<NavLink to={`/image/${idx}`} viewTransition>
{({ isTransitioning }) => (
<>
<p
style={{
viewTransitionName: isTransitioning
? "image-title"
: "none",
}}
>
Image Number {idx}
</p>
<img
src={src}
style={{
viewTransitionName: isTransitioning
? "image-expand"
: "none",
}}
/>
</>
)}
</NavLink>
```
### 2. Using the `useViewTransitionState` hook
```tsx filename=routes/image-gallery.tsx
function NavImage(props: { src: string; idx: number }) {
const href = `/image/${props.idx}`;
// Hook provides transition state for specific route
const isTransitioning = useViewTransitionState(href);
return (
<Link to={href} viewTransition>
<p
style={{
viewTransitionName: isTransitioning
? "image-title"
: "none",
}}
>
Image Number {props.idx}
</p>
<img
src={props.src}
style={{
viewTransitionName: isTransitioning
? "image-expand"
: "none",
}}
/>
</Link>
);
}
```
[view-transitions-api]: https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition
[view-transitions-guide]: https://developer.chrome.com/docs/web-platform/view-transitions
+50
View File
@@ -0,0 +1,50 @@
---
title: Webhooks
# can make a quick how-to on creating a webhook, this was copy/pasted from another doc, needs to be reviewed first
hidden: true
---
# Webhooks
Resource routes can be used to handle webhooks. For example, you can create a webhook that receives notifications from GitHub when a new commit is pushed to a repository:
```tsx
import type { Route } from "./+types/github";
import crypto from "node:crypto";
export const action = async ({
request,
}: Route.ActionArgs) => {
if (request.method !== "POST") {
return Response.json(
{ message: "Method not allowed" },
{
status: 405,
},
);
}
const payload = await request.json();
/* Validate the webhook */
const signature = request.headers.get(
"X-Hub-Signature-256",
);
const generatedSignature = `sha256=${crypto
.createHmac("sha256", process.env.GITHUB_WEBHOOK_SECRET)
.update(JSON.stringify(payload))
.digest("hex")}`;
if (signature !== generatedSignature) {
return Response.json(
{ message: "Signature mismatch" },
{
status: 401,
},
);
}
/* process the webhook (e.g. enqueue a background job) */
return Response.json({ success: true });
};
```