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
@@ -0,0 +1,50 @@
---
title: Backend For Frontend
---
# Backend For Frontend
[MODES: framework]
<br/>
<br/>
While React Router can serve as your fullstack application, it also fits perfectly into the "Backend for Frontend" architecture.
The BFF strategy employs a web server with a job scoped to serving the frontend web app and connecting it to the services it needs: your database, mailer, job queues, existing backend APIs (REST, GraphQL), etc. Instead of your UI integrating directly from the browser to these services, it connects to the BFF, and the BFF connects to your services.
Mature apps already have a lot of backend application code in Ruby, Elixir, PHP, etc., and there's no reason to justify migrating it all to a server-side JavaScript runtime just to get the benefits of React Router. Instead, you can use your React Router app as a backend for your frontend.
You can use `fetch` right from your loaders and actions to your backend.
```tsx lines=[7,13,17]
import escapeHtml from "escape-html";
export async function loader() {
const apiUrl = "https://api.example.com/some-data.json";
const res = await fetch(apiUrl, {
headers: {
Authorization: `Bearer ${process.env.API_TOKEN}`,
},
});
const data = await res.json();
const prunedData = data.map((record) => {
return {
id: record.id,
title: record.title,
formattedBody: escapeHtml(record.content),
};
});
return { prunedData };
}
```
There are several benefits of this approach vs. fetching directly from the browser. The highlighted lines above show how you can:
1. Simplify third-party integrations and keep tokens and secrets out of client bundles
2. Prune the data down to send less kB over the network, speeding up your app significantly
3. Move a lot of code from browser bundles to the server, like `escapeHtml`, which speeds up your app. Additionally, moving code to the server usually makes your code easier to maintain since server-side code doesn't have to worry about UI states for async operations
Again, React Router can be used as your only server by talking directly to the database and other services with server-side JavaScript APIs, but it also works perfectly as a backend for your frontend. Go ahead and keep your existing API server for application logic and let React Router connect the UI to it.
+61
View File
@@ -0,0 +1,61 @@
---
title: Automatic Code Splitting
---
# Automatic Code Splitting
[MODES: framework]
<br/>
<br/>
When using React Router's framework features, your application is automatically code split to improve the performance of initial load times when users visit your application.
## Code Splitting by Route
Consider this simple route config:
```tsx filename=app/routes.ts
import {
type RouteConfig,
route,
} from "@react-router/dev/routes";
export default [
route("/contact", "./contact.tsx"),
route("/about", "./about.tsx"),
] satisfies RouteConfig;
```
Instead of bundling all routes into a single giant build, the modules referenced (`contact.tsx` and `about.tsx`) become entry points to the bundler.
Because these entry points are coupled to URL segments, React Router knows just from a URL which bundles are needed in the browser, and more importantly, which are not.
If the user visits `"/about"` then the bundles for `about.tsx` will be loaded but not `contact.tsx`. This drastically reduces the JavaScript footprint for initial page loads and speeds up your application.
## Removal of Server Code
Any server-only [Route Module APIs][route-module] will be removed from the bundles. Consider this route module:
```tsx
export async function loader() {
return { message: "hello" };
}
export async function action() {
console.log(Date.now());
return { ok: true };
}
export async function headers() {
return { "Cache-Control": "max-age=300" };
}
export default function Component({ loaderData }) {
return <div>{loaderData.message}</div>;
}
```
After building for the browser, only the `Component` will still be in the bundle, so you can use server-only code in the other module exports.
[route-module]: ../start/framework/route-module
+135
View File
@@ -0,0 +1,135 @@
---
title: Network Concurrency Management
---
# Network Concurrency Management
[MODES: framework, data]
<br/>
<br/>
When building web applications, managing network requests can be a daunting task. The challenges of ensuring up-to-date data and handling simultaneous requests often lead to complex logic in the application to deal with interruptions and race conditions. React Router simplifies this process by automating network management while mirroring and expanding upon the intuitive behavior of web browsers.
To help understand how React Router handles concurrency, it's important to remember that after `form` submissions, React Router will fetch fresh data from the `loader`s. This is called revalidation.
## Natural Alignment with Browser Behavior
React Router's handling of network concurrency is heavily inspired by the default behavior of web browsers when processing documents.
### Link Navigation
**Browser Behavior**: When you click on a link in a browser and then click on another before the page transition completes, the browser prioritizes the most recent `action`. It cancels the initial request, focusing solely on the latest link clicked.
**React Router Behavior**: React Router manages client-side navigation the same way. When a link is clicked within a React Router application, it initiates fetch requests for each `loader` tied to the target URL. If another navigation interrupts the initial navigation, React Router cancels the previous fetch requests, ensuring that only the latest requests proceed.
### Form Submission
**Browser Behavior**: If you initiate a form submission in a browser and then quickly submit another form again, the browser disregards the first submission, processing only the latest one.
**React Router Behavior**: React Router mimics this behavior when working with forms. If a form is submitted and another submission occurs before the first completes, React Router cancels the original fetch requests. It then waits for the latest submission to complete before triggering page revalidation again.
## Concurrent Submissions and Revalidation
While standard browsers are limited to one request at a time for navigations and form submissions, React Router elevates this behavior. Unlike navigation, with [`useFetcher`][use_fetcher] multiple requests can be in flight simultaneously.
React Router is designed to handle multiple form submissions to server `action`s and concurrent revalidation requests efficiently. It ensures that as soon as new data is available, the state is updated promptly. However, React Router also safeguards against potential pitfalls by refraining from committing stale data when other `action`s introduce race conditions.
For instance, if three form submissions are in progress, and one completes, React Router updates the UI with that data immediately without waiting for the other two so that the UI remains responsive and dynamic. As the remaining submissions finalize, React Router continues to update the UI, ensuring that the most recent data is displayed.
Using this key:
- `|`: Submission begins
- ✓: Action complete, data revalidation begins
- ✅: Revalidated data is committed to the UI
- ❌: Request cancelled
We can visualize this scenario in the following diagram:
```text
submission 1: |----✓-----✅
submission 2: |-----✓-----✅
submission 3: |-----✓-----✅
```
However, if a subsequent submission's revalidation completes before an earlier one, React Router discards the earlier data, ensuring that only the most up-to-date information is reflected in the UI:
```text
submission 1: |----✓---------❌
submission 2: |-----✓-----✅
submission 3: |-----✓-----✅
```
Because the revalidation from submission (2) started later and landed earlier than submission (1), the requests from submission (1) are canceled and only the data from submission (2) is committed to the UI. It was requested later, so it's more likely to contain the updated values from both (1) and (2).
## Potential for Stale Data
It's unlikely your users will ever experience this, but there are still chances for the user to see stale data in very rare conditions with inconsistent infrastructure. Even though React Router cancels requests for stale data, they will still end up making it to the server. Canceling a request in the browser simply releases browser resources for that request; it can't "catch up" and stop it from getting to the server. In extremely rare conditions, a canceled request may change data after the interrupting `action`'s revalidation lands. Consider this diagram:
```text
👇 interruption with new submission
|----❌----------------------✓
|-------✓-----✅
👆
initial request reaches the server
after the interrupting submission
has completed revalidation
```
The user is now looking at different data than what is on the server. Note that this problem is both extremely rare and exists with default browser behavior, too. The chance of the initial request reaching the server later than both the submission and revalidation of the second is unexpected on any network and server infrastructure. If this is a concern with your infrastructure, you can send timestamps with your form submissions and write server logic to ignore stale submissions.
## Example
In UI components like comboboxes, each keystroke can trigger a network request. Managing such rapid, consecutive requests can be tricky, especially when ensuring that the displayed results match the most recent query. However, with React Router, this challenge is automatically handled, ensuring that users see the correct results without developers having to micromanage the network.
```tsx filename=app/pages/city-search.tsx
export async function loader({ request }) {
const { searchParams } = new URL(request.url);
const cities = await searchCities(searchParams.get("q"));
return cities;
}
export function CitySearchCombobox() {
const fetcher = useFetcher<typeof loader>();
return (
<fetcher.Form action="/city-search">
<Combobox aria-label="Cities">
<ComboboxInput
name="q"
onChange={(event) =>
// submit the form onChange to get the list of cities
fetcher.submit(event.target.form)
}
/>
{/* render with the loader's data */}
{fetcher.data ? (
<ComboboxPopover className="shadow-popup">
{fetcher.data.length > 0 ? (
<ComboboxList>
{fetcher.data.map((city) => (
<ComboboxOption
key={city.id}
value={city.name}
/>
))}
</ComboboxList>
) : (
<span>No results found</span>
)}
</ComboboxPopover>
) : null}
</Combobox>
</fetcher.Form>
);
}
```
All the application needs to know is how to query the data and how to render it. React Router handles the network.
## Conclusion
React Router offers developers an intuitive, browser-based approach to managing network requests. By mirroring browser behaviors and enhancing them where needed, it simplifies the complexities of concurrency, revalidation, and potential race conditions. Whether you're building a simple webpage or a sophisticated web application, React Router ensures that your user interactions are smooth, reliable, and always up to date.
[use_fetcher]: ../api/hooks/useFetcher
+292
View File
@@ -0,0 +1,292 @@
---
title: Form vs. fetcher
---
# Form vs. fetcher
[MODES: framework, data]
## Overview
Developing in React Router offers a rich set of tools that can sometimes overlap in functionality, creating a sense of ambiguity for newcomers. The key to effective development in React Router is understanding the nuances and appropriate use cases for each tool. This document seeks to provide clarity on when and why to use specific APIs.
## APIs in Focus
- [`<Form>`][form-component]
- [`useFetcher`][use-fetcher]
- [`useNavigation`][use-navigation]
Understanding the distinctions and intersections of these APIs is vital for efficient and effective React Router development.
## URL Considerations
The primary criterion when choosing among these tools is whether you want the URL to change or not:
- **URL Change Desired**: When navigating or transitioning between pages, or after certain actions like creating or deleting records. This ensures that the user's browser history accurately reflects their journey through your application.
- **Expected Behavior**: In many cases, when users hit the back button, they should be taken to the previous page. Other times the history entry may be replaced but the URL change is important nonetheless.
- **No URL Change Desired**: For actions that don't significantly change the context or primary content of the current view. This might include updating individual fields or minor data manipulations that don't warrant a new URL or page reload. This also applies to loading data with fetchers for things like popovers, combo boxes, etc.
### When the URL Should Change
These actions typically reflect significant changes to the user's context or state:
- **Creating a New Record**: After creating a new record, it's common to redirect users to a page dedicated to that new record, where they can view or further modify it.
- **Deleting a Record**: If a user is on a page dedicated to a specific record and decides to delete it, the logical next step is to redirect them to a general page, such as a list of all records.
For these cases, developers should consider using a combination of [`<Form>`][form-component] and [`useNavigation`][use-navigation]. These tools can be coordinated to handle form submission, invoke specific actions, retrieve action-related data through component props, and manage navigation respectively.
### When the URL Shouldn't Change
These actions are generally more subtle and don't require a context switch for the user:
- **Updating a Single Field**: Maybe a user wants to change the name of an item in a list or update a specific property of a record. This action is minor and doesn't necessitate a new page or URL.
- **Deleting a Record from a List**: In a list view, if a user deletes an item, they likely expect to remain on the list view, with that item no longer in the list.
- **Creating a Record in a List View**: When adding a new item to a list, it often makes sense for the user to remain in that context, seeing their new item added to the list without a full page transition.
- **Loading Data for a Popover or Combobox**: When loading data for a popover or combobox, the user's context remains unchanged. The data is loaded in the background and displayed in a small, self-contained UI element.
For such actions, [`useFetcher`][use-fetcher] is the go-to API. It's versatile, combining functionalities of these APIs, and is perfectly suited for tasks where the URL should remain unchanged.
## API Comparison
As you can see, the two sets of APIs have a lot of similarities:
| Navigation/URL API | Fetcher API |
| ----------------------------- | -------------------- |
| `<Form>` | `<fetcher.Form>` |
| `actionData` (component prop) | `fetcher.data` |
| `navigation.state` | `fetcher.state` |
| `navigation.formAction` | `fetcher.formAction` |
| `navigation.formData` | `fetcher.formData` |
## Examples
### Creating a New Record
```tsx filename=app/pages/new-recipe.tsx lines=[16,23-24,29]
import {
Form,
redirect,
useNavigation,
} from "react-router";
import type { Route } from "./+types/new-recipe";
export async function action({
request,
}: Route.ActionArgs) {
const formData = await request.formData();
const errors = await validateRecipeFormData(formData);
if (errors) {
return { errors };
}
const recipe = await db.recipes.create(formData);
return redirect(`/recipes/${recipe.id}`);
}
export function NewRecipe({
actionData,
}: Route.ComponentProps) {
const { errors } = actionData || {};
const navigation = useNavigation();
const isSubmitting =
navigation.formAction === "/recipes/new";
return (
<Form method="post">
<label>
Title: <input name="title" />
{errors?.title ? <span>{errors.title}</span> : null}
</label>
<label>
Ingredients: <textarea name="ingredients" />
{errors?.ingredients ? (
<span>{errors.ingredients}</span>
) : null}
</label>
<label>
Directions: <textarea name="directions" />
{errors?.directions ? (
<span>{errors.directions}</span>
) : null}
</label>
<button type="submit">
{isSubmitting ? "Saving..." : "Create Recipe"}
</button>
</Form>
);
}
```
The example leverages [`<Form>`][form-component], component props, and [`useNavigation`][use-navigation] to facilitate an intuitive record creation process.
Using `<Form>` ensures direct and logical navigation. After creating a record, the user is naturally guided to the new recipe's unique URL, reinforcing the outcome of their action.
The component props bridge server and client, providing immediate feedback on submission issues. This quick response enables users to rectify any errors without hindrance.
Lastly, `useNavigation` dynamically reflects the form's submission state. This subtle UI change, like toggling the button's label, assures users that their actions are being processed.
Combined, these APIs offer a balanced blend of structured navigation and feedback.
### Updating a Record
Now consider we're looking at a list of recipes that have delete buttons on each item. When a user clicks the delete button, we want to delete the recipe from the database and remove it from the list without navigating away from the list.
First, consider the basic route setup to get a list of recipes on the page:
```tsx filename=app/pages/recipes.tsx
import type { Route } from "./+types/recipes";
export async function loader({
request,
}: Route.LoaderArgs) {
return {
recipes: await db.recipes.findAll({ limit: 30 }),
};
}
export default function Recipes({
loaderData,
}: Route.ComponentProps) {
const { recipes } = loaderData;
return (
<ul>
{recipes.map((recipe) => (
<RecipeListItem key={recipe.id} recipe={recipe} />
))}
</ul>
);
}
```
Now we'll look at the action that deletes a recipe and the component that renders each recipe in the list.
```tsx filename=app/pages/recipes.tsx lines=[10,21,27]
import { useFetcher } from "react-router";
import type { Recipe } from "./recipe.server";
import type { Route } from "./+types/recipes";
export async function action({
request,
}: Route.ActionArgs) {
const formData = await request.formData();
const id = formData.get("id");
await db.recipes.delete(id);
return { ok: true };
}
export default function Recipes() {
return (
// ...
// doesn't matter, somewhere it's using <RecipeListItem />
)
}
function RecipeListItem({ recipe }: { recipe: Recipe }) {
const fetcher = useFetcher();
const isDeleting = fetcher.state !== "idle";
return (
<li>
<h2>{recipe.title}</h2>
<fetcher.Form method="post">
<input type="hidden" name="id" value={recipe.id} />
<button disabled={isDeleting} type="submit">
{isDeleting ? "Deleting..." : "Delete"}
</button>
</fetcher.Form>
</li>
);
}
```
Using [`useFetcher`][use-fetcher] in this scenario works perfectly. Instead of navigating away or refreshing the entire page, we want in-place updates. When a user deletes a recipe, the `action` is called and the fetcher manages the corresponding state transitions.
The key advantage here is the maintenance of context. The user stays on the list when the deletion completes. The fetcher's state management capabilities are leveraged to give real-time feedback: it toggles between `"Deleting..."` and `"Delete"`, providing a clear indication of the ongoing process.
Furthermore, with each `fetcher` having the autonomy to manage its own state, operations on individual list items become independent, ensuring that actions on one item don't affect the others (though revalidation of the page data is a shared concern covered in [Network Concurrency Management][network-concurrency-management]).
In essence, `useFetcher` offers a seamless mechanism for actions that don't necessitate a change in the URL or navigation, enhancing the user experience by providing real-time feedback and context preservation.
### Mark Article as Read
Imagine you want to mark that an article has been read by the current user, after they've been on the page for a while and scrolled to the bottom. You could make a hook that looks something like this:
```tsx
import { useFetcher } from "react-router";
function useMarkAsRead({ articleId, userId }) {
const marker = useFetcher();
useSpentSomeTimeHereAndScrolledToTheBottom(() => {
marker.submit(
{ userId },
{
action: `/article/${articleId}/mark-as-read`,
method: "post",
},
);
});
}
```
### User Avatar Details Popup
Anytime you show the user avatar, you could put a hover effect that fetches data from a loader and displays it in a popup.
```tsx filename=app/pages/user-details.tsx
import { useState, useEffect } from "react";
import { useFetcher } from "react-router";
import type { Route } from "./+types/user-details";
export async function loader({ params }: Route.LoaderArgs) {
return await fakeDb.user.find({
where: { id: params.id },
});
}
type LoaderData = Route.ComponentProps["loaderData"];
function UserAvatar({ partialUser }) {
const userDetails = useFetcher<LoaderData>();
const [showDetails, setShowDetails] = useState(false);
useEffect(() => {
if (
showDetails &&
userDetails.state === "idle" &&
!userDetails.data
) {
userDetails.load(`/user-details/${partialUser.id}`);
}
}, [showDetails, userDetails, partialUser.id]);
return (
<div
onMouseEnter={() => setShowDetails(true)}
onMouseLeave={() => setShowDetails(false)}
>
<img src={partialUser.profileImageUrl} />
{showDetails ? (
userDetails.state === "idle" && userDetails.data ? (
<UserPopup user={userDetails.data} />
) : (
<UserPopupLoading />
)
) : null}
</div>
);
}
```
## Conclusion
React Router offers a range of tools to cater to varied developmental needs. While some functionalities might seem to overlap, each tool has been crafted with specific scenarios in mind. By understanding the intricacies and ideal applications of `<Form>`, `useFetcher`, and `useNavigation`, along with how data flows through component props, developers can create more intuitive, responsive, and user-friendly web applications.
[form-component]: ../api/components/Form
[use-fetcher]: ../api/hooks/useFetcher
[use-navigation]: ../api/hooks/useNavigation
[network-concurrency-management]: ./concurrency
@@ -0,0 +1,137 @@
---
title: Hot Module Replacement
---
# Hot Module Replacement
[MODES: framework]
<br/>
<br/>
Hot Module Replacement is a technique for updating modules in your app without needing to reload the page.
It's a great developer experience, and React Router supports it when using Vite.
HMR does its best to preserve browser state across updates.
For example, let's say you have form within a modal and you fill out all the fields.
As soon as you save any changes to the code, traditional live reload would hard refresh the page causing all of those fields to be reset.
Every time you make a change, you'd have to open up the modal _again_ and fill out the form _again_.
But with HMR, all of that state is preserved _across updates_.
## React Fast Refresh
React already has mechanisms for updating the DOM via its [virtual DOM][virtual-dom] in response to user interactions like clicking a button.
Wouldn't it be great if React could handle updating the DOM in response to code changes too?
That's exactly what [React Fast Refresh][react-refresh] is all about!
Of course, React is all about components, not general JavaScript code, so React Fast Refresh only handles hot updates for exported React components.
But React Fast Refresh does have some limitations that you should be aware of.
### Class Component State
React Fast Refresh does not preserve state for class components.
This includes higher-order components that internally return classes:
```tsx
export class ComponentA extends Component {} // ❌
export const ComponentB = HOC(ComponentC); // ❌ Won't work if HOC returns a class component
export function ComponentD() {} // ✅
export const ComponentE = () => {}; // ✅
export default function ComponentF() {} // ✅
```
### Named Function Components
Function components must be named, not anonymous, for React Fast Refresh to track changes:
```tsx
export default () => {}; // ❌
export default function () {} // ❌
const ComponentA = () => {};
export default ComponentA; // ✅
export default function ComponentB() {} // ✅
```
### Supported Exports
React Fast Refresh can only handle component exports. While React Router manages [route exports like `action`, ` headers`, `links`, `loader`, and `meta`][route-module] for you, any user-defined exports will cause full reloads:
```tsx
// These exports are handled by the React Router Vite plugin
// to be HMR-compatible
export const meta = { title: "Home" }; // ✅
export const links = [
{ rel: "stylesheet", href: "style.css" },
]; // ✅
// These exports are removed by the React Router Vite plugin
// so they never affect HMR
export const headers = { "Cache-Control": "max-age=3600" }; // ✅
export const loader = async () => {}; // ✅
export const action = async () => {}; // ✅
// This is not a route module export, nor a component export,
// so it will cause a full reload for this route
export const myValue = "some value"; // ❌
export default function Route() {} // ✅
```
👆 Routes probably shouldn't be exporting random values like that anyway.
If you want to reuse values across routes, stick them in their own non-route module:
```ts filename=my-custom-value.ts
export const myValue = "some value";
```
### Changing Hooks
React Fast Refresh cannot track changes for a component when hooks are being added or removed from it, causing full reloads just for the next render. After the hooks have been updated, changes should result in hot updates again. For example, if you add a `useState` to your component, you may lose that component's local state for the next render.
Additionally, if you are destructuring a hook's return value, React Fast Refresh will not be able to preserve state for the component if the destructured key is removed or renamed.
For example:
```tsx
export default function Component({ loaderData }) {
const { pet } = useMyCustomHook();
return (
<div>
<input />
<p>My dog's name is {pet.name}!</p>
</div>
);
}
```
If you change the key `pet` to `dog`:
```diff
export default function Component() {
- const { pet } = useMyCustomHook();
+ const { dog } = useMyCustomHook();
return (
<div>
<input />
- <p>My dog's name is {pet.name}!</p>
+ <p>My dog's name is {dog.name}!</p>
</div>
);
}
```
then React Fast Refresh will not be able to preserve state `<input />` ❌.
### Component Keys
In some cases, React cannot distinguish between existing components being changed and new components being added. [React needs `key`s][react-keys] to disambiguate these cases and track changes when sibling elements are modified.
[virtual-dom]: https://reactjs.org/docs/faq-internals.html#what-is-the-virtual-dom
[react-refresh]: https://github.com/facebook/react/tree/main/packages/react-refresh
[react-keys]: https://react.dev/learn/rendering-lists#why-does-react-need-keys
[route-module]: ../start/framework/route-module
+14
View File
@@ -0,0 +1,14 @@
---
title: Hydration
hidden: true
---
There are a few nuances worth noting around the behavior of `HydrateFallback`:
- It is only relevant on initial document request and hydration, and will not be rendered on any subsequent client-side navigations
- It is only relevant when you are also setting [`clientLoader.hydrate=true`][hydrate-true] on a given route
- It is also relevant if you do have a `clientLoader` without a server `loader`, as this implies `clientLoader.hydrate=true` since there is otherwise no loader data at all to return from `useLoaderData`
- Even if you do not specify a `HydrateFallback` in this case, React Router will not render your route component and will bubble up to any ancestor `HydrateFallback` component
- This is to ensure that `useLoaderData` remains "happy-path"
- Without a server `loader`, `useLoaderData` would return `undefined` in any rendered route components
- You cannot render an `<Outlet/>` in a `HydrateFallback` because children routes can't be guaranteed to operate correctly since their ancestor loader data may not yet be available if they are running `clientLoader` functions on hydration (i.e., use cases such as `useRouteLoaderData()` or `useMatches()`)
@@ -0,0 +1,86 @@
---
title: Index Query Param
---
# Index Query Param
[MODES: framework, data]
## Overview
You may find a wild `?index` appearing in the URL of your app when submitting forms.
Because of nested routes, multiple routes in your route hierarchy can match the URL. Unlike navigations where all matching route [`loader`][loader]s are called to build up the UI, when a [`form`][form_element] is submitted, _only one action is called_.
Because index routes share the same URL as their parent, the `?index` param lets you disambiguate between the two.
## Understanding Index Routes
For example, consider the following route structure:
```ts filename=app/routes.ts
import {
type RouteConfig,
route,
index,
} from "@react-router/dev/routes";
export default [
route("projects", "./pages/projects.tsx", [
index("./pages/projects/index.tsx"),
route(":id", "./pages/projects/project.tsx"),
]),
] satisfies RouteConfig;
```
This creates two routes that match `/projects`:
- The parent route (`./pages/projects.tsx`)
- The index route (`./pages/projects/index.tsx`)
## Form Submission Targeting
For example, consider the following forms:
```tsx
<Form method="post" action="/projects" />
<Form method="post" action="/projects?index" />
```
The `?index` param will submit to the index route; the action without the index param will submit to the parent route.
When a [`<Form>`][form_component] is rendered in an index route without an [`action`][action], the `?index` param will automatically be appended so that the form posts to the index route. The following form, when submitted, will post to `/projects?index` because it is rendered in the context of the `projects` index route:
```tsx filename=app/pages/projects/index.tsx
function ProjectsIndex() {
return <Form method="post" />;
}
```
If you moved the code to the project layout (`./pages/projects.tsx` in this example), it would instead post to `/projects`.
This applies to `<Form>` and all of its cousins:
```tsx
function Component() {
const submit = useSubmit();
submit({}, { action: "/projects" });
submit({}, { action: "/projects?index" });
}
```
```tsx
function Component() {
const fetcher = useFetcher();
fetcher.submit({}, { action: "/projects" });
fetcher.submit({}, { action: "/projects?index" });
<fetcher.Form action="/projects" />;
<fetcher.Form action="/projects?index" />;
<fetcher.Form />; // defaults to the route in context
}
```
[loader]: ../start/data/route-object#loader
[form_element]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form
[form_component]: ../api/components/Form
[action]: ../start/data/route-object#action
+4
View File
@@ -0,0 +1,4 @@
---
title: Explanations
order: 5
---
@@ -0,0 +1,78 @@
---
title: Lazy Route Discovery
---
# Lazy Route Discovery
[MODES: framework]
<br/>
<br/>
Lazy Route Discovery is a performance optimization that loads route information progressively as users navigate through your application, rather than loading the complete route manifest upfront.
With Lazy Route Discovery enabled (the default), React Router sends only the routes needed for the initial server-side render in the manifest. As users navigate to new parts of your application, additional route information is fetched dynamically and added to the client-side manifest.
The route manifest contains metadata about your routes (JavaScript/CSS imports, whether routes have `loaders`/`actions`, etc.) but not the actual route module implementations. This allows React Router to understand your application's structure without downloading unnecessary route information.
## Route Discovery Process
When a user navigates to a new route that isn't in the current manifest:
1. **Route Discovery Request** - React Router makes a request to the internal `/__manifest` endpoint
2. **Manifest Patch** - The server responds with the required route information
3. **Route Loading** - React Router loads the necessary route modules and data
4. **Navigation** - The user navigates to the new route
## Eager Discovery Optimization
To prevent navigation waterfalls, React Router implements eager route discovery. All [`<Link>`](../api/components/Link) and [`<NavLink>`](../api/components/NavLink) components rendered on the current page are automatically discovered via a batched request to the server.
This discovery request typically completes before users click any links, making subsequent navigation feel synchronous even with lazy route discovery enabled.
```tsx
// Links are automatically discovered by default
<Link to="/dashboard">Dashboard</Link>
// Opt out of discovery for specific links
<Link to="/admin" discover="none">Admin</Link>
```
## Performance Benefits
Lazy Route Discovery provides several performance improvements:
- **Faster Initial Load** - Smaller initial bundle size by excluding unused route metadata
- **Reduced Memory Usage** - Route information is loaded only when needed
- **Scalability** - Applications with hundreds of routes see more significant benefits
## Configuration
You can configure route discovery behavior in your `react-router.config.ts`:
```tsx filename=react-router.config.ts
export default {
// Default: lazy discovery with /__manifest endpoint
routeDiscovery: {
mode: "lazy",
manifestPath: "/__manifest",
},
// Custom manifest path (useful for multiple apps on same domain)
routeDiscovery: {
mode: "lazy",
manifestPath: "/my-app-manifest",
},
// Disable lazy discovery (include all routes initially)
routeDiscovery: { mode: "initial" },
} satisfies Config;
```
## Deployment Considerations
When using lazy route discovery, ensure your deployment setup handles manifest requests properly:
- **Route Handling** - Ensure `/__manifest` requests reach your React Router handler
- **CDN Caching** - If using CDN/edge caching, include `version` and `paths` query parameters in your cache key for the manifest endpoint
- **Multiple Applications** - Use a custom `manifestPath` if running multiple React Router applications on the same domain
+6
View File
@@ -0,0 +1,6 @@
---
title: Location Object
hidden: true
---
<!-- put some stuff about what it is and how it can be used, probably good opportunity for a couple how-tos as well with scroll restoration, etc -->
@@ -0,0 +1,150 @@
---
title: Progressive Enhancement
---
# Progressive Enhancement
[MODES: framework]
<br/>
<br/>
> Progressive enhancement is a strategy in web design that puts emphasis on web content first, allowing everyone to access the basic content and functionality of a web page, whilst users with additional browser features or faster Internet access receive the enhanced version instead.
>
> <cite>- [Wikipedia][wikipedia]</cite>
When using React Router with Server-Side Rendering (the default in framework mode), you can automatically leverage the benefits of progressive enhancement.
## Why Progressive Enhancement Matters
Coined in 2003 by Steven Champeon & Nick Finck, the phrase emerged during a time of varied CSS and JavaScript support across different browsers, with many users actually browsing the web with JavaScript disabled.
Today, we are fortunate to develop for a much more consistent web and where the majority of users have JavaScript enabled.
However, we still believe in the core principles of progressive enhancement in React Router. It leads to fast and resilient apps with simple development workflows.
**Performance**: While it's easy to think that only 5% of your users have slow connections, the reality is that 100% of your users have slow connections 5% of the time.
**Resilience**: Everybody has JavaScript disabled until it's loaded.
**Simplicity**: Building your apps in a progressively enhanced way with React Router is actually simpler than building a traditional SPA.
## Performance
Server rendering allows your app to do more things in parallel than a typical [Single Page App (SPA)][spa], making the initial loading experience and subsequent navigations faster.
Typical SPAs send a blank document and only start doing work when JavaScript has loaded:
```
HTML |---|
JavaScript |---------|
Data |---------------|
page rendered 👆
```
A React Router app can start doing work the moment the request hits the server and stream the response so that the browser can start downloading JavaScript, other assets, and data in parallel:
```
👇 first byte
HTML |---|-----------|
JavaScript |---------|
Data |---------------|
page rendered 👆
```
## Resilience and Accessibility
While your users probably don't browse the web with JavaScript disabled, everybody uses the websites without JavaScript before it finishes loading. React Router embraces progressive enhancement by building on top of HTML, allowing you to build your app in a way that works without JavaScript, and then layer on JavaScript to enhance the experience.
The simplest case is a `<Link to="/account">`. These render an `<a href="/account">` tag that works without JavaScript. When JavaScript loads, React Router will intercept clicks and handle the navigation with client side routing. This gives you more control over the UX instead of just spinning favicons in the browser tab--but it works either way.
Now consider a simple add to cart button:
```tsx
export function AddToCart({ id }) {
return (
<Form method="post" action="/add-to-cart">
<input type="hidden" name="id" value={id} />
<button type="submit">Add To Cart</button>
</Form>
);
}
```
Whether JavaScript has loaded or not doesn't matter, this button will add the product to the cart.
When JavaScript loads, React Router will intercept the form submission and handle it client side. This allows you to add your own pending UI, or other client side behavior.
## Simplicity
When you start to rely on basic features of the web like HTML and URLs, you will find that you reach for client side state and state management much less.
Consider the button from before, with no fundamental change to the code, we can pepper in some client side behavior:
```tsx lines=[1,4,7,10-12,14]
import { useFetcher } from "react-router";
export function AddToCart({ id }) {
const fetcher = useFetcher();
return (
<fetcher.Form method="post" action="/add-to-cart">
<input name="id" value={id} />
<button type="submit">
{fetcher.state === "submitting"
? "Adding..."
: "Add To Cart"}
</button>
</fetcher.Form>
);
}
```
This feature continues to work the very same as it did before when JavaScript is loading, but once JavaScript loads:
- `useFetcher` no longer causes a navigation like `<Form>` does, so the user can stay on the same page and keep shopping
- The app code determines the pending UI instead of spinning favicons in the browser
It's not about building it two different waysonce for JavaScript and once withoutit's about building it in iterations. Start with the simplest version of the feature and ship it; then iterate to an enhanced user experience.
Not only will the user get a progressively enhanced experience, but the app developer gets to "progressively enhance" the UI without changing the fundamental design of the feature.
Another example where progressive enhancement leads to simplicity is with the URL. When you start with a URL, you don't need to worry about client side state management. You can just use the URL as the source of truth for the UI.
```tsx
export function SearchBox() {
return (
<Form method="get" action="/search">
<input type="search" name="query" />
<SearchIcon />
</Form>
);
}
```
This component doesn't need any state management. It just renders a form that submits to `/search`. When JavaScript loads, React Router will intercept the form submission and handle it client side. Here's the next iteration:
```tsx lines=[1,4-6,11]
import { useNavigation } from "react-router";
export function SearchBox() {
const navigation = useNavigation();
const isSearching =
navigation.location.pathname === "/search";
return (
<Form method="get" action="/search">
<input type="search" name="query" />
{isSearching ? <Spinner /> : <SearchIcon />}
</Form>
);
}
```
No fundamental change in architecture, simply a progressive enhancement for both the user and the code.
See also: [State Management][state_management]
[wikipedia]: https://en.wikipedia.org/wiki/Progressive_enhancement
[spa]: ../how-to/spa
[state_management]: ./state-management
+88
View File
@@ -0,0 +1,88 @@
---
title: Race Conditions
---
# Race Conditions
[MODES: framework, data]
<br/>
<br/>
While impossible to eliminate every possible race condition in your application, React Router automatically handles the most common race conditions found in web user interfaces.
## Browser Behavior
React Router's handling of network concurrency is heavily inspired by the behavior of web browsers when processing documents.
Consider clicking a link to a new document, and then clicking a different link before the new page has finished loading. The browser will:
1. cancel the first request
2. immediately process the new navigation
The same behavior applies to form submissions. When a pending form submission is interrupted by a new one, the first is canceled and the new submission is immediately processed.
## React Router Behavior
Like the browser, interrupted navigations with links and form submissions will cancel in flight data requests and immediately process the new event.
Fetchers are a bit more nuanced since they are not singleton events like navigation. Fetchers can't interrupt other fetcher instances, but they can interrupt themselves and the behavior is the same as everything else: cancel the interrupted request and immediately process the new one.
Fetchers do, however, interact with each other when it comes to revalidation. After a fetcher's action request returns to the browser, a revalidation for all page data is sent. This means multiple revalidation requests can be in-flight at the same time. React Router will commit all "fresh" revalidation responses and cancel any stale requests. A stale request is any request that started _earlier_ than one that has returned.
This management of the network prevents the most common UI bugs caused by network race conditions.
Since networks are unpredictable, and your server still processes these cancelled requests, your backend may still experience race conditions and have potential data integrity issues. These risks are the same risks as using default browser behavior with plain HTML `<forms>`, which we consider to be low, and outside the scope of React Router.
## Practical Benefits
Consider building a type-ahead combobox. As the user types, you send a request to the server. As they type each new character you send a new request. It's important to not show the user results for a value that's not in the text field anymore.
When using a fetcher, this is automatically managed for you. Consider this pseudo-code:
```tsx
// route("/city-search", "./search-cities.ts")
export async function loader({ request }) {
const { searchParams } = new URL(request.url);
return searchCities(searchParams.get("q"));
}
```
```tsx
export function CitySearchCombobox() {
const fetcher = useFetcher();
return (
<fetcher.Form action="/city-search">
<Combobox aria-label="Cities">
<ComboboxInput
name="q"
onChange={(event) =>
// submit the form onChange to get the list of cities
fetcher.submit(event.target.form)
}
/>
{fetcher.data ? (
<ComboboxPopover className="shadow-popup">
{fetcher.data.length > 0 ? (
<ComboboxList>
{fetcher.data.map((city) => (
<ComboboxOption
key={city.id}
value={city.name}
/>
))}
</ComboboxList>
) : (
<span>No results found</span>
)}
</ComboboxPopover>
) : null}
</Combobox>
</fetcher.Form>
);
}
```
Calls to `fetcher.submit` will cancel pending requests on that fetcher automatically. This ensures you never show the user results for a request for a different input value.
@@ -0,0 +1,160 @@
---
title: React Transitions
---
# React Transitions
[MODES: framework, data, declarative]
<br/>
<br/>
[React 18][react-18] introduced the concept of "transitions" which allow you to differentiate urgent from non-urgent UI updates. To learn more about React Transitions and "concurrent rendering" Please refer to React's official documentation:
- [What is Concurrent React][concurrent]
- [Transitions][transitions]
- [`React.useTransition`][use-transition]
- [`React.startTransition`][start-transition]
[React 19][react-19] enhances the async/concurrent landscape by introducing [Actions][actions] and support for using async functions in Transitions. With the support for async Transitions, a new [`React.useOptimistic`][use-optimistic-blog] [hook][use-optimistic] was also introduced that allows you to surface state updates during a Transition to show users instant feedback.
## Transitions in React Router
The introduction of Transitions in React makes the story of how React Router manages your navigations and router state a bit more complicated. These are powerful APIs but they don't come without some nuance and added complexity. We aim to make React Router work seamlessly with the new React features, but in some cases there may exist some tension between the new React ways to do things and some patterns you are already using in your React Router apps (i.e., pending states, optimistic UI).
To ensure a smooth adoption story, we've introduced changes related to Transitions behind an opt-in `useTransitions` flag so that you can upgrade in a non-breaking fashion.
### Current Behavior
We first leveraged `React.startTransition` to make React Router more Suspense-friendly in React Router [6.13.0][rr-6-13-0] via the `future.v7_startTransition` flag. In v7, that became the default behavior and all router state updates are currently wrapped in `React.startTransition`.
This default behavior has 2 potential issues that `useTransitions` is designed to solve:
- There are some valid use cases where you _don't_ want your updates wrapped in `startTransition`
- One specific issue is that `React.useSyncExternalStore` updates can't be Transitions ([^1][uses-transition-issue], [^2][uses-transition-tweet]). `useSyncExternalStore` forces a sync update, which means fallbacks can be shown in update transitions that would otherwise avoid showing the fallback.
- React Router has a `flushSync` option on navigations to use [`React.flushSync`][flush-sync] for state updates instead, but that's not always a proper solution
- React 19 has added a new `startTransition(() => Promise))` API as well as a new `useOptimistic` hook to surface updates during Transitions
- Without some updates to React Router, `startTransition(() => navigate(path))` doesn't work as you might expect, because we are not using `useOptimistic` internally so router state updates don't surface during the navigation, which breaks hooks like `useNavigation`
To provide a solution to both of the above issues, we're introducing a new `useTransitions` prop to the router components that will let you opt-out of using `startTransition` for router state updates (solving the first issue), or opt-into a more enhanced usage of `startTransition` + `useOptimistic` (solving the second issue). Because the current behavior is a bit incomplete with the new React 19 APIs, we plan to make the opt-in behavior the default in React Router v8, but we will likely retain the opt-out flag for use cases such as `useSyncExternalStore`.
### Opt-out via `useTransitions=false`
If your application is not "Transition-friendly" due to the usage of `useSyncExternalStore` (or other reasons), then you can opt-out via the prop:
```tsx
// Framework Mode (entry.client.tsx)
<HydratedRouter useTransitions={false} />
// Data Mode
<RouterProvider useTransitions={false} />
// Declarative Mode
<BrowserRouter useTransitions={false} />
```
This will stop the router from wrapping internal state updates in `startTransition`.
### Opt-in via `useTransitions=true`
<docs-info>Opting into this feature in Framework or Data Mode requires that you are using React 19 because it needs access to [`React.useOptimistic`][use-optimistic]</docs-info>
If you want to make your application play nicely with all of the new React 19 features that rely on concurrent mode and Transitions, then you can opt-in via the new prop:
```tsx
// Framework Mode (entry.client.tsx)
<HydratedRouter useTransitions />
// Data Mode
<RouterProvider useTransitions />
// Declarative Mode
<BrowserRouter useTransitions />
```
With this flag enabled:
- All internal state updates are wrapped in `React.startTransition` (current behavior without the flag)
- All `<Link>`/`<Form>` navigations will be wrapped in `React.startTransition`, using the promise returned by `useNavigate`/`useSubmit` so that the Transition lasts for the duration of the navigation
- `useNavigate`/`useSubmit` do not automatically wrap in `React.startTransition`, so you can opt-out of a Transition-enabled navigation by using those directly
- In Framework/Data modes, a subset of the router state updates during a navigation will be surfaced to the UI via `useOptimistic`
- State related to the _ongoing_ navigation and all fetcher information will be surfaced:
- `state.navigation` for `useNavigation()`
- `state.revalidation` for `useRevalidator()`
- `state.actionData` for `useActionData()`
- `state.fetchers` for `useFetcher()` and `useFetchers()`
- State related to the _current_ location will not be surfaced:
- `state.location` for `useLocation`
- `state.matches` for `useMatches()`,
- `state.loaderData` for `useLoaderData()`
- `state.errors` for `useRouteError()`
- etc.
Enabling this flag means that you can now have fully-Transition-enabled navigations that play nicely with any other ongoing Transition-enabled aspects of your application.
The only APIs that are automatically wrapped in an async Transition are `<Link>` and `<Form>`. For everything else, you need to wrap the operation in `startTransition` yourself.
```tsx
// Automatically Transition-enabled
<Link to="/path" />
<Form method="post" action="/path" />
// Manually Transition-enabled
startTransition(() => navigate("/path"));
startTransition(() => submit(data, { method: 'post', action: "/path" }));
startTransition(() => fetcher.load("/path"));
startTransition(() => fetcher.submit(data, { method: "post", action: "/path" }));
// Not Transition-enabled
navigate("/path");
submit(data, { method: 'post', action: "/path" });
fetcher.load("/path");
fetcher.submit(data, { method: "post", action: "/path" });
```
**Important:** You must always `return` or `await` the `navigate` promise inside `startTransition` so that the Transition encompasses the full duration of the navigation. If you forget to `return` or `await` the promise, the Transition will end prematurely and things won't work as expected.
```tsx
// ✅ Returned promise
startTransition(() => navigate("/path"));
startTransition(() => {
setOptimistic(something);
return navigate("/path"));
});
// ✅ Awaited promise
startTransition(async () => {
setOptimistic(something);
await navigate("/path"));
});
// ❌ Non-returned promise
startTransition(() => {
setOptimistic(something);
navigate("/path"));
});
// ❌ Non-Awaited promise
startTransition(async () => {
setOptimistic(something);
navigate("/path"));
});
```
#### `popstate` navigations
There is currently a bug with optimistic states and `popstate`. If you need to read the current route during a back navigation, which cannot complete synchronously (e.g. Suspends on uncached data), you can set the optimistic state before navigating back or defer the optimistic update in a timer or microtask.
[react-18]: https://react.dev/blog/2022/03/29/react-v18
[concurrent]: https://react.dev/blog/2022/03/29/react-v18#what-is-concurrent-react
[transitions]: https://react.dev/blog/2022/03/29/react-v18#new-feature-transitions
[use-transition]: https://react.dev/reference/react/useTransition#reference
[start-transition]: https://react.dev/reference/react/startTransition
[react-19]: https://react.dev/blog/2024/12/05/react-19
[actions]: https://react.dev/blog/2024/12/05/react-19#actions
[use-optimistic-blog]: https://react.dev/blog/2024/12/05/react-19#new-hook-optimistic-updates
[use-optimistic]: https://react.dev/reference/react/useOptimistic
[flush-sync]: https://react.dev/reference/react-dom/flushSync
[rr-6-13-0]: https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#v6130
[uses-transition-issue]: https://github.com/facebook/react/issues/26382
[uses-transition-tweet]: https://x.com/rickhanlonii/status/1683636856808775682
@@ -0,0 +1,7 @@
---
title: Route Matching
hidden: true
# want to explain how the matching algorithm works with any potential gotchas
---
# Route Matching
@@ -0,0 +1,4 @@
---
title: Server vs. Client Code Execution
hidden: true
---
@@ -0,0 +1,465 @@
---
title: Sessions and Cookies
---
# Sessions and Cookies
[MODES: framework, data]
## Sessions
Sessions are an important part of websites that allow the server to identify requests coming from the same person, especially when it comes to server-side form validation or when JavaScript is not on the page. Sessions are a fundamental building block of many sites that let users "log in", including social, e-commerce, business, and educational websites.
When using React Router as your framework, sessions are managed on a per-route basis (rather than something like express middleware) in your `loader` and `action` methods using a "session storage" object (that implements the [`SessionStorage`][session-storage] interface). Session storage understands how to parse and generate cookies, and how to store session data in a database or filesystem.
### Using Sessions
This is an example of a cookie session storage:
```ts filename=app/sessions.server.ts
import { createCookieSessionStorage } from "react-router";
type SessionData = {
userId: string;
};
type SessionFlashData = {
error: string;
};
const { getSession, commitSession, destroySession } =
createCookieSessionStorage<SessionData, SessionFlashData>(
{
// a Cookie from `createCookie` or the CookieOptions to create one
cookie: {
name: "__session",
// all of these are optional
domain: "reactrouter.com",
// Expires can also be set (although maxAge overrides it when used in combination).
// Note that this method is NOT recommended as `new Date` creates only one date on each server deployment, not a dynamic date in the future!
//
// expires: new Date(Date.now() + 60_000),
httpOnly: true,
maxAge: 60,
path: "/",
sameSite: "lax",
secrets: ["s3cret1"],
secure: true,
},
},
);
export { getSession, commitSession, destroySession };
```
We recommend setting up your session storage object in `app/sessions.server.ts` so all routes that need to access session data can import from the same spot.
The input/output to a session storage object are HTTP cookies. `getSession()` retrieves the current session from the incoming request's `Cookie` header, and `commitSession()`/`destroySession()` provide the `Set-Cookie` header for the outgoing response.
You'll use methods to get access to sessions in your `loader` and `action` functions.
After retrieving a session with `getSession`, the returned session object has a handful of methods and properties:
```tsx
export async function action({
request,
}: ActionFunctionArgs) {
const session = await getSession(
request.headers.get("Cookie"),
);
session.get("foo");
session.has("bar");
// etc.
}
```
See the [Session API][session-api] for all methods available on the session object.
### Login form example
A login form might look something like this:
```tsx filename=app/routes/login.tsx lines=[4-7,12-14,16,22,25,33-35,46,51,56,61]
import { data, redirect } from "react-router";
import type { Route } from "./+types/login";
import {
getSession,
commitSession,
} from "../sessions.server";
export async function loader({
request,
}: Route.LoaderArgs) {
const session = await getSession(
request.headers.get("Cookie"),
);
if (session.has("userId")) {
// Redirect to the home page if they are already signed in.
return redirect("/");
}
return data(
{ error: session.get("error") },
{
headers: {
"Set-Cookie": await commitSession(session),
},
},
);
}
export async function action({
request,
}: Route.ActionArgs) {
const session = await getSession(
request.headers.get("Cookie"),
);
const form = await request.formData();
const username = form.get("username");
const password = form.get("password");
const userId = await validateCredentials(
username,
password,
);
if (userId == null) {
session.flash("error", "Invalid username/password");
// Redirect back to the login page with errors.
return redirect("/login", {
headers: {
"Set-Cookie": await commitSession(session),
},
});
}
session.set("userId", userId);
// Login succeeded, send them to the home page.
return redirect("/", {
headers: {
"Set-Cookie": await commitSession(session),
},
});
}
export default function Login({
loaderData,
}: Route.ComponentProps) {
const { error } = loaderData;
return (
<div>
{error ? <div className="error">{error}</div> : null}
<form method="POST">
<div>
<p>Please sign in</p>
</div>
<label>
Username: <input type="text" name="username" />
</label>
<label>
Password:{" "}
<input type="password" name="password" />
</label>
</form>
</div>
);
}
```
And then a logout form might look something like this:
```tsx filename=app/routes/logout.tsx
import {
getSession,
destroySession,
} from "../sessions.server";
import type { Route } from "./+types/logout";
export async function action({
request,
}: Route.ActionArgs) {
const session = await getSession(
request.headers.get("Cookie"),
);
return redirect("/login", {
headers: {
"Set-Cookie": await destroySession(session),
},
});
}
export default function LogoutRoute() {
return (
<>
<p>Are you sure you want to log out?</p>
<Form method="post">
<button>Logout</button>
</Form>
<Link to="/">Never mind</Link>
</>
);
}
```
<docs-warning>It's important that you logout (or perform any mutation for that matter) in an `action` and not a `loader`. Otherwise you open your users to [Cross-Site Request Forgery][csrf] attacks.</docs-warning>
### Session Gotchas
Because of nested routes, multiple loaders can be called to construct a single page. When using `session.flash()` or `session.unset()`, you need to be sure no other loaders in the request are going to want to read that, otherwise you'll get race conditions. Typically if you're using flash, you'll want to have a single loader read it, if another loader wants a flash message, use a different key for that loader.
### Creating custom session storage
React Router makes it easy to store sessions in your own database if needed. The [`createSessionStorage()`][create-session-storage] API requires a `cookie` (for options for creating a cookie, see [cookies][cookies]) and a set of create, read, update, and delete (CRUD) methods for managing the session data. The cookie is used to persist the session ID.
- `createData` will be called from `commitSession` on the initial session creation when no session ID exists in the cookie
- `readData` will be called from `getSession` when a session ID exists in the cookie
- `updateData` will be called from `commitSession` when a session ID already exists in the cookie
- `deleteData` is called from `destroySession`
The following example shows how you could do this using a generic database client:
```ts
import { createSessionStorage } from "react-router";
function createDatabaseSessionStorage({
cookie,
host,
port,
}) {
// Configure your database client...
const db = createDatabaseClient(host, port);
return createSessionStorage({
cookie,
async createData(data, expires) {
// `expires` is a Date after which the data should be considered
// invalid. You could use it to invalidate the data somehow or
// automatically purge this record from your database.
const id = await db.insert(data);
return id;
},
async readData(id) {
return (await db.select(id)) || null;
},
async updateData(id, data, expires) {
await db.update(id, data);
},
async deleteData(id) {
await db.delete(id);
},
});
}
```
And then you can use it like this:
```ts
const { getSession, commitSession, destroySession } =
createDatabaseSessionStorage({
host: "localhost",
port: 1234,
cookie: {
name: "__session",
sameSite: "lax",
},
});
```
The `expires` argument to `createData` and `updateData` is the same `Date` at which the cookie itself expires and is no longer valid. You can use this information to automatically purge the session record from your database to save on space, or to ensure that you do not otherwise return any data for old, expired cookies.
### Additional session utils
There are also several other session utilities available if you need them:
- [`isSession`][is-session]
- [`createMemorySessionStorage`][create-memory-session-storage]
- [`createSession`][create-session] (custom storage)
- [`createFileSessionStorage`][create-file-session-storage] (node)
- [`createWorkersKVSessionStorage`][create-workers-kv-session-storage] (Cloudflare Workers)
- [`createArcTableSessionStorage`][create-arc-table-session-storage] (architect, Amazon DynamoDB)
## Cookies
A [cookie][cookie] is a small piece of information that your server sends someone in a HTTP response that their browser will send back on subsequent requests. This technique is a fundamental building block of many interactive websites that adds state so you can build authentication (see [sessions][sessions]), shopping carts, user preferences, and many other features that require remembering who is "logged in".
React Router's [`Cookie` interface][cookie-api] provides a logical, reusable container for cookie metadata.
### Using cookies
While you may create these cookies manually, it is more common to use a [session storage][sessions].
In React Router, you will typically work with cookies in your `loader` and/or `action` functions, since those are the places where you need to read and write data.
Let's say you have a banner on your e-commerce site that prompts users to check out the items you currently have on sale. The banner spans the top of your homepage, and includes a button on the side that allows the user to dismiss the banner so they don't see it for at least another week.
First, create a cookie:
```ts filename=app/cookies.server.ts
import { createCookie } from "react-router";
export const userPrefs = createCookie("user-prefs", {
maxAge: 604_800, // one week
});
```
Then, you can `import` the cookie and use it in your `loader` and/or `action`. The `loader` in this case just checks the value of the user preference so you can use it in your component for deciding whether to render the banner. When the button is clicked, the `<form>` calls the `action` on the server and reloads the page without the banner.
### User preferences example
```tsx filename=app/routes/home.tsx lines=[4,9-11,18-20,29]
import { Link, Form, redirect } from "react-router";
import type { Route } from "./+types/home";
import { userPrefs } from "../cookies.server";
export async function loader({
request,
}: Route.LoaderArgs) {
const cookieHeader = request.headers.get("Cookie");
const cookie =
(await userPrefs.parse(cookieHeader)) || {};
return { showBanner: cookie.showBanner };
}
export async function action({
request,
}: Route.ActionArgs) {
const cookieHeader = request.headers.get("Cookie");
const cookie =
(await userPrefs.parse(cookieHeader)) || {};
const bodyParams = await request.formData();
if (bodyParams.get("bannerVisibility") === "hidden") {
cookie.showBanner = false;
}
return redirect("/", {
headers: {
"Set-Cookie": await userPrefs.serialize(cookie),
},
});
}
export default function Home({
loaderData,
}: Route.ComponentProps) {
return (
<div>
{loaderData.showBanner ? (
<div>
<Link to="/sale">Don't miss our sale!</Link>
<Form method="post">
<input
type="hidden"
name="bannerVisibility"
value="hidden"
/>
<button type="submit">Hide</button>
</Form>
</div>
) : null}
<h1>Welcome!</h1>
</div>
);
}
```
### Cookie attributes
Cookies have [several attributes][cookie-attrs] that control when they expire, how they are accessed, and where they are sent. Any of these attributes may be specified either in `createCookie(name, options)`, or during `serialize()` when the `Set-Cookie` header is generated.
```ts
const cookie = createCookie("user-prefs", {
// These are defaults for this cookie.
path: "/",
sameSite: "lax",
httpOnly: true,
secure: true,
expires: new Date(Date.now() + 60_000),
maxAge: 60,
});
// You can either use the defaults:
cookie.serialize(userPrefs);
// Or override individual ones as needed:
cookie.serialize(userPrefs, { sameSite: "strict" });
```
Please read [more info about these attributes][cookie-attrs] to get a better understanding of what they do.
### Signing cookies
It is possible to sign a cookie to automatically verify its contents when it is received. Since it's relatively easy to spoof HTTP headers, this is a good idea for any information that you do not want someone to be able to fake, like authentication information (see [sessions][sessions]).
To sign a cookie, provide one or more `secrets` when you first create the cookie:
```ts
const cookie = createCookie("user-prefs", {
secrets: ["s3cret1"],
});
```
Cookies that have one or more `secrets` will be stored and verified in a way that ensures the cookie's integrity.
Secrets may be rotated by adding new secrets to the front of the `secrets` array. Cookies that have been signed with old secrets will still be decoded successfully in `cookie.parse()`, and the newest secret (the first one in the array) will always be used to sign outgoing cookies created in `cookie.serialize()`.
```ts filename=app/cookies.server.ts
export const cookie = createCookie("user-prefs", {
secrets: ["n3wsecr3t", "olds3cret"],
});
```
```tsx filename=app/routes/my-route.tsx
import { data } from "react-router";
import { cookie } from "../cookies.server";
import type { Route } from "./+types/my-route";
export async function loader({
request,
}: Route.LoaderArgs) {
const oldCookie = request.headers.get("Cookie");
// oldCookie may have been signed with "olds3cret", but still parses ok
const value = await cookie.parse(oldCookie);
return data("...", {
headers: {
// Set-Cookie is signed with "n3wsecr3t"
"Set-Cookie": await cookie.serialize(value),
},
});
}
```
### Additional cookie utils
There are also several other cookie utilities available if you need them:
- [`isCookie`][is-cookie]
- [`createCookie`][create-cookie]
To learn more about each attribute, please see the [MDN Set-Cookie docs][cookie-attrs].
[csrf]: https://developer.mozilla.org/en-US/docs/Glossary/CSRF
[cookies]: #cookies
[sessions]: #sessions
[session-storage]: https://api.reactrouter.com/v7/interfaces/react-router.SessionStorage
[session-api]: https://api.reactrouter.com/v7/interfaces/react-router.Session
[is-session]: https://api.reactrouter.com/v7/functions/react-router.isSession
[cookie-api]: https://api.reactrouter.com/v7/interfaces/react-router.Cookie
[create-session-storage]: https://api.reactrouter.com/v7/functions/react-router.createSessionStorage
[create-session]: https://api.reactrouter.com/v7/functions/react-router.createSession
[create-memory-session-storage]: https://api.reactrouter.com/v7/functions/react-router.createMemorySessionStorage
[create-file-session-storage]: https://api.reactrouter.com/v7/functions/_react-router_node.createFileSessionStorage
[create-workers-kv-session-storage]: https://api.reactrouter.com/v7/functions/_react-router_cloudflare.createWorkersKVSessionStorage
[create-arc-table-session-storage]: https://api.reactrouter.com/v7/functions/_react-router_architect.createArcTableSessionStorage
[cookie]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies
[cookie-attrs]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes
[is-cookie]: https://api.reactrouter.com/v7/functions/react-router.isCookie
[create-cookie]: https://api.reactrouter.com/v7/functions/react-router.createCookie
+16
View File
@@ -0,0 +1,16 @@
---
title: Special Files
hidden: true
---
# Special Files
The content of this page has been moved to the following:
- [`react-router.config.ts`](../api/framework-conventions/react-router.config.ts) - Optional configuration file for your app
- [`root.tsx`](../api/framework-conventions/root.tsx) - Required root route that renders the HTML document
- [`routes.ts`](../api/framework-conventions/routes.ts) - Required route configuration mapping URLs to components
- [`entry.client.tsx`](../api/framework-conventions/entry.client.tsx) - Optional client-side entry point for hydration
- [`entry.server.tsx`](../api/framework-conventions/entry.server.tsx) - Optional server-side entry point for rendering
- [`.server` modules](../api/framework-conventions/server-modules) - Server-only modules excluded from client bundles
- [`.client` modules](../api/framework-conventions/client-modules) - Client-only modules excluded from server bundles
+524
View File
@@ -0,0 +1,524 @@
---
title: State Management
---
# State Management
[MODES: framework, data]
<br/>
<br/>
State management in React typically involves maintaining a synchronized cache of server data on the client side. However, when using React Router as your framework, most of the traditional caching solutions become redundant because of how it inherently handles data synchronization.
## Understanding State Management in React
In a typical React context, when we refer to "state management", we're primarily discussing how we synchronize server state with the client. A more apt term could be "cache management" because the server is the source of truth and the client state is mostly functioning as a cache.
Popular caching solutions in React include:
- **[Redux][redux]:** A predictable state container for JavaScript apps.
- **[TanStack Query][tanstack_query]:** Hooks for fetching, caching, and updating asynchronous data in React.
- **[Apollo][apollo]:** A comprehensive state management library for JavaScript that integrates with GraphQL.
In certain scenarios, using these libraries may be warranted. However, with React Router's unique server-focused approach, their utility becomes less prevalent. In fact, most React Router applications forgo them entirely.
## How React Router Simplifies State
React Router seamlessly bridges the gap between the backend and frontend via mechanisms like loaders, actions, and forms with automatic synchronization through revalidation. This offers developers the ability to directly use server state within components without managing a cache, the network communication, or data revalidation, making most client-side caching redundant.
Here's why using typical React state patterns might be an anti-pattern in React Router:
1. **Network-related State:** If your React state is managing anything related to the network—such as data from loaders, pending form submissions, or navigational states—it's likely that you're managing state that React Router already manages:
- **[`useNavigation`][use_navigation]**: This hook gives you access to `navigation.state`, `navigation.formData`, `navigation.location`, etc.
- **[`useFetcher`][use_fetcher]**: This facilitates interaction with `fetcher.state`, `fetcher.formData`, `fetcher.data` etc.
- **[`loaderData`][loader_data]**: Access the data for a route.
- **[`actionData`][action_data]**: Access the data from the latest action.
2. **Storing Data in React Router:** A lot of data that developers might be tempted to store in React state has a more natural home in React Router, such as:
- **URL Search Params:** Parameters within the URL that hold state.
- **[Cookies][cookies]:** Small pieces of data stored on the user's device.
- **[Server Sessions][sessions]:** Server-managed user sessions.
- **Server Caches:** Cached data on the server side for quicker retrieval.
3. **Performance Considerations:** At times, client state is leveraged to avoid redundant data fetching. With React Router, you can use the [`Cache-Control`][cache_control_header] headers within `loader`s, allowing you to tap into the browser's native cache. However, this approach has its limitations and should be used judiciously. It's usually more beneficial to optimize backend queries or implement a server cache. This is because such changes benefit all users and do away with the need for individual browser caches.
As a developer transitioning to React Router, it's essential to recognize and embrace its inherent efficiencies rather than applying traditional React patterns. React Router offers a streamlined solution to state management leading to less code, fresh data, and no state synchronization bugs.
## Examples
### Network Related State
For examples on using React Router's internal state to manage network related state, refer to [Pending UI][pending_ui].
### URL Search Params
Consider a UI that lets the user customize between list view or detail view. Your instinct might be to reach for React state:
```tsx bad lines=[2,6,9]
export function List() {
const [view, setView] = useState("list");
return (
<div>
<div>
<button onClick={() => setView("list")}>
View as List
</button>
<button onClick={() => setView("details")}>
View with Details
</button>
</div>
{view === "list" ? <ListView /> : <DetailView />}
</div>
);
}
```
Now consider you want the URL to update when the user changes the view. Note the state synchronization:
```tsx bad lines=[7,16,24]
import { useNavigate, useSearchParams } from "react-router";
export function List() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [view, setView] = useState(
searchParams.get("view") || "list",
);
return (
<div>
<div>
<button
onClick={() => {
setView("list");
navigate(`?view=list`);
}}
>
View as List
</button>
<button
onClick={() => {
setView("details");
navigate(`?view=details`);
}}
>
View with Details
</button>
</div>
{view === "list" ? <ListView /> : <DetailView />}
</div>
);
}
```
Instead of synchronizing state, you can simply read and set the state in the URL directly with boring old HTML forms:
```tsx good lines=[5,9-16]
import { Form, useSearchParams } from "react-router";
export function List() {
const [searchParams] = useSearchParams();
const view = searchParams.get("view") || "list";
return (
<div>
<Form>
<button name="view" value="list">
View as List
</button>
<button name="view" value="details">
View with Details
</button>
</Form>
{view === "list" ? <ListView /> : <DetailView />}
</div>
);
}
```
### Persistent UI State
Consider a UI that toggles a sidebar's visibility. We have three ways to handle the state:
1. React state
2. Browser local storage
3. Cookies
In this discussion, we'll break down the trade-offs associated with each method.
#### React State
React state provides a simple solution for temporary state storage.
**Pros**:
- **Simple**: Easy to implement and understand.
- **Encapsulated**: State is scoped to the component.
**Cons**:
- **Transient**: Doesn't survive page refreshes, returning to the page later, or unmounting and remounting the component.
**Implementation**:
```tsx
function Sidebar() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen((open) => !open)}>
{isOpen ? "Close" : "Open"}
</button>
<aside hidden={!isOpen}>
<Outlet />
</aside>
</div>
);
}
```
#### Local Storage
To persist state beyond the component lifecycle, browser local storage is a step-up. See our doc on [Client Data][client_data] for more advanced examples.
**Pros**:
- **Persistent**: Maintains state across page refreshes and component mounts/unmounts.
- **Encapsulated**: State is scoped to the component.
**Cons**:
- **Requires Synchronization**: React components must sync up with local storage to initialize and save the current state.
- **Server Rendering Limitation**: The [`window`][window_global] and [`localStorage`][local_storage_global] objects are not accessible during server-side rendering, so state must be initialized in the browser with an effect.
- **UI Flickering**: On initial page loads, the state in local storage may not match what was rendered by the server and the UI will flicker when JavaScript loads.
**Implementation**:
```tsx
function Sidebar() {
const [isOpen, setIsOpen] = useState(false);
// synchronize initially
useLayoutEffect(() => {
const isOpen = window.localStorage.getItem("sidebar");
setIsOpen(isOpen);
}, []);
// synchronize on change
useEffect(() => {
window.localStorage.setItem("sidebar", isOpen);
}, [isOpen]);
return (
<div>
<button onClick={() => setIsOpen((open) => !open)}>
{isOpen ? "Close" : "Open"}
</button>
<aside hidden={!isOpen}>
<Outlet />
</aside>
</div>
);
}
```
In this approach, state must be initialized within an effect. This is crucial to avoid complications during server-side rendering. Directly initializing the React state from `localStorage` will cause errors since `window.localStorage` is unavailable during server rendering.
```tsx bad lines=[4]
function Sidebar() {
const [isOpen, setIsOpen] = useState(
// error: window is not defined
window.localStorage.getItem("sidebar"),
);
// ...
}
```
By initializing the state within an effect, there's potential for a mismatch between the server-rendered state and the state stored in local storage. This discrepancy will lead to brief UI flickering shortly after the page renders and should be avoided.
#### Cookies
Cookies offer a comprehensive solution for this use case. However, this method introduces added preliminary setup before making the state accessible within the component.
**Pros**:
- **Server Rendering**: State is available on the server for rendering and even for server actions.
- **Single Source of Truth**: Eliminates state synchronization hassles.
- **Persistence**: Maintains state across page loads and component mounts/unmounts. State can even persist across devices if you switch to a database-backed session.
- **Progressive Enhancement**: Functions even before JavaScript loads.
**Cons**:
- **Boilerplate**: Requires more code because of the network.
- **Exposed**: The state is not encapsulated to a single component, other parts of the app must be aware of the cookie.
**Implementation**:
First we'll need to create a cookie object:
```tsx
import { createCookie } from "react-router";
export const prefs = createCookie("prefs");
```
Next we set up the server action and loader to read and write the cookie:
```tsx filename=app/routes/sidebar.tsx
import { data, Outlet } from "react-router";
import type { Route } from "./+types/sidebar";
import { prefs } from "./prefs-cookie";
// read the state from the cookie
export async function loader({
request,
}: Route.LoaderArgs) {
const cookieHeader = request.headers.get("Cookie");
const cookie = (await prefs.parse(cookieHeader)) || {};
return data({ sidebarIsOpen: cookie.sidebarIsOpen });
}
// write the state to the cookie
export async function action({
request,
}: Route.ActionArgs) {
const cookieHeader = request.headers.get("Cookie");
const cookie = (await prefs.parse(cookieHeader)) || {};
const formData = await request.formData();
const isOpen = formData.get("sidebar") === "open";
cookie.sidebarIsOpen = isOpen;
return data(isOpen, {
headers: {
"Set-Cookie": await prefs.serialize(cookie),
},
});
}
```
After the server code is set up, we can use the cookie state in our UI:
```tsx
function Sidebar({ loaderData }: Route.ComponentProps) {
const fetcher = useFetcher();
let { sidebarIsOpen } = loaderData;
// use optimistic UI to immediately change the UI state
if (fetcher.formData?.has("sidebar")) {
sidebarIsOpen =
fetcher.formData.get("sidebar") === "open";
}
return (
<div>
<fetcher.Form method="post">
<button
name="sidebar"
value={sidebarIsOpen ? "closed" : "open"}
>
{sidebarIsOpen ? "Close" : "Open"}
</button>
</fetcher.Form>
<aside hidden={!sidebarIsOpen}>
<Outlet />
</aside>
</div>
);
}
```
While this is certainly more code that touches more of the application to account for the network requests and responses, the UX is greatly improved. Additionally, state comes from a single source of truth without any state synchronization required.
In summary, each of the discussed methods offers a unique set of benefits and challenges:
- **React state**: Offers simple but transient state management.
- **Local Storage**: Provides persistence but with synchronization requirements and UI flickering.
- **Cookies**: Delivers robust, persistent state management at the cost of added boilerplate.
None of these are wrong, but if you want to persist the state across visits, cookies offer the best user experience.
### Form Validation and Action Data
Client-side validation can augment the user experience, but similar enhancements can be achieved by leaning more towards server-side processing and letting it handle the complexities.
The following example illustrates the inherent complexities of managing network state, coordinating state from the server, and implementing validation redundantly on both the client and server sides. It's just for illustration, so forgive any obvious bugs or problems you find.
```tsx bad lines=[2,11,27,38,63]
export function Signup() {
// A multitude of React State declarations
const [isSubmitting, setIsSubmitting] = useState(false);
const [userName, setUserName] = useState("");
const [userNameError, setUserNameError] = useState(null);
const [password, setPassword] = useState(null);
const [passwordError, setPasswordError] = useState("");
// Replicating server-side logic in the client
function validateForm() {
setUserNameError(null);
setPasswordError(null);
const errors = validateSignupForm(userName, password);
if (errors) {
if (errors.userName) {
setUserNameError(errors.userName);
}
if (errors.password) {
setPasswordError(errors.password);
}
}
return Boolean(errors);
}
// Manual network interaction handling
async function handleSubmit() {
if (validateForm()) {
setSubmitting(true);
const res = await postJSON("/api/signup", {
userName,
password,
});
const json = await res.json();
setIsSubmitting(false);
// Server state synchronization to the client
if (json.errors) {
if (json.errors.userName) {
setUserNameError(json.errors.userName);
}
if (json.errors.password) {
setPasswordError(json.errors.password);
}
}
}
}
return (
<form
onSubmit={(event) => {
event.preventDefault();
handleSubmit();
}}
>
<p>
<input
type="text"
name="username"
value={userName}
onChange={() => {
// Synchronizing form state for the fetch
setUserName(event.target.value);
}}
/>
{userNameError ? <i>{userNameError}</i> : null}
</p>
<p>
<input
type="password"
name="password"
onChange={(event) => {
// Synchronizing form state for the fetch
setPassword(event.target.value);
}}
/>
{passwordError ? <i>{passwordError}</i> : null}
</p>
<button disabled={isSubmitting} type="submit">
Sign Up
</button>
{isSubmitting ? <BusyIndicator /> : null}
</form>
);
}
```
The backend endpoint, `/api/signup`, also performs validation and sends error feedback. Note that some essential validation, like detecting duplicate usernames, can only be done server-side using information the client doesn't have access to.
```tsx bad
export async function signupHandler(request: Request) {
const errors = await validateSignupRequest(request);
if (errors) {
return { ok: false, errors: errors };
}
await signupUser(request);
return { ok: true, errors: null };
}
```
Now, let's contrast this with a React Router-based implementation. The action remains consistent, but the component is vastly simplified due to the direct utilization of server state via `actionData`, and leveraging the network state that React Router inherently manages.
```tsx filename=app/routes/signup.tsx good lines=[20-22]
import { useNavigation } from "react-router";
import type { Route } from "./+types/signup";
export async function action({
request,
}: ActionFunctionArgs) {
const errors = await validateSignupRequest(request);
if (errors) {
return { ok: false, errors: errors };
}
await signupUser(request);
return { ok: true, errors: null };
}
export function Signup({
actionData,
}: Route.ComponentProps) {
const navigation = useNavigation();
const userNameError = actionData?.errors?.userName;
const passwordError = actionData?.errors?.password;
const isSubmitting = navigation.formAction === "/signup";
return (
<Form method="post">
<p>
<input type="text" name="username" />
{userNameError ? <i>{userNameError}</i> : null}
</p>
<p>
<input type="password" name="password" />
{passwordError ? <i>{passwordError}</i> : null}
</p>
<button disabled={isSubmitting} type="submit">
Sign Up
</button>
{isSubmitting ? <BusyIndicator /> : null}
</Form>
);
}
```
The extensive state management from our previous example is distilled into just three code lines. We eliminate the necessity for React state, change event listeners, submit handlers, and state management libraries for such network interactions.
Direct access to the server state is made possible through `actionData`, and network state through `useNavigation` (or `useFetcher`).
As bonus party trick, the form is functional even before JavaScript loads (see [Progressive Enhancement][progressive_enhancement]). Instead of React Router managing the network operations, the default browser behaviors step in.
If you ever find yourself entangled in managing and synchronizing state for network operations, React Router likely offers a more elegant solution.
[redux]: https://redux.js.org/
[tanstack_query]: https://tanstack.com/query/latest
[apollo]: https://www.apollographql.com/
[use_navigation]: https://api.reactrouter.com/v7/functions/react-router.useNavigation
[use_fetcher]: https://api.reactrouter.com/v7/functions/react-router.useFetcher
[loader_data]: ../start/framework/data-loading
[action_data]: ../start/framework/actions
[cookies]: ./sessions-and-cookies#cookies
[sessions]: ./sessions-and-cookies#sessions
[cache_control_header]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
[pending_ui]: ../start/framework/pending-ui
[client_data]: ../how-to/client-data
[window_global]: https://developer.mozilla.org/en-US/docs/Web/API/Window/window
[local_storage_global]: https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
[progressive_enhancement]: ./progressive-enhancement
+87
View File
@@ -0,0 +1,87 @@
---
title: Styling
---
# Styling
[MODES: framework]
<br/>
<br/>
Framework mode uses the React Router Vite plugin, so the styling story is mostly just Vite's styling story.
React Router does not have a separate CSS pipeline for Framework mode. In practice, there are three patterns that matter:
1. Import CSS as a side effect
2. Use the route module `links` export
3. Render a stylesheet `<link>` directly
## Side-Effect CSS Imports
Because Framework mode uses Vite, you can import CSS files as side effects:
```tsx filename=app/root.tsx
import "./app.css";
```
```tsx filename=app/routes/dashboard.tsx
import "./dashboard.css";
```
This is often the simplest option. Global styles can be imported in `root.tsx`, and route or component styles can be imported next to the module that uses them.
## `links` Export
React Router also supports adding stylesheets through the route module `links` export.
This is useful when you want a stylesheet URL from Vite and need React Router to render a real `<link rel="stylesheet">` tag for the route:
```tsx filename=app/routes/dashboard.tsx
import dashboardHref from "./dashboard.css?url";
export function links() {
return [{ rel: "stylesheet", href: dashboardHref }];
}
```
The `links` export feeds the [`<Links />`][links-component] component in your root route. This is the React Router-specific styling API in Framework mode. For more on route module exports, see [Route Module][route-module].
## Direct `<link>` Rendering
If you're using React 19, you can also render a stylesheet `<link>` directly in your route component:
```tsx filename=app/routes/dashboard.tsx
import dashboardHref from "./dashboard.css?url";
export default function Dashboard() {
return (
<>
<link
rel="stylesheet"
href={dashboardHref}
precedence="default"
/>
<h1>Dashboard</h1>
</>
);
}
```
This uses React's built-in [`<link>`][react-link] support, which hoists the stylesheet into the document `<head>`. That gives you another way to colocate stylesheet tags with the route that needs them.
## Everything Else
For CSS Modules, Tailwind, PostCSS, Sass, Vanilla Extract, and other styling tools, use the normal Vite setup for those tools.
See:
- [Vite CSS Features][vite-css]
- [Vite Static Asset Handling][vite-assets]
- [`<Links />`][links-component]
[links-component]: ../api/components/Links
[react-link]: https://react.dev/reference/react-dom/components/link
[route-module]: ../start/framework/route-module
[vite-assets]: https://vite.dev/guide/assets.html
[vite-css]: https://vite.dev/guide/features.html#css
+82
View File
@@ -0,0 +1,82 @@
---
title: Type Safety
---
# Type Safety
[MODES: framework]
<br/>
<br/>
If you haven't done so already, check out our guide for [setting up type safety][route-module-type-safety] in a new project.
React Router generates types for each route in your app to provide type safety for the route module exports.
For example, let's say you have a `products/:id` route configured:
```ts filename=app/routes.ts
import {
type RouteConfig,
route,
} from "@react-router/dev/routes";
export default [
route("products/:id", "./routes/product.tsx"),
] satisfies RouteConfig;
```
You can import route-specific types like so:
```tsx filename=app/routes/product.tsx
import type { Route } from "./+types/product";
// types generated for this route 👆
export function loader({ params }: Route.LoaderArgs) {
// 👆 { id: string }
return { planet: `world #${params.id}` };
}
export default function Component({
loaderData, // 👈 { planet: string }
}: Route.ComponentProps) {
return <h1>Hello, {loaderData.planet}!</h1>;
}
```
## How it works
React Router's type generation executes your route config (`app/routes.ts` by default) to determine the routes for your app.
It then generates a `+types/<route file>.d.ts` for each route within a special `.react-router/types/` directory.
With [`rootDirs` configured][route-module-type-safety], TypeScript can import these generated files as if they were right next to their corresponding route modules.
For a deeper dive into some of the design decisions, check out our [type inference decision doc](https://github.com/remix-run/react-router/blob/main/decisions/0012-type-inference.md).
[route-module-type-safety]: ../how-to/route-module-type-safety
## `typegen` command
You can manually generate types with the `typegen` command:
```sh
react-router typegen
```
The following types are generated for each route:
- `LoaderArgs`
- `ClientLoaderArgs`
- `ActionArgs`
- `ClientActionArgs`
- `HydrateFallbackProps`
- `ComponentProps` (for the `default` export)
- `ErrorBoundaryProps`
### --watch
If you run `react-router dev` — or if your custom server calls `vite.createServer` — then React Router's Vite plugin is already generating up-to-date types for you.
But if you really need to run type generation on its own, you can also use `--watch` to automatically regenerate types as files change:
```sh
react-router typegen --watch
```
+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 });
};
```
+39
View File
@@ -0,0 +1,39 @@
---
title: React Router Home
order: 1
---
# React Router Home
React Router is a multi-strategy router for React bridging the gap from React 18 to React 19. You can use it maximally as a React framework or as minimally as you want.
## Getting Started
There are three primary ways, or "modes", to use it in your app, so there are three guides to get you started.
- [Declarative](./start/declarative/installation)
- [Data](./start/data/installation)
- [Framework](./start/framework/installation)
Learn which mode is right for you in [Picking a Mode](./start/modes).
## Using These Guides
Across the docs you'll see the following icons:
[MODES: framework, data, declarative]
<p></p>
These icons indicate which mode the content is relevant to.
Additional auto-generated reference documentation is available:
[Autogenerated Reference Docs ↗](https://api.reactrouter.com/v7/)
## Upgrading
If you are caught up on future flags, upgrading from React Router v6 or Remix v2 is generally non-breaking. Remix v2 apps are encouraged to upgrade to React Router v7.
- [Upgrade from v6](./upgrading/v6)
- [Upgrade from Remix](./upgrading/remix)
+138
View File
@@ -0,0 +1,138 @@
---
title: Actions
order: 5
---
# Actions
[MODES: data]
## Defining Actions
Data mutations are done through Route actions defined on the `action` property of a route object. When the action completes, all loader data on the page is revalidated to keep your UI in sync with the data without writing any code to do it.
```tsx
import { createBrowserRouter } from "react-router";
import { someApi } from "./api";
let router = createBrowserRouter([
{
path: "/projects/:projectId",
Component: Project,
action: async ({ request }) => {
let formData = await request.formData();
let title = formData.get("title");
let project = await someApi.updateProject({ title });
return project;
},
},
]);
```
## Calling Actions
Actions are called declaratively through `<Form>` and imperatively through `useSubmit` (or `<fetcher.Form>` and `fetcher.submit`) by referencing the route's path and a "post" method.
### Calling actions with a Form
```tsx
import { Form } from "react-router";
function SomeComponent() {
return (
<Form action="/projects/123" method="post">
<input type="text" name="title" />
<button type="submit">Submit</button>
</Form>
);
}
```
This will cause a navigation and a new entry will be added to the browser history.
### Calling actions with useSubmit
You can submit form data to an action imperatively with `useSubmit`.
```tsx
import { useCallback } from "react";
import { useSubmit } from "react-router";
import { useFakeTimer } from "fake-lib";
function useQuizTimer() {
let submit = useSubmit();
let cb = useCallback(() => {
submit(
{ quizTimedOut: true },
{ action: "/end-quiz", method: "post" },
);
}, []);
let tenMinutes = 10 * 60 * 1000;
useFakeTimer(tenMinutes, cb);
}
```
This will cause a navigation and a new entry will be added to the browser history.
### Calling actions with a fetcher
Fetchers allow you to submit data to actions (and loaders) without causing a navigation (no new entries in the browser history).
```tsx
import { useFetcher } from "react-router";
function Task() {
let fetcher = useFetcher();
let busy = fetcher.state !== "idle";
return (
<fetcher.Form method="post" action="/update-task/123">
<input type="text" name="title" />
<button type="submit">
{busy ? "Saving..." : "Save"}
</button>
</fetcher.Form>
);
}
```
They also have the imperative `submit` method.
```tsx
fetcher.submit(
{ title: "New Title" },
{ action: "/update-task/123", method: "post" },
);
```
See the [Using Fetchers][fetchers] guide for more information.
## Accessing Action Data
Actions can return data available through `useActionData` in the route component or `fetcher.data` when using a fetcher.
```tsx
function Project() {
let actionData = useActionData();
return (
<div>
<h1>Project</h1>
<Form method="post">
<input type="text" name="title" />
<button type="submit">Submit</button>
</Form>
{actionData ? (
<p>{actionData.title} updated</p>
) : null}
</div>
);
}
```
---
Next: [Navigating](./navigating)
[fetchers]: ../../how-to/fetchers
+198
View File
@@ -0,0 +1,198 @@
---
title: Custom Framework
order: 8
---
# Custom Framework
[MODES: data]
## Introduction
Instead of using `@react-router/dev`, you can integrate React Router's framework features (like loaders, actions, fetchers, etc.) into your own bundler and server abstractions with Data Mode.
## Client Rendering
### 1. Create a Router
The browser runtime API that enables route module APIs (loaders, actions, etc.) is `createBrowserRouter`.
It takes an array of route objects that support loaders, actions, error boundaries and more. The React Router Vite plugin creates one of these from `routes.ts`, but you can create one manually (or with an abstraction) and use your own bundler.
```tsx
import { createBrowserRouter } from "react-router";
let router = createBrowserRouter([
{
path: "/",
Component: Root,
children: [
{
path: "shows/:showId",
Component: Show,
loader: ({ request, params }) =>
fetch(`/api/show/${params.showId}.json`, {
signal: request.signal,
}),
},
],
},
]);
```
### 2. Render the Router
To render the router in the browser, use `<RouterProvider>`.
```tsx
import {
createBrowserRouter,
RouterProvider,
} from "react-router";
import { createRoot } from "react-dom/client";
createRoot(document.getElementById("root")).render(
<RouterProvider router={router} />,
);
```
### 3. Lazy Loading
Routes can take most of their definition lazily with the `lazy` property.
```tsx
createBrowserRouter([
{
path: "/show/:showId",
lazy: {
loader: async () =>
(await import("./show.loader.js")).loader,
action: async () =>
(await import("./show.action.js")).action,
Component: async () =>
(await import("./show.component.js")).Component,
},
},
]);
```
## Server Rendering
To server render a custom setup, there are a few server APIs available for rendering and data loading.
This guide simply gives you some ideas about how it works. For deeper understanding, please see the [Custom Framework Example Repo](https://github.com/remix-run/custom-react-router-framework-example)
### 1. Define Your Routes
Routes are the same kinds of objects on the server as the client.
```tsx
export default [
{
path: "/",
Component: Root,
children: [
{
path: "shows/:showId",
Component: Show,
loader: ({ params }) => {
return db.loadShow(params.id);
},
},
],
},
];
```
### 2. Create a static handler
Turn your routes into a request handler with `createStaticHandler`:
```tsx
import { createStaticHandler } from "react-router";
import routes from "./some-routes";
let { query, dataRoutes } = createStaticHandler(routes);
```
### 3. Get Routing Context and Render
React Router works with web fetch [Requests](https://developer.mozilla.org/en-US/docs/Web/API/Request), so if your server doesn't, you'll need to adapt whatever objects it uses to a web fetch `Request` object.
This step assumes your server receives `Request` objects.
```tsx
import { renderToString } from "react-dom/server";
import {
createStaticHandler,
createStaticRouter,
StaticRouterProvider,
} from "react-router";
import routes from "./some-routes.js";
let { query, dataRoutes } = createStaticHandler(routes);
export async function handler(request: Request) {
// 1. run actions/loaders to get the routing context with `query`
let context = await query(request);
// If `query` returns a Response, send it raw (a route probably a redirected)
if (context instanceof Response) {
return context;
}
// 2. Create a static router for SSR
let router = createStaticRouter(dataRoutes, context);
// 3. Render everything with StaticRouterProvider
let html = renderToString(
<StaticRouterProvider
router={router}
context={context}
/>,
);
// Setup headers from action and loaders from deepest match
let leaf = context.matches[context.matches.length - 1];
let actionHeaders = context.actionHeaders[leaf.route.id];
let loaderHeaders = context.loaderHeaders[leaf.route.id];
let headers = new Headers(actionHeaders);
if (loaderHeaders) {
for (let [key, value] of loaderHeaders.entries()) {
headers.append(key, value);
}
}
headers.set("Content-Type", "text/html; charset=utf-8");
// 4. send a response
return new Response(`<!DOCTYPE html>${html}`, {
status: context.statusCode,
headers,
});
}
```
### 4. Hydrate in the browser
Hydration data is embedded onto `window.__staticRouterHydrationData`, use that to initialize your client side router and render a `<RouterProvider>`.
```tsx
import { StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { RouterProvider } from "react-router/dom";
import routes from "./app/routes.js";
import { createBrowserRouter } from "react-router";
let router = createBrowserRouter(routes, {
hydrationData: window.__staticRouterHydrationData,
});
hydrateRoot(
document,
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
);
```
+44
View File
@@ -0,0 +1,44 @@
---
title: Data Loading
order: 4
---
# Data Loading
[MODES: data]
## Providing Data
Data is provided to route components from route loaders:
```tsx
createBrowserRouter([
{
path: "/",
loader: async () => {
// return data from here
return { records: await getSomeRecords() };
},
Component: MyRoute,
},
]);
```
## Accessing Data
The data is available in route components with `useLoaderData`.
```tsx
import { useLoaderData } from "react-router";
function MyRoute() {
const { records } = useLoaderData();
return <div>{records.length}</div>;
}
```
As the user navigates between routes, the loaders are called before the route component is rendered.
---
Next: [Actions](./actions)
+4
View File
@@ -0,0 +1,4 @@
---
title: Data Mode
order: 3
---
+56
View File
@@ -0,0 +1,56 @@
---
title: Installation
order: 1
---
# Installation
[MODES: data]
## Bootstrap with a Bundler Template
You can start with a React template from Vite and choose "React", otherwise bootstrap your application however you prefer (Parcel, Webpack, etc).
```shellscript nonumber
npx create-vite@latest
```
## Install React Router
Next install React Router from npm:
```shellscript nonumber
npm i react-router
```
## Create a Router and Render
Create a router and pass it to `RouterProvider`:
```tsx lines=[3-4,6-11,16]
import React from "react";
import ReactDOM from "react-dom/client";
import { createBrowserRouter } from "react-router";
import { RouterProvider } from "react-router/dom";
const router = createBrowserRouter([
{
path: "/",
element: <div>Hello World</div>,
},
]);
const root = document.getElementById("root");
ReactDOM.createRoot(root).render(
<RouterProvider router={router} />,
);
```
<docs-info>Data Routers should not be held in React state. You should create your router
once outside of the React tree and pass it to `<RouterProvider>`. You can use
`patchRoutesOnNavigation` to add additional routes programmatically.</docs-info>
---
Next: [Routing](./routing)
+12
View File
@@ -0,0 +1,12 @@
---
title: Navigating
order: 6
---
# Navigating
Navigating in Data Mode is the same as Framework Mode, please see the [Navigating](../framework/navigating) guide for more information.
---
Next: [Pending UI](./pending-ui)
+12
View File
@@ -0,0 +1,12 @@
---
title: Pending UI
order: 7
---
# Pending UI
Pending UI is the same as Framework Mode, please see the [Pending UI](../framework/pending-ui) guide for more information.
---
Next: [Custom Framework](./custom)
+268
View File
@@ -0,0 +1,268 @@
---
title: Route Object
order: 3
---
# Route Object
[MODES: data]
## Introduction
The objects passed to `createBrowserRouter` are called Route Objects.
```tsx lines=[2-5]
createBrowserRouter([
{
path: "/",
Component: App,
},
]);
```
Route modules are the foundation of React Router's data features, they define:
- data loading
- actions
- revalidation
- error boundaries
- and more
This guide is a quick overview of every route object feature.
## Component
The `Component` property in a route object defines the component that will render when the route matches.
```tsx lines=[4]
createBrowserRouter([
{
path: "/",
Component: MyRouteComponent,
},
]);
function MyRouteComponent() {
return (
<div>
<h1>Look ma!</h1>
<p>
I'm still using React Router after like 10 years.
</p>
</div>
);
}
```
## `middleware`
Route [middleware][middleware] runs sequentially before and after navigations. This gives you a singular place to do things like logging and authentication. The `next` function continues down the chain, and on the leaf route the `next` function executes the loaders/actions for the navigation.
```tsx
createBrowserRouter([
{
path: "/",
middleware: [loggingMiddleware],
loader: rootLoader,
Component: Root,
children: [{
path: 'auth',
middleware: [authMiddleware],
loader: authLoader,
Component: Auth,
children: [...]
}]
},
]);
async function loggingMiddleware({ request }, next) {
let url = new URL(request.url);
console.log(`Starting navigation: ${url.pathname}${url.search}`);
const start = performance.now();
await next();
const duration = performance.now() - start;
console.log(`Navigation completed in ${duration}ms`);
}
const userContext = createContext<User>();
async function authMiddleware ({ context }) {
const userId = getUserId();
if (!userId) {
throw redirect("/login");
}
context.set(userContext, await getUserById(userId));
};
```
See also:
- [Middleware][middleware]
## `loader`
Route loaders provide data to route components before they are rendered.
```tsx
import {
useLoaderData,
createBrowserRouter,
} from "react-router";
createBrowserRouter([
{
path: "/",
loader: loader,
Component: MyRoute,
},
]);
async function loader({ params }) {
return { message: "Hello, world!" };
}
function MyRoute() {
let data = useLoaderData();
return <h1>{data.message}</h1>;
}
```
See also:
- [`loader` params][loader-params]
## `action`
Route actions allow server-side data mutations with automatic revalidation of all loader data on the page when called from `<Form>`, `useFetcher`, and `useSubmit`.
```tsx
import {
createBrowserRouter,
useLoaderData,
useActionData,
Form,
} from "react-router";
import { TodoList } from "~/components/TodoList";
createBrowserRouter([
{
path: "/items",
action: action,
loader: loader,
Component: Items,
},
]);
async function action({ request }) {
const data = await request.formData();
const todo = await fakeDb.addItem({
title: data.get("title"),
});
return { ok: true };
}
// this data will be revalidated after the action completes...
async function loader() {
const items = await fakeDb.getItems();
return { items };
}
// ...so that the list here is updated automatically
export default function Items() {
let data = useLoaderData();
return (
<div>
<List items={data.items} />
<Form method="post" navigate={false}>
<input type="text" name="title" />
<button type="submit">Create Todo</button>
</Form>
</div>
);
}
```
## `shouldRevalidate`
Loader data is automatically revalidated after certain events like navigations and form submissions.
This hook enables you to opt in or out of the default revalidation behavior. The default behavior is nuanced to avoid calling loaders unnecessarily.
A route loader is revalidated when:
- its own route params change
- any change to URL search params
- after an action is called and returns a non-error status code
By defining this function, you opt out of the default behavior completely and can manually control when loader data is revalidated for navigations and form submissions.
```tsx
import type { ShouldRevalidateFunctionArgs } from "react-router";
function shouldRevalidate(
arg: ShouldRevalidateFunctionArgs,
) {
return true; // false
}
createBrowserRouter([
{
path: "/",
shouldRevalidate: shouldRevalidate,
Component: MyRoute,
},
]);
```
[`ShouldRevalidateFunctionArgs` Reference Documentation ↗](https://api.reactrouter.com/v7/interfaces/react-router.ShouldRevalidateFunctionArgs.html)
Please note the default behavior is different in [Framework Mode](../modes).
## `lazy`
Most properties can be lazily imported to reduce the initial bundle size.
```tsx
createBrowserRouter([
{
path: "/app",
lazy: async () => {
// load component and loader in parallel before rendering
const [Component, loader] = await Promise.all([
import("./app"),
import("./app-loader"),
]);
return { Component, loader };
},
},
]);
```
## `handle`
Route handle allows apps to add anything to a route match in `useMatches` to create abstractions (like breadcrumbs, etc.).
```tsx
createBrowserRouter([
{
path: "/app",
handle: {
breadcrumb: "App",
},
},
]);
```
See also:
- [`useMatches`][use-matches]
---
Next: [Data Loading](./data-loading)
[loader-params]: https://api.reactrouter.com/v7/interfaces/react-router.LoaderFunctionArgs
[middleware]: ../../how-to/middleware
[use-matches]: ../../api/hooks/useMatches
+281
View File
@@ -0,0 +1,281 @@
---
title: Routing
order: 2
---
# Routing
[MODES: data]
## Configuring Routes
Routes are configured as the first argument to `createBrowserRouter`. At a minimum, you need a path and component:
```tsx
import { createBrowserRouter } from "react-router";
function Root() {
return <h1>Hello world</h1>;
}
const router = createBrowserRouter([
{ path: "/", Component: Root },
]);
```
Here is a larger sample route config:
```ts filename=app/routes.ts
createBrowserRouter([
{
path: "/",
Component: Root,
children: [
{ index: true, Component: Home },
{ path: "about", Component: About },
{
path: "auth",
Component: AuthLayout,
children: [
{ path: "login", Component: Login },
{ path: "register", Component: Register },
],
},
{
path: "concerts",
children: [
{ index: true, Component: ConcertsHome },
{ path: ":city", Component: ConcertsCity },
{ path: "trending", Component: ConcertsTrending },
],
},
],
},
]);
```
## Route Objects
Route objects define the behavior of a route beyond just the path and component, like data loading and actions. We'll go into more detail in the [Route Object guide](./route-object), but here's a quick example of a loader.
```tsx filename=app/team.tsx
import {
createBrowserRouter,
useLoaderData,
} from "react-router";
createBrowserRouter([
{
path: "/teams/:teamId",
loader: async ({ params }) => {
let team = await fetchTeam(params.teamId);
return { name: team.name };
},
Component: Team,
},
]);
function Team() {
let data = useLoaderData();
return <h1>{data.name}</h1>;
}
```
## Nested Routes
Routes can be nested inside parent routes through `children`.
```ts filename=app/routes.ts
createBrowserRouter([
{
path: "/dashboard",
Component: Dashboard,
children: [
{ index: true, Component: Home },
{ path: "settings", Component: Settings },
],
},
]);
```
The path of the parent is automatically included in the child, so this config creates both `"/dashboard"` and `"/dashboard/settings"` URLs.
Child routes are rendered through the `<Outlet/>` in the parent route.
```tsx filename=app/dashboard.tsx
import { Outlet } from "react-router";
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* will either be <Home> or <Settings> */}
<Outlet />
</div>
);
}
```
## Layout Routes
Omitting the `path` in a route creates new [Nested Routes](#nested-routes) for its children without adding any segments to the URL.
```tsx lines=[3,16]
createBrowserRouter([
{
// no path on this parent route, just the component
Component: MarketingLayout,
children: [
{ index: true, Component: Home },
{ path: "contact", Component: Contact },
],
},
{
path: "projects",
children: [
{ index: true, Component: ProjectsHome },
{
// again, no path, just a component for the layout
Component: ProjectLayout,
children: [
{ path: ":pid", Component: Project },
{ path: ":pid/edit", Component: EditProject },
],
},
],
},
]);
```
Note that:
- `Home` and `Contact` will be rendered into the `MarketingLayout` outlet
- `Project` and `EditProject` will be rendered into the `ProjectLayout` outlet while `ProjectsHome` will not.
## Index Routes
Index routes are defined by setting `index: true` on a route object without a path.
```ts
{ index: true, Component: Home }
```
Index routes render into their parent's [Outlet][outlet] at their parent's URL (like a default child route).
```ts lines=[4,5,10,11]
import { createBrowserRouter } from "react-router";
createBrowserRouter([
// renders at "/"
{ index: true, Component: Home },
{
Component: Dashboard,
path: "/dashboard",
children: [
// renders at "/dashboard"
{ index: true, Component: DashboardHome },
{ path: "settings", Component: DashboardSettings },
],
},
]);
```
Note that index routes can't have children.
## Prefix Route
A route with just a path and no component creates a group of routes with a path prefix.
```tsx lines=[3]
createBrowserRouter([
{
// no component, just a path
path: "/projects",
children: [
{ index: true, Component: ProjectsHome },
{ path: ":pid", Component: Project },
{ path: ":pid/edit", Component: EditProject },
],
},
]);
```
This creates the routes `/projects`, `/projects/:pid`, and `/projects/:pid/edit` without introducing a layout component.
## Dynamic Segments
If a path segment starts with `:` then it becomes a "dynamic segment". When the route matches the URL, the dynamic segment will be parsed from the URL and provided as `params` to other router APIs.
```ts lines=[2]
{
path: "teams/:teamId",
loader: async ({ params }) => {
// params are available in loaders/actions
let team = await fetchTeam(params.teamId);
return { name: team.name };
},
Component: Team,
}
```
```tsx
import { useParams } from "react-router";
function Team() {
// params are available in components through useParams
let params = useParams();
// ...
}
```
You can have multiple dynamic segments in one route path:
```ts
{
path: "c/:categoryId/p/:productId";
}
```
## Optional Segments
You can make a route segment optional by adding a `?` to the end of the segment.
```ts
{
path: ":lang?/categories";
}
```
You can have optional static segments, too:
```ts
{
path: "users/:userId/edit?";
}
```
## Splats
Also known as "catchall" and "star" segments. If a route path pattern ends with `/*` then it will match any characters following the `/`, including other `/` characters.
```ts
{
path: "files/*";
loader: async ({ params }) => {
params["*"]; // will contain the remaining URL after files/
};
}
```
You can destructure the `*`, you just have to assign it a new name. A common name is `splat`:
```tsx
const { "*": splat } = params;
```
---
Next: [Route Object](./route-object)
[outlet]: https://api.reactrouter.com/v7/functions/react-router.Outlet.html
+8
View File
@@ -0,0 +1,8 @@
---
title: Testing
order: 9
---
# Testing
You can use `createRoutesStub` in data and framework modes. Please refer to the [Testing Guide](../framework/testing).
+4
View File
@@ -0,0 +1,4 @@
---
title: Declarative Mode
order: 4
---
@@ -0,0 +1,43 @@
---
title: Installation
order: 1
---
# Installation
[MODES: declarative]
## Introduction
You can start with a React template from Vite and choose "React", otherwise bootstrap your application however you prefer.
```shellscript nonumber
npx create-vite@latest
```
Next install React Router from npm:
```shellscript nonumber
npm i react-router
```
Finally, render a `<BrowserRouter>` around your application:
```tsx lines=[3,9-11]
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router";
import App from "./app";
const root = document.getElementById("root");
ReactDOM.createRoot(root).render(
<BrowserRouter>
<App />
</BrowserRouter>,
);
```
---
Next: [Routing](./routing)
+133
View File
@@ -0,0 +1,133 @@
---
title: Navigating
order: 3
---
# Navigating
[MODES: declarative]
## Introduction
Users navigate your application with `<Link>`, `<NavLink>`, and `useNavigate`.
## NavLink
This component is for navigation links that need to render an active state.
```tsx
import { NavLink } from "react-router";
export function MyAppNav() {
return (
<nav>
<NavLink to="/" end>
Home
</NavLink>
<NavLink to="/trending" end>
Trending Concerts
</NavLink>
<NavLink to="/concerts">All Concerts</NavLink>
<NavLink to="/account">Account</NavLink>
</nav>
);
}
```
Whenever a `NavLink` is active, it will automatically have an `.active` class name for easy styling with CSS:
```css
a.active {
color: red;
}
```
It also has callback props on `className`, `style`, and `children` with the active state for inline styling or conditional rendering:
```tsx
// className
<NavLink
to="/messages"
className={({ isActive }) =>
isActive ? "text-red-500" : "text-black"
}
>
Messages
</NavLink>
```
```tsx
// style
<NavLink
to="/messages"
style={({ isActive }) => ({
color: isActive ? "red" : "black",
})}
>
Messages
</NavLink>
```
```tsx
// children
<NavLink to="/message">
{({ isActive }) => (
<span className={isActive ? "active" : ""}>
{isActive ? "👉" : ""} Tasks
</span>
)}
</NavLink>
```
## Link
Use `<Link>` when the link doesn't need active styling:
```tsx
import { Link } from "react-router";
export function LoggedOutMessage() {
return (
<p>
You've been logged out.{" "}
<Link to="/login">Login again</Link>
</p>
);
}
```
## useNavigate
This hook allows the programmer to navigate the user to a new page without the user interacting.
For normal navigation, it's best to use `Link` or `NavLink`. They provide a better default user experience like keyboard events, accessibility labeling, "open in new window", right click context menus, etc.
Reserve usage of `useNavigate` to situations where the user is _not_ interacting but you need to navigate, for example:
- After a form submission completes
- Logging them out after inactivity
- Timed UIs like quizzes, etc.
```tsx
import { useNavigate } from "react-router";
export function LoginPage() {
let navigate = useNavigate();
return (
<>
<MyHeader />
<MyLoginForm
onSuccess={() => {
navigate("/dashboard");
}}
/>
<MyFooter />
</>
);
}
```
---
Next: [Url values](./url-values)
+237
View File
@@ -0,0 +1,237 @@
---
title: Routing
order: 2
---
# Routing
[MODES: declarative]
## Configuring Routes
Routes are configured by rendering `<Routes>` and `<Route>` that couple URL segments to UI elements.
```tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter, Routes, Route } from "react-router";
import App from "./app";
const root = document.getElementById("root");
ReactDOM.createRoot(root).render(
<BrowserRouter>
<Routes>
<Route path="/" element={<App />} />
</Routes>
</BrowserRouter>,
);
```
Here's a larger sample config:
```tsx
<Routes>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route element={<AuthLayout />}>
<Route path="login" element={<Login />} />
<Route path="register" element={<Register />} />
</Route>
<Route path="concerts">
<Route index element={<ConcertsHome />} />
<Route path=":city" element={<City />} />
<Route path="trending" element={<Trending />} />
</Route>
</Routes>
```
## Nested Routes
Routes can be nested inside parent routes.
```tsx
<Routes>
<Route path="dashboard" element={<Dashboard />}>
<Route index element={<Home />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
```
The path of the parent is automatically included in the child, so this config creates both `"/dashboard"` and `"/dashboard/settings"` URLs.
Child routes are rendered through the `<Outlet/>` in the parent route.
```tsx filename=app/dashboard.tsx
import { Outlet } from "react-router";
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* will either be <Home/> or <Settings/> */}
<Outlet />
</div>
);
}
```
## Layout Routes
Routes _without_ a `path` create new nesting for their children, but they don't add any segments to the URL.
```tsx lines=[2,9]
<Routes>
<Route element={<MarketingLayout />}>
<Route index element={<MarketingHome />} />
<Route path="contact" element={<Contact />} />
</Route>
<Route path="projects">
<Route index element={<ProjectsHome />} />
<Route element={<ProjectsLayout />}>
<Route path=":pid" element={<Project />} />
<Route path=":pid/edit" element={<EditProject />} />
</Route>
</Route>
</Routes>
```
## Index Routes
Index routes render into their parent's `<Outlet/>` at their parent's URL (like a default child route). They are configured with the `index` prop:
```tsx lines=[4,8]
<Routes>
<Route path="/" element={<Root />}>
{/* renders into the outlet in <Root> at "/" */}
<Route index element={<Home />} />
<Route path="dashboard" element={<Dashboard />}>
{/* renders into the outlet in <Dashboard> at "/dashboard" */}
<Route index element={<DashboardHome />} />
<Route path="settings" element={<Settings />} />
</Route>
</Route>
</Routes>
```
Note that index routes can't have children. If you're expecting that behavior, you probably want a [layout route](#layout-routes).
## Route Prefixes
A `<Route path>` _without_ an `element` prop adds a path prefix to its child routes, without introducing a parent layout.
```tsx filename=app/routes.ts lines=[1]
<Route path="projects">
<Route index element={<ProjectsHome />} />
<Route element={<ProjectsLayout />}>
<Route path=":pid" element={<Project />} />
<Route path=":pid/edit" element={<EditProject />} />
</Route>
</Route>
```
## Dynamic Segments
If a path segment starts with `:` then it becomes a "dynamic segment". When the route matches the URL, the dynamic segment will be parsed from the URL and provided as `params` to other router APIs like `useParams`.
```tsx
<Route path="teams/:teamId" element={<Team />} />
```
```tsx filename=app/team.tsx
import { useParams } from "react-router";
export default function Team() {
let params = useParams();
// params.teamId
}
```
You can have multiple dynamic segments in one route path:
```tsx
<Route
path="/c/:categoryId/p/:productId"
element={<Product />}
/>
```
```tsx filename=app/category-product.tsx
import { useParams } from "react-router";
export default function CategoryProduct() {
let { categoryId, productId } = useParams();
// ...
}
```
You should ensure that all dynamic segments in a given path are unique. Otherwise, as the `params` object is populated - latter dynamic segment values will override earlier values.
## Optional Segments
You can make a route segment optional by adding a `?` to the end of the segment.
```tsx
<Route path=":lang?/categories" element={<Categories />} />
```
You can have optional static segments, too:
```tsx
<Route path="users/:userId/edit?" element={<User />} />
```
## Splats
Also known as "catchall" and "star" segments. If a route path pattern ends with `/*` then it will match any characters following the `/`, including other `/` characters.
```tsx
<Route path="files/*" element={<File />} />
```
```tsx
let params = useParams();
// params["*"] will contain the remaining URL after files/
let filePath = params["*"];
```
You can destructure the `*`, you just have to assign it a new name. A common name is `splat`:
```tsx
let { "*": splat } = useParams();
```
## Linking
Link to routes from your UI with `Link` and `NavLink`
```tsx
import { NavLink, Link } from "react-router";
function Header() {
return (
<nav>
{/* NavLink makes it easy to show active states */}
<NavLink
to="/"
className={({ isActive }) =>
isActive ? "active" : ""
}
>
Home
</NavLink>
<Link to="/concerts/salt-lake-city">Concerts</Link>
</nav>
);
}
```
---
Next: [Navigating](./navigating)
@@ -0,0 +1,65 @@
---
title: URL Values
---
# URL Values
[MODES: declarative]
## Route Params
Route params are the parsed values from a dynamic segment.
```tsx
<Route path="/concerts/:city" element={<City />} />
```
In this case, `:city` is the dynamic segment. The parsed value for that city will be available from `useParams`
```tsx
import { useParams } from "react-router";
function City() {
let { city } = useParams();
let data = useFakeDataLibrary(`/api/v2/cities/${city}`);
// ...
}
```
## URL Search Params
Search params are the values after a `?` in the URL. They are accessible from `useSearchParams`, which returns an instance of [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams)
```tsx
function SearchResults() {
let [searchParams] = useSearchParams();
return (
<div>
<p>
You searched for <i>{searchParams.get("q")}</i>
</p>
<FakeSearchResults />
</div>
);
}
```
## Location Object
React Router creates a custom `location` object with some useful information on it accessible with `useLocation`.
```tsx
function useAnalytics() {
let location = useLocation();
useEffect(() => {
sendFakeAnalytics(location.pathname);
}, [location]);
}
function useScrollRestoration() {
let location = useLocation();
useEffect(() => {
fakeRestoreScroll(location.key);
}, [location]);
}
```
+174
View File
@@ -0,0 +1,174 @@
---
title: Actions
order: 6
---
# Actions
[MODES: framework]
## Introduction
Data mutations are done through Route actions. When the action completes, all loader data on the page is revalidated to keep your UI in sync with the data without writing any code to do it.
Route actions defined with `action` are only called on the server while actions defined with `clientAction` are run in the browser.
## Client Actions
Client actions only run in the browser and take priority over a server action when both are defined.
```tsx filename=app/project.tsx
// route('/projects/:projectId', './project.tsx')
import type { Route } from "./+types/project";
import { Form } from "react-router";
import { someApi } from "./api";
export async function clientAction({
request,
}: Route.ClientActionArgs) {
let formData = await request.formData();
let title = formData.get("title");
let project = await someApi.updateProject({ title });
return project;
}
export default function Project({
actionData,
}: Route.ComponentProps) {
return (
<div>
<h1>Project</h1>
<Form method="post">
<input type="text" name="title" />
<button type="submit">Submit</button>
</Form>
{actionData ? (
<p>{actionData.title} updated</p>
) : null}
</div>
);
}
```
## Server Actions
Server actions only run on the server and are removed from client bundles.
```tsx filename=app/project.tsx
// route('/projects/:projectId', './project.tsx')
import type { Route } from "./+types/project";
import { Form } from "react-router";
import { fakeDb } from "../db";
export async function action({
request,
}: Route.ActionArgs) {
let formData = await request.formData();
let title = formData.get("title");
let project = await fakeDb.updateProject({ title });
return project;
}
export default function Project({
actionData,
}: Route.ComponentProps) {
return (
<div>
<h1>Project</h1>
<Form method="post">
<input type="text" name="title" />
<button type="submit">Submit</button>
</Form>
{actionData ? (
<p>{actionData.title} updated</p>
) : null}
</div>
);
}
```
## Calling Actions
Actions are called declaratively through `<Form>` and imperatively through `useSubmit` (or `<fetcher.Form>` and `fetcher.submit`) by referencing the route's path and a "post" method.
### Calling actions with a Form
```tsx
import { Form } from "react-router";
function SomeComponent() {
return (
<Form action="/projects/123" method="post">
<input type="text" name="title" />
<button type="submit">Submit</button>
</Form>
);
}
```
This will cause a navigation and a new entry will be added to the browser history.
### Calling actions with useSubmit
You can submit form data to an action imperatively with `useSubmit`.
```tsx
import { useCallback } from "react";
import { useSubmit } from "react-router";
import { useFakeTimer } from "fake-lib";
function useQuizTimer() {
let submit = useSubmit();
let cb = useCallback(() => {
submit(
{ quizTimedOut: true },
{ action: "/end-quiz", method: "post" },
);
}, []);
let tenMinutes = 10 * 60 * 1000;
useFakeTimer(tenMinutes, cb);
}
```
This will cause a navigation and a new entry will be added to the browser history.
### Calling actions with a fetcher
Fetchers allow you to submit data to actions (and loaders) without causing a navigation (no new entries in the browser history).
```tsx
import { useFetcher } from "react-router";
function Task() {
let fetcher = useFetcher();
let busy = fetcher.state !== "idle";
return (
<fetcher.Form method="post" action="/update-task/123">
<input type="text" name="title" />
<button type="submit">
{busy ? "Saving..." : "Save"}
</button>
</fetcher.Form>
);
}
```
They also have the imperative `submit` method.
```tsx
fetcher.submit(
{ title: "New Title" },
{ action: "/update-task/123", method: "post" },
);
```
See the [Using Fetchers][fetchers] guide for more information.
---
Next: [Navigating](./navigating)
[fetchers]: ../../how-to/fetchers
+201
View File
@@ -0,0 +1,201 @@
---
title: Data Loading
order: 5
---
# Data Loading
[MODES: framework]
## Introduction
Data is provided to the route component from `loader` and `clientLoader`.
Loader data is automatically serialized from loaders and deserialized in components. In addition to primitive values like strings and numbers, loaders can return promises, maps, sets, dates and more.
The type for the `loaderData` prop is [automatically generated][type-safety].
<docs-info>We try to support the same set of [serializable types][serializable-types] that React permits server components to pass as props to client components. This future proofs your application for any eventual migration to [RSC][rsc].</docs-info>
## Client Data Loading
`clientLoader` is used to fetch data on the client. This is useful for pages or full projects that you'd prefer to fetch data from the browser only.
```tsx filename=app/product.tsx
// route("products/:pid", "./product.tsx");
import type { Route } from "./+types/product";
export async function clientLoader({
params,
}: Route.ClientLoaderArgs) {
const res = await fetch(`/api/products/${params.pid}`);
const product = await res.json();
return product;
}
// HydrateFallback is rendered while the client loader is running
export function HydrateFallback() {
return <div>Loading...</div>;
}
export default function Product({
loaderData,
}: Route.ComponentProps) {
const { name, description } = loaderData;
return (
<div>
<h1>{name}</h1>
<p>{description}</p>
</div>
);
}
```
## Server Data Loading
When server rendering, `loader` is used for both initial page loads and client navigations. Client navigations call the loader through an automatic `fetch` by React Router from the browser to your server.
```tsx filename=app/product.tsx
// route("products/:pid", "./product.tsx");
import type { Route } from "./+types/product";
import { fakeDb } from "../db";
export async function loader({ params }: Route.LoaderArgs) {
const product = await fakeDb.getProduct(params.pid);
return product;
}
export default function Product({
loaderData,
}: Route.ComponentProps) {
const { name, description } = loaderData;
return (
<div>
<h1>{name}</h1>
<p>{description}</p>
</div>
);
}
```
Note that the `loader` function is removed from client bundles so you can use server only APIs without worrying about them being included in the browser.
## Static Data Loading
When pre-rendering, loaders are used to fetch data during the production build.
```tsx filename=app/product.tsx
// route("products/:pid", "./product.tsx");
import type { Route } from "./+types/product";
export async function loader({ params }: Route.LoaderArgs) {
let product = await getProductFromCSVFile(params.pid);
return product;
}
export default function Product({
loaderData,
}: Route.ComponentProps) {
const { name, description } = loaderData;
return (
<div>
<h1>{name}</h1>
<p>{description}</p>
</div>
);
}
```
The URLs to pre-render are specified in `react-router.config.ts`:
```ts filename=react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
async prerender() {
let products = await readProductsFromCSVFile();
return products.map(
(product) => `/products/${product.id}`,
);
},
} satisfies Config;
```
Note that when server rendering, any URLs that aren't pre-rendered will be server rendered as usual, allowing you to pre-render some data at a single route while still server rendering the rest.
## Using Both Loaders
`loader` and `clientLoader` can be used together. The `loader` will be used on the server for initial SSR (or pre-rendering) and the `clientLoader` will be used on subsequent client-side navigations.
```tsx filename=app/product.tsx
// route("products/:pid", "./product.tsx");
import type { Route } from "./+types/product";
import { fakeDb } from "../db";
export async function loader({ params }: Route.LoaderArgs) {
return fakeDb.getProduct(params.pid);
}
export async function clientLoader({
serverLoader,
params,
}: Route.ClientLoaderArgs) {
const res = await fetch(`/api/products/${params.pid}`);
const serverData = await serverLoader();
return { ...serverData, ...(await res.json()) };
}
export default function Product({
loaderData,
}: Route.ComponentProps) {
const { name, description } = loaderData;
return (
<div>
<h1>{name}</h1>
<p>{description}</p>
</div>
);
}
```
You can also force the client loader to run during hydration and before the page renders by setting the `hydrate` property on the function. In this situation you will want to render a `HydrateFallback` component to show a fallback UI while the client loader runs.
```tsx filename=app/product.tsx
export async function loader() {
/* ... */
}
export async function clientLoader() {
/* ... */
}
// force the client loader to run during hydration
clientLoader.hydrate = true as const; // `as const` for type inference
export function HydrateFallback() {
return <div>Loading...</div>;
}
export default function Product() {
/* ... */
}
```
---
Next: [Actions][actions]
See also:
- [Streaming with Suspense][streaming]
- [Client Data][client-data]
- [Using Fetchers][fetchers]
[type-safety]: ../../explanation/type-safety
[serializable-types]: https://react.dev/reference/rsc/use-client#serializable-types
[rsc]: ../../how-to/react-server-components
[actions]: ./actions
[streaming]: ../../how-to/suspense
[client-data]: ../../how-to/client-data
[fetchers]: ../../how-to/fetchers#loading-data
+100
View File
@@ -0,0 +1,100 @@
---
title: Deploying
order: 10
---
# Deploying
[MODES: framework]
## Introduction
React Router can be deployed two ways:
- Fullstack Hosting
- Static Hosting
The official [React Router templates](https://github.com/remix-run/react-router-templates) can help you bootstrap an application or be used as a reference for your own application.
When deploying to static hosting, you can deploy React Router the same as any other single page application with React.
## Templates
After running the `create-react-router` command, make sure to follow the instructions in the README.
### Node.js with Docker
```
npx create-react-router@latest --template remix-run/react-router-templates/default
```
- Server Rendering
- Tailwind CSS
The containerized application can be deployed to any platform that supports Docker, including:
- AWS ECS
- Google Cloud Run
- Azure Container Apps
- Digital Ocean App Platform
- Fly.io
- Railway
### Node with Docker (Custom Server)
```
npx create-react-router@latest --template remix-run/react-router-templates/node-custom-server
```
- Server Rendering
- Tailwind CSS
- Custom express server for more control
The containerized application can be deployed to any platform that supports Docker, including:
- AWS ECS
- Google Cloud Run
- Azure Container Apps
- Digital Ocean App Platform
- Fly.io
- Railway
### Node with Docker and Postgres
```
npx create-react-router@latest --template remix-run/react-router-templates/node-postgres
```
- Server Rendering
- Postgres Database with Drizzle
- Tailwind CSS
- Custom express server for more control
The containerized application can be deployed to any platform that supports Docker, including:
- AWS ECS
- Google Cloud Run
- Azure Container Apps
- Digital Ocean App Platform
- Fly.io
- Railway
### Vercel
Vercel maintains their own template for React Router. Checkout the [Vercel Guide](https://vercel.com/templates/react-router/react-router-boilerplate) for more information.
### Cloudflare Workers
Cloudflare maintains their own template for React Router. Checkout the [Cloudflare Guide](https://developers.cloudflare.com/workers/framework-guides/web-apps/react-router/) for more information.
### Netlify
Netlify maintains their own template for React Router. Checkout the [Netlify Guide](https://docs.netlify.com/build/frameworks/framework-setup-guides/react-router/) for more information.
### EdgeOne Pages
EdgeOne Pages maintains their own template for React Router. Checkout the [EdgeOne Pages Guide](https://pages.edgeone.ai/document/framework-react-router) for more information.
### DeployHQ
DeployHQ maintains their own guide for deploying React Router v7 to your own server. Checkout the [DeployHQ Guide](https://www.deployhq.com/guides/deploy-react-router-from-github) for more information.
+4
View File
@@ -0,0 +1,4 @@
---
title: Framework Mode
order: 2
---
@@ -0,0 +1,41 @@
---
title: Installation
order: 1
---
# Installation
[MODES: framework]
## Introduction
Most projects start with a template. Let's use a basic template maintained by React Router:
```shellscript nonumber
npx create-react-router@latest my-react-router-app
```
Now change into the new directory and start the app
```shellscript nonumber
cd my-react-router-app
npm i
npm run dev
```
You can now open your browser to `http://localhost:5173`
You can [view the template on GitHub][default-template] to see how to manually set up your project.
We also have a number of [ready to deploy templates][react-router-templates] available for you to get started with:
```shellscript nonumber
npx create-react-router@latest --template remix-run/react-router-templates/<template-name>
```
---
Next: [Routing](./routing)
[default-template]: https://github.com/remix-run/react-router-templates/tree/main/default
[react-router-templates]: https://github.com/remix-run/react-router-templates
+182
View File
@@ -0,0 +1,182 @@
---
title: Navigating
order: 6
---
# Navigating
[MODES: framework]
## Introduction
Users navigate your application with `<Link>`, `<NavLink>`, `<Form>`, `redirect`, and `useNavigate`.
## NavLink
This component is for navigation links that need to render active and pending states.
```tsx
import { NavLink } from "react-router";
export function MyAppNav() {
return (
<nav>
<NavLink to="/" end>
Home
</NavLink>
<NavLink to="/trending" end>
Trending Concerts
</NavLink>
<NavLink to="/concerts">All Concerts</NavLink>
<NavLink to="/account">Account</NavLink>
</nav>
);
}
```
`NavLink` renders default class names for different states for easy styling with CSS:
```css
a.active {
color: red;
}
a.pending {
animate: pulse 1s infinite;
}
a.transitioning {
/* css transition is running */
}
```
It also has callback props on `className`, `style`, and `children` with the states for inline styling or conditional rendering:
```tsx
// className
<NavLink
to="/messages"
className={({ isActive, isPending, isTransitioning }) =>
[
isPending ? "pending" : "",
isActive ? "active" : "",
isTransitioning ? "transitioning" : "",
].join(" ")
}
>
Messages
</NavLink>
```
```tsx
// style
<NavLink
to="/messages"
style={({ isActive, isPending, isTransitioning }) => {
return {
fontWeight: isActive ? "bold" : "",
color: isPending ? "red" : "black",
viewTransitionName: isTransitioning ? "slide" : "",
};
}}
>
Messages
</NavLink>
```
```tsx
// children
<NavLink to="/tasks">
{({ isActive, isPending, isTransitioning }) => (
<span className={isActive ? "active" : ""}>Tasks</span>
)}
</NavLink>
```
## Link
Use `<Link>` when the link doesn't need active styling:
```tsx
import { Link } from "react-router";
export function LoggedOutMessage() {
return (
<p>
You've been logged out.{" "}
<Link to="/login">Login again</Link>
</p>
);
}
```
## Form
The form component can be used to navigate with `URLSearchParams` provided by the user.
```tsx
<Form action="/search">
<input type="text" name="q" />
</Form>
```
If the user enters "journey" into the input and submits it, they will navigate to:
```
/search?q=journey
```
Forms with `<Form method="post" />` will also navigate to the action prop but will submit the data as `FormData` instead of `URLSearchParams`. However, it is more common to `useFetcher()` to POST form data. See [Using Fetchers](../../how-to/fetchers).
## redirect
Inside of route loaders and actions, you can return a `redirect` to another URL.
```tsx
import { redirect } from "react-router";
export async function loader({ request }) {
let user = await getUser(request);
if (!user) {
return redirect("/login");
}
return { userName: user.name };
}
```
It is common to redirect to a new record after it has been created:
```tsx
import { redirect } from "react-router";
export async function action({ request }) {
let formData = await request.formData();
let project = await createProject(formData);
return redirect(`/projects/${project.id}`);
}
```
## useNavigate
This hook allows the programmer to navigate the user to a new page without the user interacting. Usage of this hook should be uncommon. It's recommended to use the other APIs in this guide when possible.
Reserve usage of `useNavigate` to situations where the user is _not_ interacting but you need to navigate, for example:
- Logging them out after inactivity
- Timed UIs like quizzes, etc.
```tsx
import { useNavigate } from "react-router";
export function useLogoutAfterInactivity() {
let navigate = useNavigate();
useFakeInactivityHook(() => {
navigate("/logout");
});
}
```
---
Next: [Pending UI](./pending-ui)
+142
View File
@@ -0,0 +1,142 @@
---
title: Pending UI
order: 7
---
# Pending UI
[MODES: framework]
## Introduction
When the user navigates to a new route, or submits data to an action, the UI should immediately respond to the user's actions with a pending or optimistic state. Application code is responsible for this.
## Global Pending Navigation
When the user navigates to a new url, the loaders for the next page are awaited before the next page renders. You can get the pending state from `useNavigation`.
```tsx
import { useNavigation } from "react-router";
export default function Root() {
const navigation = useNavigation();
const isNavigating = Boolean(navigation.location);
return (
<html>
<body>
{isNavigating && <GlobalSpinner />}
<Outlet />
</body>
</html>
);
}
```
## Local Pending Navigation
Pending indicators can also be localized to the link. NavLink's children, className, and style props can be functions that receive the pending state.
```tsx
import { NavLink } from "react-router";
function Navbar() {
return (
<nav>
<NavLink to="/home">
{({ isPending }) => (
<span>Home {isPending && <Spinner />}</span>
)}
</NavLink>
<NavLink
to="/about"
style={({ isPending }) => ({
color: isPending ? "gray" : "black",
})}
>
About
</NavLink>
</nav>
);
}
```
## Pending Form Submission
When a form is submitted, the UI should immediately respond to the user's actions with a pending state. This is easiest to do with a [fetcher][use_fetcher] form because it has its own independent state (whereas normal forms cause a global navigation).
```tsx filename=app/project.tsx lines=[10-12]
import { useFetcher } from "react-router";
function NewProjectForm() {
const fetcher = useFetcher();
return (
<fetcher.Form method="post">
<input type="text" name="title" />
<button type="submit">
{fetcher.state !== "idle"
? "Submitting..."
: "Submit"}
</button>
</fetcher.Form>
);
}
```
For non-fetcher form submissions, pending states are available on `useNavigation`.
```tsx filename=app/projects/new.tsx
import { useNavigation, Form } from "react-router";
function NewProjectForm() {
const navigation = useNavigation();
return (
<Form method="post" action="/projects/new">
<input type="text" name="title" />
<button type="submit">
{navigation.formAction === "/projects/new"
? "Submitting..."
: "Submit"}
</button>
</Form>
);
}
```
## Optimistic UI
When the future state of the UI is known by the form submission data, an optimistic UI can be implemented for instant UX.
```tsx filename=app/project.tsx lines=[4-7]
function Task({ task }) {
const fetcher = useFetcher();
let isComplete = task.status === "complete";
if (fetcher.formData) {
isComplete =
fetcher.formData.get("status") === "complete";
}
return (
<div>
<div>{task.title}</div>
<fetcher.Form method="post">
<button
name="status"
value={isComplete ? "incomplete" : "complete"}
>
{isComplete ? "Mark Incomplete" : "Mark Complete"}
</button>
</fetcher.Form>
</div>
);
}
```
---
Next: [Testing](./testing)
[use_fetcher]: https://api.reactrouter.com/v7/functions/react-router.useFetcher.html
+59
View File
@@ -0,0 +1,59 @@
---
title: Rendering Strategies
order: 4
---
# Rendering Strategies
[MODES: framework]
## Introduction
There are three rendering strategies in React Router:
- Client Side Rendering
- Server Side Rendering
- Static Pre-rendering
## Client Side Rendering
Routes are always client side rendered as the user navigates around the app. If you're looking to build a Single Page App, disable server rendering:
```ts filename=react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
ssr: false,
} satisfies Config;
```
## Server Side Rendering
```ts filename=react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
ssr: true,
} satisfies Config;
```
Server side rendering requires a deployment that supports it. Though it's a global setting, individual routes can still be statically pre-rendered. Routes can also use client data loading with `clientLoader` to avoid server rendering/fetching for their portion of the UI.
## Static Pre-rendering
```ts filename=react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
// return a list of URLs to prerender at build time
async prerender() {
return ["/", "/about", "/contact"];
},
} satisfies Config;
```
Pre-rendering is a build-time operation that generates static HTML and client navigation data payloads for a list of URLs. This is useful for SEO and performance, especially for deployments without server rendering. When pre-rendering, route module loaders are used to fetch data at build time.
---
Next: [Data Loading](./data-loading)
+527
View File
@@ -0,0 +1,527 @@
---
title: Route Module
order: 3
---
# Route Module
[MODES: framework]
## Introduction
The files referenced in `routes.ts` are called Route Modules.
```tsx filename=app/routes.ts
route("teams/:teamId", "./team.tsx"),
// route module ^^^^^^^^
```
Route modules are the foundation of React Router's framework features, they define:
- automatic code-splitting
- data loading
- actions
- revalidation
- error boundaries
- and more
This guide is a quick overview of every route module feature. The rest of the getting started guides will cover these features in more detail.
## Component (`default`)
The `default` export in a route module defines the component that will render when the route matches.
```tsx filename=app/routes/my-route.tsx
export default function MyRouteComponent() {
return (
<div>
<h1>Look ma!</h1>
<p>
I'm still using React Router after like 10 years.
</p>
</div>
);
}
```
### Props passed to the Component
When the component is rendered, it is provided the props defined in `Route.ComponentProps` that React Router will automatically generate for you. These props include:
1. `loaderData`: The data returned from the `loader` function in this route module
2. `actionData`: The data returned from the `action` function in this route module
3. `params`: An object containing the route parameters (if any).
4. `matches`: An array of all the matches in the current route tree.
You can use these props in place of hooks like `useLoaderData` or `useParams`. This may be preferable because they will be automatically typed correctly for the route.
### Using props
```tsx filename=app/routes/my-route-with-default-params.tsx
import type { Route } from "./+types/route-name";
export default function MyRouteComponent({
loaderData,
actionData,
params,
matches,
}: Route.ComponentProps) {
return (
<div>
<h1>Welcome to My Route with Props!</h1>
<p>Loader Data: {JSON.stringify(loaderData)}</p>
<p>Action Data: {JSON.stringify(actionData)}</p>
<p>Route Parameters: {JSON.stringify(params)}</p>
<p>Matched Routes: {JSON.stringify(matches)}</p>
</div>
);
}
```
## `middleware`
Route [middleware][middleware] runs sequentially on the server before and after document and
data requests. This gives you a singular place to do things like logging,
authentication, and post-processing of responses. The `next` function continues down the chain, and on the leaf route the `next` function executes the loaders/actions for the navigation.
Here's an example middleware to log requests on the server:
```tsx filename=root.tsx
async function loggingMiddleware(
{ request, context },
next,
) {
console.log(
`${new Date().toISOString()} ${request.method} ${request.url}`,
);
const start = performance.now();
const response = await next();
const duration = performance.now() - start;
console.log(
`${new Date().toISOString()} Response ${response.status} (${duration}ms)`,
);
return response;
}
export const middleware = [loggingMiddleware];
```
Here's an example middleware to check for logged in users and set the user in
`context` you can then access from loaders:
```tsx filename=routes/_auth.tsx
async function authMiddleware({ 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);
}
export const middleware = [authMiddleware];
```
<docs-warning>Please make sure you understand [when middleware runs][when-middleware-runs] to make sure your application will behave the way you intend when adding middleware to your routes.</docs-warning>
See also:
- [`middleware` params][middleware-params]
- [Middleware][middleware]
## `clientMiddleware`
This is the client-side equivalent of `middleware` and runs in the browser during client navigations. The only difference from server middleware is that client middleware doesn't return Responses because they're not wrapping an HTTP request on the server.
Here's an example middleware to log requests on the client:
```tsx filename=root.tsx
async function loggingMiddleware(
{ request, context },
next,
) {
console.log(
`${new Date().toISOString()} ${request.method} ${request.url}`,
);
const start = performance.now();
await next(); // 👈 No Response returned
const duration = performance.now() - start;
console.log(
`${new Date().toISOString()} (${duration}ms)`,
);
// ✅ No need to return anything
}
export const clientMiddleware = [loggingMiddleware];
```
See also:
- [Middleware][middleware]
- [Client Data][client-data]
## `loader`
Route loaders provide data to route components before they are rendered. They are only called on the server when server rendering or during the build with pre-rendering.
```tsx
export async function loader() {
return { message: "Hello, world!" };
}
export default function MyRoute({ loaderData }) {
return <h1>{loaderData.message}</h1>;
}
```
See also:
- [`loader` params][loader-params]
## `clientLoader`
Called only in the browser, route client loaders provide data to route components in addition to, or in place of, route loaders.
```tsx
export async function clientLoader({ serverLoader }) {
// call the server loader
const serverData = await serverLoader();
// And/or fetch data on the client
const data = getDataFromClient();
// Return the data to expose through useLoaderData()
return data;
}
```
Client loaders can participate in initial page load hydration of server rendered pages by setting the `hydrate` property on the function:
```tsx
export async function clientLoader() {
// ...
}
clientLoader.hydrate = true as const;
```
<docs-info>
By using `as const`, TypeScript will infer that the type for `clientLoader.hydrate` is `true` instead of `boolean`.
That way, React Router can derive types for `loaderData` based on the value of `clientLoader.hydrate`.
</docs-info>
See also:
- [`clientLoader` params][client-loader-params]
- [Client Data][client-data]
## `action`
Route actions allow server-side data mutations with automatic revalidation of all loader data on the page when called from `<Form>`, `useFetcher`, and `useSubmit`.
```tsx
// route("/list", "./list.tsx")
import { Form } from "react-router";
import { TodoList } from "~/components/TodoList";
// this data will be loaded after the action completes...
export async function loader() {
const items = await fakeDb.getItems();
return { items };
}
// ...so that the list here is updated automatically
export default function Items({ loaderData }) {
return (
<div>
<List items={loaderData.items} />
<Form method="post" navigate={false} action="/list">
<input type="text" name="title" />
<button type="submit">Create Todo</button>
</Form>
</div>
);
}
export async function action({ request }) {
const data = await request.formData();
const todo = await fakeDb.addItem({
title: data.get("title"),
});
return { ok: true };
}
```
See also:
- [`action` params][action-params]
## `clientAction`
Like route actions but only called in the browser.
```tsx
export async function clientAction({ serverAction }) {
fakeInvalidateClientSideCache();
// can still call the server action if needed
const data = await serverAction();
return data;
}
```
See also:
- [`clientAction` params][client-action-params]
- [Client Data][client-data]
## `ErrorBoundary`
When other route module APIs throw, the route module `ErrorBoundary` will render instead of the route component.
```tsx
import {
isRouteErrorResponse,
useRouteError,
} from "react-router";
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div>
<h1>
{error.status} {error.statusText}
</h1>
<p>{error.data}</p>
</div>
);
} 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>;
}
}
```
See also:
- [`useRouteError`][use-route-error]
- [`isRouteErrorResponse`][is-route-error-response]
## `HydrateFallback`
On initial page load, the route component renders only after the client loader is finished. If exported, a `HydrateFallback` can render immediately in place of the route component.
```tsx filename=routes/client-only-route.tsx
export async function clientLoader() {
const data = await fakeLoadLocalGameData();
return data;
}
export function HydrateFallback() {
return <p>Loading Game...</p>;
}
export default function Component({ loaderData }) {
return <Game data={loaderData} />;
}
```
## `headers`
The route `headers` function defines the HTTP headers to be sent with the response when server rendering.
```tsx
export function headers() {
return {
"X-Stretchy-Pants": "its for fun",
"Cache-Control": "max-age=300, s-maxage=3600",
};
}
```
See also:
- [`Headers`][headers]
## `handle`
Route handle allows apps to add anything to a route match in `useMatches` to create abstractions (like breadcrumbs, etc.).
```tsx
export const handle = {
its: "all yours",
};
```
See also:
- [`useMatches`][use-matches]
## `links`
Route links define [`<link>` element][link-element]s to be rendered in the document `<head>`.
```tsx
export function links() {
return [
{
rel: "icon",
href: "/favicon.png",
type: "image/png",
},
{
rel: "stylesheet",
href: "https://example.com/some/styles.css",
},
{
rel: "preload",
href: "/images/banner.jpg",
as: "image",
},
];
}
```
All routes links will be aggregated and rendered through the `<Links />` component, usually rendered in your app root:
```tsx
import { Links } from "react-router";
export default function Root() {
return (
<html>
<head>
<Links />
</head>
<body />
</html>
);
}
```
See also:
- [Styling][styling]
## `meta`
Route meta defines [meta tags][meta-element] to be rendered in the `<Meta />` component, usually placed in the `<head>`.
<docs-warning>
Since React 19, [using the built-in `<meta>` element](https://react.dev/reference/react-dom/components/meta) is recommended over the use of the route module's `meta` export.
Here is an example of how to use it and the `<title>` element:
```tsx
export default function MyRoute() {
return (
<div>
<title>Very cool app</title>
<meta property="og:title" content="Very cool app" />
<meta
name="description"
content="This app is the best"
/>
{/* The rest of your route content... */}
</div>
);
}
```
</docs-warning>
```tsx filename=app/product.tsx
export function meta() {
return [
{ title: "Very cool app" },
{
property: "og:title",
content: "Very cool app",
},
{
name: "description",
content: "This app is the best",
},
];
}
```
```tsx filename=app/root.tsx
import { Meta } from "react-router";
export default function Root() {
return (
<html>
<head>
<Meta />
</head>
<body />
</html>
);
}
```
The meta of the last matching route is used, allowing you to override parent routes' meta. It's important to note that the entire meta descriptor array is replaced, not merged. This gives you the flexibility to build your own meta composition logic across pages at different levels.
**See also**
- [`meta` params][meta-params]
- [`meta` function return types][meta-function]
## `shouldRevalidate`
In framework mode with SSR, route loaders are automatically revalidated after all navigations and form submissions (this is different from [Data Mode][data-mode-should-revalidate]). This enables middleware and loaders to share a request context and optimize in different ways than they would in Data Mode.
Defining this function allows you to opt out of revalidation for a route loader for navigations and form submissions.
```tsx
import type { ShouldRevalidateFunctionArgs } from "react-router";
export function shouldRevalidate(
arg: ShouldRevalidateFunctionArgs,
) {
return true;
}
```
When using [SPA Mode][spa-mode], there are no server loaders to call on navigations, so `shouldRevalidate` behaves the same as it does in [Data Mode][data-mode-should-revalidate].
[`ShouldRevalidateFunctionArgs` Reference Documentation ↗](https://api.reactrouter.com/v7/interfaces/react-router.ShouldRevalidateFunctionArgs.html)
---
Next: [Rendering Strategies](./rendering)
[middleware-params]: https://api.reactrouter.com/v7/types/react-router.MiddlewareFunction.html
[middleware]: ../../how-to/middleware
[when-middleware-runs]: ../../how-to/middleware#when-middleware-runs
[loader-params]: https://api.reactrouter.com/v7/interfaces/react-router.LoaderFunctionArgs
[client-loader-params]: https://api.reactrouter.com/v7/types/react-router.ClientLoaderFunctionArgs
[action-params]: https://api.reactrouter.com/v7/interfaces/react-router.ActionFunctionArgs
[client-action-params]: https://api.reactrouter.com/v7/types/react-router.ClientActionFunctionArgs
[use-route-error]: ../../api/hooks/useRouteError
[is-route-error-response]: ../../api/utils/isRouteErrorResponse
[headers]: https://developer.mozilla.org/en-US/docs/Web/API/Response/headers
[use-matches]: ../../api/hooks/useMatches
[link-element]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link
[meta-element]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta
[meta-params]: https://api.reactrouter.com/v7/interfaces/react-router.MetaArgs
[meta-function]: https://api.reactrouter.com/v7/types/react-router.MetaDescriptor.html
[data-mode-should-revalidate]: ../data/route-object#shouldrevalidate
[spa-mode]: ../../how-to/spa
[client-data]: ../../how-to/client-data
[styling]: ../../explanation/styling
+362
View File
@@ -0,0 +1,362 @@
---
title: Routing
order: 2
---
# Routing
[MODES: framework]
## Configuring Routes
Routes are configured in `app/routes.ts`. Each route has two required parts: a URL pattern to match the URL, and a file path to the route module that defines its behavior.
```ts filename=app/routes.ts
import {
type RouteConfig,
route,
} from "@react-router/dev/routes";
export default [
route("some/path", "./some/file.tsx"),
// pattern ^ ^ module file
] satisfies RouteConfig;
```
Here is a larger sample route config:
```ts filename=app/routes.ts
import {
type RouteConfig,
route,
index,
layout,
prefix,
} from "@react-router/dev/routes";
export default [
index("./home.tsx"),
route("about", "./about.tsx"),
layout("./auth/layout.tsx", [
route("login", "./auth/login.tsx"),
route("register", "./auth/register.tsx"),
]),
...prefix("concerts", [
index("./concerts/home.tsx"),
route(":city", "./concerts/city.tsx"),
route("trending", "./concerts/trending.tsx"),
]),
] satisfies RouteConfig;
```
If you prefer to define your routes via file naming conventions rather than configuration, the `@react-router/fs-routes` package provides a [file system routing convention][file-route-conventions]. You can even combine different routing conventions if you like:
```ts filename=app/routes.ts
import {
type RouteConfig,
route,
} from "@react-router/dev/routes";
import { flatRoutes } from "@react-router/fs-routes";
export default [
route("/", "./home.tsx"),
...(await flatRoutes()),
] satisfies RouteConfig;
```
## Route Modules
The files referenced in `routes.ts` define each route's behavior:
```tsx filename=app/routes.ts
route("teams/:teamId", "./team.tsx"),
// route module ^^^^^^^^
```
Here's a sample route module:
```tsx filename=app/team.tsx
// provides type safety/inference
import type { Route } from "./+types/team";
// provides `loaderData` to the component
export async function loader({ params }: Route.LoaderArgs) {
let team = await fetchTeam(params.teamId);
return { name: team.name };
}
// renders after the loader is done
export default function Component({
loaderData,
}: Route.ComponentProps) {
return <h1>{loaderData.name}</h1>;
}
```
Route modules have more features like actions, headers, and error boundaries, but they will be covered in the next guide: [Route Modules](./route-module)
## Nested Routes
Routes can be nested inside parent routes.
```ts filename=app/routes.ts
import {
type RouteConfig,
route,
index,
} from "@react-router/dev/routes";
export default [
// parent route
route("dashboard", "./dashboard.tsx", [
// child routes
index("./home.tsx"),
route("settings", "./settings.tsx"),
]),
] satisfies RouteConfig;
```
The path of the parent is automatically included in the child, so this config creates both `"/dashboard"` and `"/dashboard/settings"` URLs.
Child routes are rendered through the `<Outlet/>` in the parent route.
```tsx filename=app/dashboard.tsx
import { Outlet } from "react-router";
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* will either be home.tsx or settings.tsx */}
<Outlet />
</div>
);
}
```
## Root Route
Every route in `routes.ts` is nested inside the special `app/root.tsx` module.
## Layout Routes
Using `layout`, layout routes create new nesting for their children, but they don't add any segments to the URL. It's like the root route but they can be added at any level.
```tsx filename=app/routes.ts lines=[10,16]
import {
type RouteConfig,
route,
layout,
index,
prefix,
} from "@react-router/dev/routes";
export default [
layout("./marketing/layout.tsx", [
index("./marketing/home.tsx"),
route("contact", "./marketing/contact.tsx"),
]),
...prefix("projects", [
index("./projects/home.tsx"),
layout("./projects/project-layout.tsx", [
route(":pid", "./projects/project.tsx"),
route(":pid/edit", "./projects/edit-project.tsx"),
]),
]),
] satisfies RouteConfig;
```
Note that:
- `home.tsx` and `contact.tsx` will be rendered into the `marketing/layout.tsx` outlet without creating any new URL paths
- `project.tsx` and `edit-project.tsx` will be rendered into the `projects/project-layout.tsx` outlet at `/projects/:pid` and `/projects/:pid/edit` while `projects/home.tsx` will not.
## Index Routes
```ts
index(componentFile),
```
Index routes render into their parent's [Outlet][outlet] at their parent's URL (like a default child route).
```ts filename=app/routes.ts
import {
type RouteConfig,
route,
index,
} from "@react-router/dev/routes";
export default [
// renders into the root.tsx Outlet at /
index("./home.tsx"),
route("dashboard", "./dashboard.tsx", [
// renders into the dashboard.tsx Outlet at /dashboard
index("./dashboard-home.tsx"),
route("settings", "./dashboard-settings.tsx"),
]),
] satisfies RouteConfig;
```
Note that index routes can't have children.
## Route Prefixes
Using `prefix`, you can add a path prefix to a set of routes without needing to introduce a parent route.
```tsx filename=app/routes.ts lines=[14]
import {
type RouteConfig,
route,
layout,
index,
prefix,
} from "@react-router/dev/routes";
export default [
layout("./marketing/layout.tsx", [
index("./marketing/home.tsx"),
route("contact", "./marketing/contact.tsx"),
]),
...prefix("projects", [
index("./projects/home.tsx"),
layout("./projects/project-layout.tsx", [
route(":pid", "./projects/project.tsx"),
route(":pid/edit", "./projects/edit-project.tsx"),
]),
]),
] satisfies RouteConfig;
```
Note that this does not introduce a new route into the route tree. Instead, it merely modifies the paths of its children.
For example, these two sets of routes are equivalent:
```ts filename=app/routes.ts
// This usage of `prefix`...
prefix("parent", [
route("child1", "./child1.tsx"),
route("child2", "./child2.tsx"),
])
// ...is equivalent to this:
[
route("parent/child1", "./child1.tsx"),
route("parent/child2", "./child2.tsx"),
]
```
## Dynamic Segments
If a path segment starts with `:` then it becomes a "dynamic segment". When the route matches the URL, the dynamic segment will be parsed from the URL and provided as `params` to other router APIs.
```ts filename=app/routes.ts
route("teams/:teamId", "./team.tsx"),
```
```tsx filename=app/team.tsx
import type { Route } from "./+types/team";
export async function loader({ params }: Route.LoaderArgs) {
// ^? { teamId: string }
}
export default function Component({
params,
}: Route.ComponentProps) {
params.teamId;
// ^ string
}
```
You can have multiple dynamic segments in one route path:
```ts filename=app/routes.ts
route("c/:categoryId/p/:productId", "./product.tsx"),
```
```tsx filename=app/product.tsx
import type { Route } from "./+types/product";
async function loader({ params }: LoaderArgs) {
// ^? { categoryId: string; productId: string }
}
```
## Optional Segments
You can make a route segment optional by adding a `?` to the end of the segment.
```ts filename=app/routes.ts
route(":lang?/categories", "./categories.tsx"),
```
You can have optional static segments, too:
```ts filename=app/routes.ts
route("users/:userId/edit?", "./user.tsx");
```
## Splats
Also known as "catchall" and "star" segments. If a route path pattern ends with `/*` then it will match any characters following the `/`, including other `/` characters.
```ts filename=app/routes.ts
route("files/*", "./files.tsx"),
```
```tsx filename=app/files.tsx
export async function loader({ params }: Route.LoaderArgs) {
// params["*"] will contain the remaining URL after files/
}
```
You can destructure the `*`, you just have to assign it a new name. A common name is `splat`:
```tsx
const { "*": splat } = params;
```
You can also use a splat to catch requests that don't match any route:
```ts filename=app/routes.ts
route("*", "./catchall.tsx"); // catchall route,
```
```tsx filename=app/catchall.tsx
export function loader() {
throw new Response("Page not found", { status: 404 });
}
```
## Component Routes
You can also use components that match the URL to elements anywhere in the component tree:
```tsx
import { Routes, Route } from "react-router";
function Wizard() {
return (
<div>
<h1>Some Wizard with Steps</h1>
<Routes>
<Route index element={<StepOne />} />
<Route path="step-2" element={<StepTwo />} />
<Route path="step-3" element={<StepThree />} />
</Routes>
</div>
);
}
```
Note that these routes do not participate in data loading, actions, code splitting, or any other route module features, so their use cases are more limited than those of the route module.
---
Next: [Route Module](./route-module)
[file-route-conventions]: ../../how-to/file-route-conventions
[outlet]: https://api.reactrouter.com/v7/functions/react-router.Outlet.html
+133
View File
@@ -0,0 +1,133 @@
---
title: Testing
order: 9
---
# Testing
[MODES: framework, data]
## Introduction
When components use things like `useLoaderData`, `<Link>`, etc, they are required to be rendered in context of a React Router app. The `createRoutesStub` function creates that context to test components in isolation.
Consider a login form component that relies on `useActionData`
```tsx
import { useActionData } from "react-router";
export function LoginForm() {
const actionData = useActionData();
const errors = actionData?.errors;
return (
<Form method="post">
<label>
<input type="text" name="username" />
{errors?.username && <div>{errors.username}</div>}
</label>
<label>
<input type="password" name="password" />
{errors?.password && <div>{errors.password}</div>}
</label>
<button type="submit">Login</button>
</Form>
);
}
```
We can test this component with `createRoutesStub`. It takes an array of objects that resemble route modules with loaders, actions, and components.
```tsx
import { createRoutesStub } from "react-router";
import {
render,
screen,
waitFor,
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { LoginForm } from "./LoginForm";
test("LoginForm renders error messages", async () => {
const USER_MESSAGE = "Username is required";
const PASSWORD_MESSAGE = "Password is required";
const Stub = createRoutesStub([
{
path: "/login",
Component: LoginForm,
action() {
return {
errors: {
username: USER_MESSAGE,
password: PASSWORD_MESSAGE,
},
};
},
},
]);
// render the app stub at "/login"
render(<Stub initialEntries={["/login"]} />);
// simulate interactions
userEvent.click(screen.getByText("Login"));
await waitFor(() => screen.findByText(USER_MESSAGE));
await waitFor(() => screen.findByText(PASSWORD_MESSAGE));
});
```
## Using with Framework Mode Types
It's important to note that `createRoutesStub` is designed for _unit_ testing of reusable components in your application that rely on contextual router information (i.e., `loaderData`, `actionData`, `matches`). These components usually obtain this information via the hooks (`useLoaderData`, `useActionData`, `useMatches`) or via props passed down from the ancestor route component. We **strongly** recommend limiting your usage of `createRoutesStub` to unit testing of these types of reusable components.
`createRoutesStub` is _not designed_ for (and is arguably incompatible with) direct testing of Route components using the [`Route.\*`](../../explanation/type-safety) types available in Framework Mode. This is because the `Route.*` types are derived from your actual application - including the real `loader`/`action` functions as well as the structure of your route tree structure (which defines the `matches` type). When you use `createRoutesStub`, you are providing stubbed values for `loaderData`, `actionData`, and even your `matches` based on the route tree you pass to `createRoutesStub`. Therefore, the types won't align with the `Route.*` types and you'll get type issues trying to use a route component in a route stub.
```tsx filename=routes/login.tsx
export default function Login({
actionData,
}: Route.ComponentProps) {
return <Form method="post">...</Form>;
}
```
```tsx filename=routes/login.test.tsx
import LoginRoute from "./login";
test("LoginRoute renders error messages", async () => {
const Stub = createRoutesStub([
{
path: "/login",
Component: LoginRoute,
// ^ ❌ Types of property 'matches' are incompatible.
action() {
/*...*/
},
},
]);
// ...
});
```
These type errors are generally accurate if you try to setup your tests like this. As long as your stubbed `loader`/`action` functions match your real implementations, then the types for `loaderData`/`actionData` will be correct, but if they differ your types will be lying to you.
`matches` is more complicated since you don't usually stub out all of the ancestor routes. In this example, there is no `root` route so `matches` will only contain your test route, while it will contain the root route and any other ancestors at runtime. There's no great way to automatically align the typegen types with the runtime types in your test.
Therefore, if you need to test Route level components, we recommend you do that via an Integration/E2E test (Playwright, Cypress, etc.) against a running application because you're venturing out of unit testing territory when testing your route as a whole.
If you _need_ to write a unit test against the route, you can add a `@ts-expect-error` comment in your test to silence the TypeScript error:
```tsx
const Stub = createRoutesStub([
{
path: "/login",
// @ts-expect-error: `matches` won't align between test code and app code
Component: LoginRoute,
action() {
/*...*/
},
},
]);
```
+4
View File
@@ -0,0 +1,4 @@
---
title: Getting Started
order: 1
---
+201
View File
@@ -0,0 +1,201 @@
---
title: Picking a Mode
order: 1
---
# Picking a Mode
React Router is a multi-strategy router for React. There are three primary ways, or "modes", to use it in your app. Across the docs you'll see these icons indicating which mode the content is relevant to:
[MODES: framework, data, declarative]
<p></p>
The features available in each mode are additive, so moving from Declarative to Data to Framework simply adds more features at the cost of architectural control. So pick your mode based on how much control or how much help you want from React Router.
The mode depends on which "top level" router API you're using:
## Declarative
Declarative mode enables basic routing features like matching URLs to components, navigating around the app, and providing active states with APIs like `<Link>`, `useNavigate`, and `useLocation`.
```tsx
import { BrowserRouter } from "react-router";
ReactDOM.createRoot(root).render(
<BrowserRouter>
<App />
</BrowserRouter>,
);
```
## Data
By moving route configuration outside of React rendering, Data Mode adds data loading, actions, pending states and more with APIs like `loader`, `action`, and `useFetcher`.
```tsx
import {
createBrowserRouter,
RouterProvider,
} from "react-router";
let router = createBrowserRouter([
{
path: "/",
Component: Root,
loader: loadRootData,
},
]);
ReactDOM.createRoot(root).render(
<RouterProvider router={router} />,
);
```
## Framework
Framework Mode wraps Data Mode with a Vite plugin to add the full React Router experience with:
- type-safe `href`
- type-safe Route Module API
- intelligent code splitting
- SPA, SSR, and static rendering strategies
- and more
```ts filename=routes.ts
import { index, route } from "@react-router/dev/routes";
export default [
index("./home.tsx"),
route("products/:pid", "./product.tsx"),
];
```
You'll then have access to the Route Module API with type-safe params, loaderData, code splitting, SPA/SSR/SSG strategies, and more.
```ts filename=product.tsx
import { Route } from "./+types/product.tsx";
export async function loader({ params }: Route.LoaderArgs) {
let product = await getProduct(params.pid);
return { product };
}
export default function Product({
loaderData,
}: Route.ComponentProps) {
return <div>{loaderData.product.name}</div>;
}
```
## Decision Advice
Every mode supports any architecture and deployment target, so the question isn't really about if you want SSR, SPA, etc. It's about how much you want to do yourself.
**Use Framework Mode if you:**
- are too new to have an opinion
- are considering Next.js, Solid Start, SvelteKit, Astro, TanStack Start, etc. and want to compare
- just want to build something with React
- might want to server render, might not
- are coming from Remix (React Router v7 is the "next version" after Remix v2)
- are migrating from Next.js
[→ Get Started with Framework Mode](./framework/installation).
**Use Data Mode if you:**
- want data features but also want to have control over bundling, data, and server abstractions
- started a data router in v6.4 and are happy with it
[→ Get Started with Data Mode](./data/custom).
**Use Declarative Mode if you:**
- want to use React Router as simply as possible
- are coming from v6 and are happy with the `<BrowserRouter>`
- have a data layer that either skips pending states (like local first, background data replication/sync) or has its own abstractions for them
- are coming from Create React App (you may want to consider framework mode though)
[→ Get Started with Declarative Mode](./declarative/installation).
## API + Mode Availability Table
This is mostly for the LLMs, but knock yourself out:
| API | Framework | Data | Declarative |
| ------------------------------ | --------- | ---- | ----------- |
| Await | ✅ | ✅ | |
| Form | ✅ | ✅ |
| Link | ✅ | ✅ | ✅ |
| `<Link discover>` | ✅ | | |
| `<Link prefetch>` | ✅ | | |
| `<Link preventScrollReset>` | ✅ | ✅ | |
| Links | ✅ | | |
| Meta | ✅ | | |
| NavLink | ✅ | ✅ | ✅ |
| `<NavLink discover>` | ✅ | | |
| `<NavLink prefetch>` | ✅ | | |
| `<NavLink preventScrollReset>` | ✅ | ✅ | |
| NavLink `isPending` | ✅ | ✅ | |
| Navigate | ✅ | ✅ | ✅ |
| Outlet | ✅ | ✅ | ✅ |
| PrefetchPageLinks | ✅ | | |
| Route | ✅ | ✅ | ✅ |
| Routes | ✅ | ✅ | ✅ |
| Scripts | ✅ | | |
| ScrollRestoration | ✅ | ✅ | |
| ServerRouter | ✅ | | |
| usePrompt | ✅ | ✅ | |
| useActionData | ✅ | ✅ | |
| useAsyncError | ✅ | ✅ | |
| useAsyncValue | ✅ | ✅ | |
| useBeforeUnload | ✅ | ✅ | ✅ |
| useBlocker | ✅ | ✅ | |
| useFetcher | ✅ | ✅ | |
| useFetchers | ✅ | ✅ | |
| useFormAction | ✅ | ✅ | |
| useHref | ✅ | ✅ | ✅ |
| useInRouterContext | ✅ | ✅ | ✅ |
| useLinkClickHandler | ✅ | ✅ | ✅ |
| useLoaderData | ✅ | ✅ | |
| useLocation | ✅ | ✅ | ✅ |
| useMatch | ✅ | ✅ | ✅ |
| useMatches | ✅ | ✅ | |
| useNavigate | ✅ | ✅ | ✅ |
| useNavigation | ✅ | ✅ | |
| useNavigationType | ✅ | ✅ | ✅ |
| useOutlet | ✅ | ✅ | ✅ |
| useOutletContext | ✅ | ✅ | ✅ |
| useParams | ✅ | ✅ | ✅ |
| useResolvedPath | ✅ | ✅ | ✅ |
| useRevalidator | ✅ | ✅ | |
| useRouteError | ✅ | ✅ | |
| useRouteLoaderData | ✅ | ✅ | |
| useRoutes | ✅ | ✅ | ✅ |
| useSearchParams | ✅ | ✅ | ✅ |
| useSubmit | ✅ | ✅ | |
| useViewTransitionState | ✅ | ✅ | |
| isCookieFunction | ✅ | ✅ | |
| isSessionFunction | ✅ | ✅ | |
| createCookie | ✅ | ✅ | |
| createCookieSessionStorage | ✅ | ✅ | |
| createMemorySessionStorage | ✅ | ✅ | |
| createPath | ✅ | ✅ | ✅ |
| createRoutesFromElements | | ✅ | |
| createRoutesStub | ✅ | ✅ | |
| createSearchParams | ✅ | ✅ | ✅ |
| data | ✅ | ✅ | |
| generatePath | ✅ | ✅ | ✅ |
| href | ✅ | | |
| isCookie | ✅ | ✅ | |
| isRouteErrorResponse | ✅ | ✅ | |
| isSession | ✅ | ✅ | |
| matchPath | ✅ | ✅ | ✅ |
| matchRoutes | ✅ | ✅ | ✅ |
| parsePath | ✅ | ✅ | ✅ |
| redirect | ✅ | ✅ | |
| redirectDocument | ✅ | ✅ | |
| renderMatches | ✅ | ✅ | ✅ |
| replace | ✅ | ✅ | |
| resolvePath | ✅ | ✅ | ✅ |
+363
View File
@@ -0,0 +1,363 @@
---
title: Framework Adoption from Component Routes
order: 4
---
# Framework Adoption from Component Routes
If you are using `<RouterProvider>` please see [Framework Adoption from RouterProvider][upgrade-router-provider] instead.
If you are using `<Routes>` this is the right place.
The React Router Vite plugin adds framework features to React Router. This guide will help you adopt the plugin in your app. If you run into any issues, please reach out for help on [Twitter](https://x.com/remix_run) or [Discord](https://rmx.as/discord).
## Features
The Vite plugin adds:
- Route loaders, actions, and automatic data revalidation
- Type-safe Routes Modules
- Automatic route code-splitting
- Automatic scroll restoration across navigations
- Optional Static pre-rendering
- Optional Server rendering
The initial setup requires the most work. However, once complete, you can adopt new features incrementally, one route at a time.
## Prerequisites
To use the Vite plugin, your project requires:
- Node.js 20+ (if using Node as your runtime)
- Vite 5+
## 1. Install the Vite plugin
**👉 Install the React Router Vite plugin**
```shellscript nonumber
npm install -D @react-router/dev
```
**👉 Install a runtime adapter**
We will assume you are using Node as your runtime.
```shellscript nonumber
npm install @react-router/node
```
**👉 Swap out the React plugin for React Router.**
```diff filename=vite.config.ts
-import react from '@vitejs/plugin-react'
+import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
- react()
+ reactRouter()
],
});
```
## 2. Add the React Router config
**👉 Create a `react-router.config.ts` file**
Add the following to the root of your project. In this config you can tell React Router about your project, like where to find the app directory and to not use SSR (server-side rendering) for now.
```shellscript nonumber
touch react-router.config.ts
```
```ts filename=react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
appDirectory: "src",
ssr: false,
} satisfies Config;
```
## 3. Add the Root entry point
In a typical Vite app, the `index.html` file is the entry point for bundling. The React Router Vite plugin moves the entry point to a `root.tsx` file so you can use React to render the shell of your app instead of static HTML, and eventually upgrade to Server Rendering if you want.
**👉 Move your existing `index.html` to `root.tsx`**
For example, if your current `index.html` looks like this:
```html filename=index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>My App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
```
You would move that markup into `src/root.tsx` and delete `index.html`:
```shellscript nonumber
touch src/root.tsx
```
```tsx filename=src/root.tsx
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "react-router";
export function Layout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<head>
<meta charSet="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>My App</title>
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function Root() {
return <Outlet />;
}
```
## 4. Add client entry module
In the typical Vite app the `index.html` file points to `src/main.tsx` as the client entry point. React Router uses a file named `src/entry.client.tsx` instead.
**👉 Make `src/entry.client.tsx` your entry point**
If your current `src/main.tsx` looks like this:
```tsx filename=src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router";
import "./index.css";
import App from "./App";
ReactDOM.createRoot(
document.getElementById("root")!,
).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
);
```
You would rename it to `entry.client.tsx` and change it to this:
```tsx filename=src/entry.client.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
import "./index.css";
ReactDOM.hydrateRoot(
document,
<React.StrictMode>
<HydratedRouter />
</React.StrictMode>,
);
```
- Use `hydrateRoot` instead of `createRoot`
- Render a `<HydratedRouter>` instead of your `<App/>` component
- Note: we stopped rendering the `<App/>` component. We'll bring it back in a later step, but first we want to get the app to boot with the new entry point.
## 5. Shuffle stuff around
Between `root.tsx` and `entry.client.tsx`, you may want to shuffle some stuff around between them.
In general:
- `root.tsx` contains any rendering things like context providers, layouts, styles, etc.
- `entry.client.tsx` should be as minimal as possible
- Remember to _not_ try to render your existing `<App/>` component yet, we'll do that in a later step
Note that your `root.tsx` file will be statically generated and served as the entry point of your app, so just that module will need to be compatible with server rendering. This is where most of your trouble will come.
## 6. Set up your routes
The React Router Vite plugin uses a `routes.ts` file to configure your routes. For now we'll add a simple catchall route to get things going.
**👉 Set up a `catchall.tsx` route**
```shellscript nonumber
touch src/routes.ts src/catchall.tsx
```
```ts filename=src/routes.ts
import {
type RouteConfig,
route,
} from "@react-router/dev/routes";
export default [
// * matches all URLs, the ? makes it optional so it will match / as well
route("*?", "catchall.tsx"),
] satisfies RouteConfig;
```
**👉 Render a placeholder route**
Eventually we'll replace this with our original `App` component, but for now we'll just render something simple to make sure we can boot the app.
```tsx filename=src/catchall.tsx
export default function Component() {
return <div>Hello, world!</div>;
}
```
[View our guide on configuring routes][configuring-routes] to learn more about the `routes.ts` file.
## 7. Boot the app
At this point you should be able to boot the app and see the root layout.
**👉 Add `dev` script and run the app**
```json filename=package.json
"scripts": {
"dev": "react-router dev"
}
```
Now make sure you can boot your app at this point before moving on:
```shellscript
npm run dev
```
You will probably want to add `.react-router/` to your `.gitignore` file to avoid tracking unnecessary files in your repository.
```txt
.react-router/
```
You can check out [Type Safety][type-safety] to learn how to fully set up and use autogenerated type safety for params, loader data, and more.
## 8. Render your app
To get back to rendering your app, we'll update the "catchall" route we set up earlier that matches all URLs so that your existing `<Routes>` get a chance to render.
**👉 Update the catchall route to render your app**
```tsx filename=src/catchall.tsx
import App from "./App";
export default function Component() {
return <App />;
}
```
Your app should be back on the screen and working as usual!
## 9. Migrate a route to a Route Module
You can now incrementally migrate your routes to route modules.
Given an existing route like this:
```tsx filename=src/App.tsx
// ...
import About from "./containers/About";
export default function App() {
return (
<Routes>
<Route path="/about" element={<About />} />
</Routes>
);
}
```
**👉 Add the route definition to `routes.ts`**
```tsx filename=src/routes.ts
import {
type RouteConfig,
route,
} from "@react-router/dev/routes";
export default [
route("/about", "./pages/about.tsx"),
route("*?", "catchall.tsx"),
] satisfies RouteConfig;
```
**👉 Add the route module**
Edit the route module to use the [Route Module API][route-modules]:
```tsx filename=src/pages/about.tsx
export async function clientLoader() {
// you can now fetch data here
return {
title: "About page",
};
}
export default function Component({ loaderData }) {
return <h1>{loaderData.title}</h1>;
}
```
See [Type Safety][type-safety] to set up autogenerated type safety for params, loader data, and more.
The first few routes you migrate are the hardest because you often have to access various abstractions a bit differently than before (like in a loader instead of from a hook or context). But once the trickiest bits get dealt with, you get into an incremental groove.
## Enable SSR and/or Pre-rendering
If you want to enable server rendering and static pre-rendering, you can do so with the `ssr` and `prerender` options in the bundler plugin. For SSR you'll need to also deploy the server build to a server.
```ts filename=react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
ssr: true,
async prerender() {
return ["/", "/about", "/contact"];
},
} satisfies Config;
```
[upgrade-router-provider]: ./router-provider
[configuring-routes]: ../start/framework/routing
[route-modules]: ../start/framework/route-module
[type-safety]: ../how-to/route-module-type-safety
+280
View File
@@ -0,0 +1,280 @@
---
title: Future Flags
order: 1
---
# Future Flags and Deprecations
This guide walks you through the process of adopting future flags in your React Router app. By following this strategy, you will be able to upgrade to the next major version of React Router with minimal changes. To read more about future flags see [API Development Strategy][api-development-strategy].
We highly recommend you make a commit after each step and ship it instead of doing everything all at once. Most flags can be adopted in any order, with exceptions noted below.
## Update to latest v7.x
First update to the latest minor version of v7.x to have the latest future flags. You may see a number of deprecation warnings as you upgrade, which we'll cover below.
👉 Update to latest v7
```sh
npm install react-router@7 @react-router/{dev,node,etc.}@7
```
## `future.v8_middleware`
[MODES: framework, data]
<br/>
<br/>
**Background**
Middleware allows you to run code before and after the [`Response`][Response] generation for the matched path. This enables common patterns like authentication, logging, error handling, and data preprocessing in a reusable way. Please see the [docs](../how-to/middleware) for more information.
👉 **Enable the Flag**
In Framework mode:
```ts filename=react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
future: {
v8_middleware: true,
},
} satisfies Config;
```
In Data mode:
```ts
import { createBrowserRouter } from "react-router/dom";
const router = createBrowserRouter(routes, {
future: {
v8_middleware: true,
},
});
```
**Update your Code**
If you're using the `context` parameter in `loader` and `action` functions, you may need to update your code:
- In Framework mode, if you're using `react-router-serve`, you should not need to make any updates. Otherwise, this only applies if you have a custom server with a `getLoadContext` function. Please see the docs on the middleware [`getLoadContext` changes](../how-to/middleware#changes-to-getloadcontextapploadcontext) and the instructions to [migrate to the new API](../how-to/middleware#migration-from-apploadcontext).
- In Data mode, add the `Future` module augmentation described in the [middleware docs](../how-to/middleware#1-typescript-augment-future-for-loaderaction-context) so `context` is typed correctly.
## `future.v8_splitRouteModules`
[MODES: framework]
<br/>
<br/>
**Background**
This feature enables splitting client-side route exports (`clientLoader`, `clientAction`, `clientMiddleware`, `HydrateFallback`) into separate chunks that can be loaded independently from the route component. This allows these exports to be fetched and executed while the component code is still downloading, improving performance for client-side data loading.
This can be set to `true` for opt-in behavior, or `"enforce"` to require all routes to be splittable (which will cause build failures for routes that cannot be split due to shared code).
👉 **Enable the Flag**
```ts filename=react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
future: {
v8_splitRouteModules: true,
},
} satisfies Config;
```
**Update your Code**
No code changes are required. This is an optimization feature that works automatically once enabled.
## `future.v8_viteEnvironmentApi`
[MODES: framework]
<br/>
<br/>
**Background**
This enables support for the experimental Vite Environment API, which provides a more flexible and powerful way to configure Vite environments. This is only available when using Vite 6+.
👉 **Enable the Flag**
```ts filename=react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
future: {
v8_viteEnvironmentApi: true,
},
} satisfies Config;
```
**Update your Code**
Most users won't need to make any changes. However, if you have custom Vite configuration that previously relied on the `isSsrBuild` flag — such as a custom server build that sets `build.rollupOptions.input` — you'll need to move that configuration under the per-environment [Environment API][vite-environment] config instead.
For example, a custom server build should move its SSR `rollupOptions` from the top-level `build` config into `environments.ssr.build`:
```diff filename=vite.config.ts
import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";
-export default defineConfig(({ isSsrBuild }) => ({
- build: {
- rollupOptions: isSsrBuild
- ? {
- input: "./server/app.ts",
- }
- : undefined,
- },
+export default defineConfig({
+ environments: {
+ ssr: {
+ build: {
+ rollupOptions: {
+ input: "./server/app.ts",
+ },
+ },
+ },
+ },
plugins: [reactRouter()],
-}));
+});
```
See the [`node-custom-server` template][node-custom-server-template] for a complete example.
## `future.v8_passThroughRequests`
[MODES: framework]
<br/>
<br/>
**Background**
By default, React Router normalizes the `request.url` passed to your `loader`, `action`, and `middleware` functions by removing React Router's internal implementation details. Specifically, it removes `.data` suffixes and internal search parameters like `?index` and `?_routes`.
This flag eliminates that normalization and passes the raw HTTP `request` instance to your handlers. This provides a few benefits:
- Reduces server-side overhead by eliminating multiple `new Request()` calls on the critical path
- Allows you to distinguish document from data requests in your handlers based on the presence of a `.data` suffix (useful for [observability] purposes)
If you were previously relying on the normalization of `request.url`, you can switch to use the new sibling `url` parameter which contains a `URL` instance representing the normalized location.
👉 **Enable the Flag**
```ts filename=react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
future: {
v8_passThroughRequests: true,
},
} satisfies Config;
```
**Update your Code**
If your code relies on inspecting the request URL, you should review it for any assumptions about the URL format:
```tsx
// ❌ Before: assuming no `.data` suffix in `request.url` pathname
export async function loader({
request,
}: Route.LoaderArgs) {
let url = new URL(request.url);
if (url.pathname === "/path") {
// This check might now behave differently because the request pathname will
// contain the `.data` suffix on data requests
}
}
// ✅ After: use `url` for normalized routing logic and `request.url`
// for raw routing logic
export async function loader({
request,
url,
}: Route.LoaderArgs) {
if (url.pathname === "/path") {
// This will always have the `.data` suffix stripped
}
// And now you can distinguish between document versus data requests
let isDataRequest = new URL(
request.url,
).pathname.endsWith(".data");
}
```
## `future.v8_trailingSlashAwareDataRequests`
[MODES: framework]
<br/>
<br/>
**Background**
React Router serves Framework mode data requests from `.data` URLs. Previously, data requests for routes with and without trailing slashes could map to the same `.data` URL because trailing slashes were not considered during URL generation. This flag preserves trailing slash semantics for data request URLs to avoid ambiguity when your app distinguishes between trailing-slash and non-trailing-slash URLs.
Currently, your HTTP and `request` pathnames would be as follows for `/a/b/c` and `/a/b/c/`
| URL `/a/b/c` | **HTTP pathname** | **`request` pathname`** |
| ------------ | ----------------- | ----------------------- |
| **Document** | `/a/b/c` | `/a/b/c` ✅ |
| **Data** | `/a/b/c.data` | `/a/b/c` ✅ |
| URL `/a/b/c/` | **HTTP pathname** | **`request` pathname`** |
| ------------- | ----------------- | ----------------------- |
| **Document** | `/a/b/c/` | `/a/b/c/` ✅ |
| **Data** | `/a/b/c.data` | `/a/b/c` ⚠️ |
With this flag enabled, these pathnames will be made consistent though a new `_.data` format for client-side `.data` requests:
| URL `/a/b/c` | **HTTP pathname** | **`request` pathname`** |
| ------------ | ----------------- | ----------------------- |
| **Document** | `/a/b/c` | `/a/b/c` ✅ |
| **Data** | `/a/b/c.data` | `/a/b/c` ✅ |
| URL `/a/b/c/` | **HTTP pathname** | **`request` pathname`** |
| ------------- | ------------------ | ----------------------- |
| **Document** | `/a/b/c/` | `/a/b/c/` ✅ |
| **Data** | `/a/b/c/_.data` ⬅️ | `/a/b/c/` ✅ |
This flag also aligns the root data request to match this behavior by changing it from `/_root.data` to `/_.data`.
👉 **Enable the Flag**
```ts filename=react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
future: {
v8_trailingSlashAwareDataRequests: true,
},
} satisfies Config;
```
**Update your Code**
If you have custom app, CDN, cache, or rewrite logic that matches `.data` request URLs, update it to handle the new trailing-slash-aware `/_.data` format.
## Unstable Future Flags (Optional)
We document some [unstable] flags here as a reference for folks contributing to the project via beta testing, but they are not generally recommended for production use and may having breaking changes patch/minor releases - adopt with caution!
_No current unstable flags to document_
[api-development-strategy]: ../community/api-development-strategy
[unstable]: ../community/api-development-strategy#unstable-flags
[observability]: ../how-to/instrumentation
[Response]: https://developer.mozilla.org/en-US/docs/Web/API/Response
[vite-environment]: https://vite.dev/guide/api-environment
[node-custom-server-template]: https://github.com/remix-run/react-router-templates/blob/7c617a435510bc3add3a5395c07bc65328b65e9e/node-custom-server/vite.config.ts
+4
View File
@@ -0,0 +1,4 @@
---
title: Upgrading
order: 2
---
+403
View File
@@ -0,0 +1,403 @@
---
title: Upgrading from Remix
order: 3
---
# Upgrading from Remix
<docs-info>
React Router v7 requires the following minimum versions:
- `node@20`
- `react@18`
- `react-dom@18`
</docs-info>
React Router v7 is the next major version of Remix after v2 (see our ["Incremental Path to React 19" blog post][incremental-path-to-react-19] for more information).
If you have enabled all [Remix v2 future flags][v2-future-flags], upgrading from Remix v2 to React Router v7 mainly involves updating dependencies.
<docs-info>
The majority of steps 2-8 can be automatically updated using a [codemod][codemod] created by community member [James Restall][jrestall].
</docs-info>
## 1. Adopt future flags
**👉 Adopt future flags**
Adopt all existing [future flags][v2-future-flags] in your Remix v2 application.
## 2. Update dependencies
Most of the "shared" APIs that used to be re-exported through the runtime-specific packages (`@remix-run/node`, `@remix-run/cloudflare`, etc.) have all been collapsed into `react-router` in v7. So instead of importing from `@react-router/node` or `@react-router/cloudflare`, you'll import those directly from `react-router`.
```diff
-import { redirect } from "@remix-run/node";
+import { redirect } from "react-router";
```
The only APIs you should be importing from the runtime-specific packages in v7 are APIs that are specific to that runtime, such as `createFileSessionStorage` for Node and `createWorkersKVSessionStorage` for Cloudflare.
**👉 Run the codemod (automated)**
You can automatically update your packages and imports with the following [codemod][codemod]. This codemod updates all of your packages and imports. Be sure to commit any pending changes before running the codemod, in case you need to revert.
```shellscript nonumber
npx codemod remix/2/react-router/upgrade
```
**👉 Install the new dependencies**
After the codemod updates your dependencies, you need to install the dependencies to remove Remix packages and add the new React Router packages.
```shellscript nonumber
npm install
```
**👉 Update your dependencies (manual)**
If you prefer not to use the codemod, you can manually update your dependencies.
<details>
<summary>Expand to see a table of package name changes in alphabetical order</summary>
| Remix v2 Package | | React Router v7 Package |
| ---------------------------------- | --- | ------------------------------------------- |
| `@remix-run/architect` | ➡️ | `@react-router/architect` |
| `@remix-run/cloudflare` | ➡️ | `@react-router/cloudflare` |
| `@remix-run/dev` | ➡️ | `@react-router/dev` |
| `@remix-run/express` | ➡️ | `@react-router/express` |
| `@remix-run/fs-routes` | ➡️ | `@react-router/fs-routes` |
| `@remix-run/node` | ➡️ | `@react-router/node` |
| `@remix-run/react` | ➡️ | `react-router` |
| `@remix-run/route-config` | ➡️ | `@react-router/dev` |
| `@remix-run/routes-option-adapter` | ➡️ | `@react-router/remix-routes-option-adapter` |
| `@remix-run/serve` | ➡️ | `@react-router/serve` |
| `@remix-run/server-runtime` | ➡️ | `react-router` |
| `@remix-run/testing` | ➡️ | `react-router` |
</details>
## 3. Change `scripts` in `package.json`
<docs-info>
If you used the codemod you can skip this step as it was automatically completed.
</docs-info>
**👉 Update the scripts in your `package.json`**
| Script | Remix v2 | | React Router v7 |
| ----------- | ----------------------------------- | --- | ------------------------------------------ |
| `dev` | `remix vite:dev` | ➡️ | `react-router dev` |
| `build` | `remix vite:build` | ➡️ | `react-router build` |
| `start` | `remix-serve build/server/index.js` | ➡️ | `react-router-serve build/server/index.js` |
| `typecheck` | `tsc` | ➡️ | `react-router typegen && tsc` |
## 4. Add a `routes.ts` file
<docs-info>
If you used the codemod _and_ Remix v2 `v3_routeConfig` flag, you can skip this step as it was automatically completed.
</docs-info>
In React Router v7 you define your routes using the `app/routes.ts` file. View the [routing documentation][routing] for more information.
**👉 Update dependencies (if using Remix v2 `v3_routeConfig` flag)**
```diff filename=app/routes.ts
-import { type RouteConfig } from "@remix-run/route-config";
-import { flatRoutes } from "@remix-run/fs-routes";
-import { remixRoutesOptionAdapter } from "@remix-run/routes-option-adapter";
+import { type RouteConfig } from "@react-router/dev/routes";
+import { flatRoutes } from "@react-router/fs-routes";
+import { remixRoutesOptionAdapter } from "@react-router/remix-routes-option-adapter";
export default [
// however your routes are defined
] satisfies RouteConfig;
```
**👉 Add a `routes.ts` file (if _not_ using Remix v2 `v3_routeConfig` flag)**
```shellscript nonumber
touch app/routes.ts
```
For backwards-compatibility, there are a few ways to adopt `routes.ts` to align with your route setup in Remix v2:
1. If you were using the "flat routes" [file-based convention][fs-routing], you can continue to use that via the new `@react-router/fs-routes` package:
```ts filename=app/routes.ts
import { type RouteConfig } from "@react-router/dev/routes";
import { flatRoutes } from "@react-router/fs-routes";
export default flatRoutes() satisfies RouteConfig;
```
2. If you were using the "nested" convention from Remix v1 via the `@remix-run/v1-route-convention` package, you can continue using that as well in conjunction with `@react-router/remix-routes-option-adapter`:
```ts filename=app/routes.ts
import { type RouteConfig } from "@react-router/dev/routes";
import { remixRoutesOptionAdapter } from "@react-router/remix-routes-option-adapter";
import { createRoutesFromFolders } from "@remix-run/v1-route-convention";
export default remixRoutesOptionAdapter(
createRoutesFromFolders,
) satisfies RouteConfig;
```
3. If you were using the `routes` option to define config-based routes, you can keep that config via `@react-router/remix-routes-option-adapter`:
```ts filename=app/routes.ts
import { type RouteConfig } from "@react-router/dev/routes";
import { remixRoutesOptionAdapter } from "@react-router/remix-routes-option-adapter";
export default remixRoutesOptionAdapter(
(defineRoutes) => {
return defineRoutes((route) => {
route("/", "home/route.tsx", { index: true });
route("about", "about/route.tsx");
route("", "concerts/layout.tsx", () => {
route("trending", "concerts/trending.tsx");
route(":city", "concerts/city.tsx");
});
});
},
) satisfies RouteConfig;
```
- Be sure to also remove the `routes` option in your `vite.config.ts`:
```diff filename=vite.config.ts
export default defineConfig({
plugins: [
remix({
ssr: true,
- ignoredRouteFiles: ['**/*'],
- routes(defineRoutes) {
- return defineRoutes((route) => {
- route("/somewhere/cool/*", "catchall.tsx");
- });
- },
})
tsconfigPaths(),
],
});
```
## 5. Add a React Router config
**👉 Add `react-router.config.ts` your project**
The config that was previously passed to the `remix` plugin in `vite.config.ts` is now exported from `react-router.config.ts`.
Note: At this point you should remove the v3 future flags you added in step 1.
```shellscript nonumber
touch react-router.config.ts
```
```diff filename=vite.config.ts
export default defineConfig({
plugins: [
- remix({
- ssr: true,
- future: {/* all the v3 flags */}
- }),
+ reactRouter(),
tsconfigPaths(),
],
});
```
```diff filename=react-router.config.ts
+import type { Config } from "@react-router/dev/config";
+export default {
+ ssr: true,
+} satisfies Config;
```
## 6. Add React Router plugin to `vite.config`
<docs-info>
If you used the codemod you can skip this step as it was automatically completed.
</docs-info>
**👉 Add `reactRouter` plugin to `vite.config`**
Change `vite.config.ts` to import and use the new `reactRouter` plugin from `@react-router/dev/vite`:
```diff filename=vite.config.ts
-import { vitePlugin as remix } from "@remix-run/dev";
+import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [
- remix(),
+ reactRouter(),
tsconfigPaths(),
],
});
```
## 7. Enable type safety
<docs-info>
If you are not using TypeScript, you can skip this step.
</docs-info>
React Router automatically generates types for your route modules into a `.react-router/` directory at the root of your app. This directory is fully managed by React Router and should be gitignore'd. Learn more about the [new type safety features][type-safety].
**👉 Add `.react-router/` to `.gitignore`**
```txt
.react-router/
```
**👉 Update `tsconfig.json`**
Update the `types` field in your `tsconfig.json` to include:
- `.react-router/types/**/*` path in the `include` field
- The appropriate `@react-router/*` package in the `types` field
- `rootDirs` for simplified relative imports
```diff filename=tsconfig.json
{
"include": [
/* ... */
+ ".react-router/types/**/*"
],
"compilerOptions": {
- "types": ["@remix-run/node", "vite/client"],
+ "types": ["@react-router/node", "vite/client"],
/* ... */
+ "rootDirs": [".", "./.react-router/types"]
}
}
```
## 8. Rename components in entry files
<docs-info>
If you used the codemod you can skip this step as it was automatically completed.
</docs-info>
If you have an `entry.server.tsx` and/or an `entry.client.tsx` file in your application, you will need to update the main components in these files:
```diff filename=app/entry.server.tsx
-import { RemixServer } from "@remix-run/react";
+import { ServerRouter } from "react-router";
-<RemixServer context={remixContext} url={request.url} />,
+<ServerRouter context={remixContext} url={request.url} />,
```
```diff filename=app/entry.client.tsx
-import { RemixBrowser } from "@remix-run/react";
+import { HydratedRouter } from "react-router/dom";
hydrateRoot(
document,
<StrictMode>
- <RemixBrowser />
+ <HydratedRouter />
</StrictMode>,
);
```
## 9. Update types for `AppLoadContext`
<docs-info>
If you were using `remix-serve` you can skip this step. This is only applicable if you were using a custom server in Remix v2.
</docs-info>
Since React Router can be used as both a React framework _and_ a stand-alone routing library, the `context` argument for `LoaderFunctionArgs` and `ActionFunctionArgs` is now optional and typed as `any` by default. You can register types for your load context to get type safety for your loaders and actions.
👉 **Register types for your load context**
Before you migrate to the new `Route.LoaderArgs` and `Route.ActionArgs` types, you can temporarily augment `LoaderFunctionArgs` and `ActionFunctionArgs` with your load context type to ease migration.
```ts filename=app/env.ts
declare module "react-router" {
// Your AppLoadContext used in v2
interface AppLoadContext {
whatever: string;
}
// TODO: remove this once we've migrated to `Route.LoaderArgs` instead for our loaders
interface LoaderFunctionArgs {
context: AppLoadContext;
}
// TODO: remove this once we've migrated to `Route.ActionArgs` instead for our actions
interface ActionFunctionArgs {
context: AppLoadContext;
}
}
export {}; // necessary for TS to treat this as a module
```
<docs-info>
Using `declare module` to register types is a standard TypeScript technique called [module augmentation][ts-module-augmentation].
You can do this in any TypeScript file covered by your `tsconfig.json`'s `include` field, but we recommend a dedicated `env.ts` within your app directory.
</docs-info>
👉 **Use the new types**
Once you adopt the [new type generation][type-safety], you can remove the `LoaderFunctionArgs`/`ActionFunctionArgs` augmentations and use the `context` argument from [`Route.LoaderArgs`][server-loaders] and [`Route.ActionArgs`][server-actions] instead.
```ts filename=app/env.ts
declare module "react-router" {
// Your AppLoadContext used in v2
interface AppLoadContext {
whatever: string;
}
}
export {}; // necessary for TS to treat this as a module
```
```ts filename=app/routes/my-route.tsx
import type { Route } from "./+types/my-route";
export function loader({ context }: Route.LoaderArgs) {}
// { whatever: string } ^^^^^^^
export function action({ context }: Route.ActionArgs) {}
// { whatever: string } ^^^^^^^
```
Congratulations! You are now on React Router v7. Go ahead and run your application to make sure everything is working as expected.
[incremental-path-to-react-19]: https://remix.run/blog/incremental-path-to-react-19
[v2-future-flags]: https://remix.run/docs/start/future-flags
[routing]: ../start/framework/routing
[fs-routing]: ../how-to/file-route-conventions
[v7-changelog-types]: https://github.com/remix-run/react-router/blob/release-next/CHANGELOG.md#type-safety-improvements
[server-loaders]: ../start/framework/data-loading#server-data-loading
[server-actions]: ../start/framework/actions#server-actions
[ts-module-augmentation]: https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
[type-safety]: ../explanation/type-safety
[codemod]: https://app.codemod.com/registry/remix/2/react-router/upgrade
[jrestall]: https://github.com/jrestall
+442
View File
@@ -0,0 +1,442 @@
---
title: Framework Adoption from RouterProvider
order: 5
---
# Framework Adoption from RouterProvider
If you are not using `<RouterProvider>` please see [Framework Adoption from Component Routes][upgrade-component-routes] instead.
The React Router Vite plugin adds framework features to React Router. This guide will help you adopt the plugin in your app. If you run into any issues, please reach out for help on [Twitter](https://x.com/remix_run) or [Discord](https://rmx.as/discord).
## Features
The Vite plugin adds:
- Route loaders, actions, and automatic data revalidation
- Type-safe Routes Modules
- Automatic route code-splitting
- Automatic scroll restoration across navigations
- Optional Static pre-rendering
- Optional Server rendering
The initial setup requires the most work. However, once complete, you can adopt new features incrementally.
## Prerequisites
To use the Vite plugin, your project requires:
- Node.js 20+ (if using Node as your runtime)
- Vite 5+
## 1. Move route definitions into route modules
The React Router Vite plugin renders its own `RouterProvider`, so you can't render an existing `RouterProvider` within it. Instead, you will need to format all of your route definitions to match the [Route Module API][route-modules].
This step will take the longest, however there are several benefits to doing this regardless of adopting the React Router Vite plugin:
- Route modules will be lazy loaded, decreasing the initial bundle size of your app
- Route definitions will be uniform, simplifying your app's architecture
- Moving to route modules is incremental, you can migrate one route at a time
**👉 Move your route definitions into route modules**
Export each piece of your route definition as a separate named export, following the [Route Module API][route-modules].
```tsx filename=src/routes/about.tsx
export async function clientLoader() {
return {
title: "About",
};
}
export default function About() {
let data = useLoaderData();
return <div>{data.title}</div>;
}
// clientAction, ErrorBoundary, etc.
```
**👉 Create a convert function**
Create a helper function to convert route module definitions into the format expected by your data router:
```tsx filename=src/main.tsx
function convert(m: any) {
let {
clientLoader,
clientAction,
default: Component,
...rest
} = m;
return {
...rest,
loader: clientLoader,
action: clientAction,
Component,
};
}
```
**👉 Lazy load and convert your route modules**
Instead of importing your route modules directly, lazy load and convert them to the format expected by your data router.
Not only does your route definition now conform to the Route Module API, but you also get the benefits of code-splitting your routes.
```diff filename=src/main.tsx
let router = createBrowserRouter([
// ... other routes
{
path: "about",
- loader: aboutLoader,
- Component: About,
+ lazy: () => import("./routes/about").then(convert),
},
// ... other routes
]);
```
Repeat this process for each route in your app.
## 2. Install the Vite plugin
Once all of your route definitions are converted to route modules, you can adopt the React Router Vite plugin.
**👉 Install the React Router Vite plugin**
```shellscript nonumber
npm install -D @react-router/dev
```
**👉 Install a runtime adapter**
We will assume you are using Node as your runtime.
```shellscript nonumber
npm install @react-router/node
```
**👉 Swap out the React plugin for React Router**
```diff filename=vite.config.ts
-import react from '@vitejs/plugin-react'
+import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
- react()
+ reactRouter()
],
});
```
## 3. Add the React Router config
**👉 Create a `react-router.config.ts` file**
Add the following to the root of your project. In this config you can tell React Router about your project, like where to find the app directory and to not use SSR (server-side rendering) for now.
```shellscript nonumber
touch react-router.config.ts
```
```ts filename=react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
appDirectory: "src",
ssr: false,
} satisfies Config;
```
## 4. Add the Root entry point
In a typical Vite app, the `index.html` file is the entry point for bundling. The React Router Vite plugin moves the entry point to a `root.tsx` file so you can use React to render the shell of your app instead of static HTML, and eventually upgrade to Server Rendering if you want.
**👉 Move your existing `index.html` to `root.tsx`**
For example, if your current `index.html` looks like this:
```html filename=index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>My App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
```
You would move that markup into `src/root.tsx` and delete `index.html`:
```shellscript nonumber
touch src/root.tsx
```
```tsx filename=src/root.tsx
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "react-router";
export function Layout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<head>
<meta charSet="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>My App</title>
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function Root() {
return <Outlet />;
}
```
**👉 Move everything above `RouterProvider` to `root.tsx`**
Any global styles, context providers, etc. should be moved into `root.tsx` so they can be shared across all routes.
For example, if your `App.tsx` looks like this:
```tsx filename=src/App.tsx
import "./index.css";
export default function App() {
return (
<OtherProviders>
<AppLayout>
<RouterProvider router={router} />
</AppLayout>
</OtherProviders>
);
}
```
You would move everything above the `RouterProvider` into `root.tsx`.
```diff filename=src/root.tsx
+import "./index.css";
// ... other imports and Layout
export default function Root() {
return (
+ <OtherProviders>
+ <AppLayout>
<Outlet />
+ </AppLayout>
+ </OtherProviders>
);
}
```
## 5. Add client entry module (optional)
In the typical Vite app the `index.html` file points to `src/main.tsx` as the client entry point. React Router uses a file named `src/entry.client.tsx` instead.
If no `entry.client.tsx` exists, the React Router Vite plugin will use a default, hidden one.
**👉 Make `src/entry.client.tsx` your entry point**
If your current `src/main.tsx` looks like this:
```tsx filename=src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router";
import App from "./App";
const router = createBrowserRouter([
// ... route definitions
]);
ReactDOM.createRoot(
document.getElementById("root")!,
).render(
<React.StrictMode>
<RouterProvider router={router} />;
</React.StrictMode>,
);
```
You would rename it to `entry.client.tsx` and change it to this:
```tsx filename=src/entry.client.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
ReactDOM.hydrateRoot(
document,
<React.StrictMode>
<HydratedRouter />
</React.StrictMode>,
);
```
- Use `hydrateRoot` instead of `createRoot`
- Render a `<HydratedRouter>` instead of your `<App/>` component
- Note: We are no longer creating the routes and manually passing them to `<RouterProvider />`. We will migrate our route definitions in the next step.
## 6. Migrate your routes
The React Router Vite plugin uses a `routes.ts` file to configure your routes. The format will be pretty similar to the definitions of your data router.
**👉 Move definitions to a `routes.ts` file**
```shellscript nonumber
touch src/routes.ts src/catchall.tsx
```
Move your route definitions to `routes.ts`. Note that the schemas don't match exactly, so you will get type errors; we'll fix this next.
```diff filename=src/routes.ts
+import type { RouteConfig } from "@react-router/dev/routes";
-const router = createBrowserRouter([
+export default [
{
path: "/",
lazy: () => import("./routes/layout").then(convert),
children: [
{
index: true,
lazy: () => import("./routes/home").then(convert),
},
{
path: "about",
lazy: () => import("./routes/about").then(convert),
},
{
path: "todos",
lazy: () => import("./routes/todos").then(convert),
children: [
{
path: ":id",
lazy: () =>
import("./routes/todo").then(convert),
},
],
},
],
},
-]);
+] satisfies RouteConfig;
```
**👉 Replace the `lazy` loader with a `file` loader**
```diff filename=src/routes.ts
export default [
{
path: "/",
- lazy: () => import("./routes/layout").then(convert),
+ file: "./routes/layout.tsx",
children: [
{
index: true,
- lazy: () => import("./routes/home").then(convert),
+ file: "./routes/home.tsx",
},
{
path: "about",
- lazy: () => import("./routes/about").then(convert),
+ file: "./routes/about.tsx",
},
{
path: "todos",
- lazy: () => import("./routes/todos").then(convert),
+ file: "./routes/todos.tsx",
children: [
{
path: ":id",
- lazy: () => import("./routes/todo").then(convert),
+ file: "./routes/todo.tsx",
},
],
},
],
},
] satisfies RouteConfig;
```
[View our guide on configuring routes][configuring-routes] to learn more about the `routes.ts` file and helper functions to further simplify the route definitions.
## 7. Boot the app
At this point you should be fully migrated to the React Router Vite plugin. Go ahead and update your `dev` script and run the app to make sure everything is working.
**👉 Add `dev` script and run the app**
```json filename=package.json
"scripts": {
"dev": "react-router dev"
}
```
Now make sure you can boot your app at this point before moving on:
```shellscript
npm run dev
```
You will probably want to add `.react-router/` to your `.gitignore` file to avoid tracking unnecessary files in your repository.
```txt
.react-router/
```
You can checkout [Type Safety][type-safety] to learn how to fully setup and use autogenerated type safety for params, loader data, and more.
## Enable SSR and/or Pre-rendering
If you want to enable server rendering and static pre-rendering, you can do so with the `ssr` and `prerender` options in the bundler plugin. For SSR you'll need to also deploy the server build to a server.
```ts filename=react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
ssr: true,
async prerender() {
return ["/", "/about", "/contact"];
},
} satisfies Config;
```
[upgrade-component-routes]: ./component-routes
[configuring-routes]: ../start/framework/routing
[route-modules]: ../start/framework/route-module
[type-safety]: ../how-to/route-module-type-safety
+382
View File
@@ -0,0 +1,382 @@
---
title: Upgrading from v6
order: 2
---
# Upgrading from v6
<docs-info>
React Router v7 requires the following minimum versions:
- `node@20`
- `react@18`
- `react-dom@18`
</docs-info>
The v7 upgrade has no breaking changes if you have enabled all future flags. These flags allow you to update your app one change at a time. We highly recommend you make a commit after each step and ship it instead of doing everything all at once.
## Update to latest v6.x
First update to the latest minor version of v6.x to have the latest future flags and console warnings.
👉 **Update to latest v6**
```shellscript nonumber
npm install react-router-dom@6
```
### v7_relativeSplatPath
**Background**
Changes the relative path matching and linking for multi-segment splats paths like `dashboard/*` (vs. just `*`). [View the CHANGELOG](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#futurev7_relativesplatpath) for more information.
👉 **Enable the flag**
Enabling the flag depends on the type of router:
```tsx
<BrowserRouter
future={{
v7_relativeSplatPath: true,
}}
/>
```
```tsx
createBrowserRouter(routes, {
future: {
v7_relativeSplatPath: true,
},
});
```
**Update your Code**
If you have any routes with a path + a splat like `<Route path="dashboard/*">` that have relative links like `<Link to="relative">` or `<Link to="../relative">` beneath them, you will need to update your code.
👉 **Split the `<Route>` into two**
Split any multi-segment splat `<Route>` into a parent route with the path and a child route with the splat:
```diff
<Routes>
<Route path="/" element={<Home />} />
- <Route path="dashboard/*" element={<Dashboard />} />
+ <Route path="dashboard">
+ <Route path="*" element={<Dashboard />} />
+ </Route>
</Routes>
// or
createBrowserRouter([
{ path: "/", element: <Home /> },
{
- path: "dashboard/*",
- element: <Dashboard />,
+ path: "dashboard",
+ children: [{ path: "*", element: <Dashboard /> }],
},
]);
```
👉 **Update relative links**
Update any `<Link>` elements within that route tree to include the extra `..` relative segment to continue linking to the same place:
```diff
function Dashboard() {
return (
<div>
<h2>Dashboard</h2>
<nav>
<Link to="/">Dashboard Home</Link>
- <Link to="team">Team</Link>
- <Link to="projects">Projects</Link>
+ <Link to="../team">Team</Link>
+ <Link to="../projects">Projects</Link>
</nav>
<Routes>
<Route path="/" element={<DashboardHome />} />
<Route path="team" element={<DashboardTeam />} />
<Route
path="projects"
element={<DashboardProjects />}
/>
</Routes>
</div>
);
}
```
### v7_startTransition
**Background**
This uses `React.useTransition` instead of `React.useState` for Router state updates. View the [CHANGELOG](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#futurev7_starttransition) for more information.
👉 **Enable the flag**
```tsx
<BrowserRouter
future={{
v7_startTransition: true,
}}
/>
// or
<RouterProvider
future={{
v7_startTransition: true,
}}
/>
```
👉 **Update your Code**
You don't need to update anything unless you are using `React.lazy` _inside_ of a component.
Using `React.lazy` inside of a component is incompatible with `React.useTransition` (or other code that makes promises inside of components). Move `React.lazy` to the module scope and stop making promises inside of components. This is not a limitation of React Router but rather incorrect usage of React.
<docs-info>We added a flag to opt-out of `React.startTransition` in v7 so you can use that to upgrade to v7 without adopting React transition-enabled navigations if needed. See the [transition docs][transitions] for more information.</docs-info>
### v7_fetcherPersist
<docs-warning>If you are not using a `<RouterProvider>` you can skip this</docs-warning>
**Background**
The fetcher lifecycle is now based on when it returns to an idle state rather than when its owner component unmounts: [View the CHANGELOG](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#persistence-future-flag-futurev7_fetcherpersist) for more information.
**Enable the Flag**
```tsx
createBrowserRouter(routes, {
future: {
v7_fetcherPersist: true,
},
});
```
**Update your Code**
It's unlikely to affect your app. You may want to check any usage of `useFetchers` as they may persist longer than they did before. Depending on what you're doing, you may render something longer than before.
### v7_normalizeFormMethod
<docs-warning>If you are not using a `<RouterProvider>` you can skip this</docs-warning>
This normalizes `formMethod` fields as uppercase HTTP methods to align with the `fetch()` behavior. [View the CHANGELOG](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#futurev7_normalizeformmethod) for more information.
👉 **Enable the Flag**
```tsx
createBrowserRouter(routes, {
future: {
v7_normalizeFormMethod: true,
},
});
```
**Update your Code**
If any of your code is checking for lowercase HTTP methods, you will need to update it to check for uppercase HTTP methods (or call `toLowerCase()` on it).
👉 **Compare `formMethod` to UPPERCASE**
```diff
-useNavigation().formMethod === "post"
-useFetcher().formMethod === "get";
+useNavigation().formMethod === "POST"
+useFetcher().formMethod === "GET";
```
### v7_partialHydration
<docs-warning>If you are not using a `<RouterProvider>` you can skip this</docs-warning>
This enables partial hydration of a data router which is primarily used for SSR frameworks, but it is also useful if you are using `lazy` to load your route modules. It's unlikely you need to worry about this, just turn the flag on. [View the CHANGELOG](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#partial-hydration) for more information.
👉 **Enable the Flag**
```tsx
createBrowserRouter(routes, {
future: {
v7_partialHydration: true,
},
});
```
**Update your Code**
With partial hydration, you need to provide a `HydrateFallback` component to render during initial hydration. Additionally, if you were using `fallbackElement` before, you need to remove it as it is now deprecated. In most cases, you will want to reuse the `fallbackElement` as the `HydrateFallback`.
👉 **Replace `fallbackElement` with `HydrateFallback`**
```diff
const router = createBrowserRouter(
[
{
path: "/",
Component: Layout,
+ HydrateFallback: Fallback,
// or
+ hydrateFallbackElement: <Fallback />,
children: [],
},
],
);
<RouterProvider
router={router}
- fallbackElement={<Fallback />}
/>
```
### v7_skipActionErrorRevalidation
<docs-warning>If you are not using a `createBrowserRouter` you can skip this</docs-warning>
When this flag is enabled, loaders will no longer revalidate by default after an action throws/returns a `Response` with a `4xx`/`5xx` status code. You may opt-into revalidation in these scenarios via `shouldRevalidate` and the `actionStatus` parameter.
👉 **Enable the Flag**
```tsx
createBrowserRouter(routes, {
future: {
v7_skipActionErrorRevalidation: true,
},
});
```
**Update your Code**
In most cases, you probably won't have to make changes to your app code. Usually, if an action errors, it's unlikely data was mutated and needs revalidation. If any of your code _does_ mutate data in action error scenarios you have 2 options:
👉 **Option 1: Change the `action` to avoid mutations in error scenarios**
```js
// Before
async function action() {
await mutateSomeData();
if (detectError()) {
throw new Response(error, { status: 400 });
}
await mutateOtherData();
// ...
}
// After
async function action() {
if (detectError()) {
throw new Response(error, { status: 400 });
}
// All data is now mutated after validations
await mutateSomeData();
await mutateOtherData();
// ...
}
```
👉 **Option 2: Opt-into revalidation via `shouldRevalidate` and `actionStatus`**
```js
async function action() {
await mutateSomeData();
if (detectError()) {
throw new Response(error, { status: 400 });
}
await mutateOtherData();
}
async function loader() { ... }
function shouldRevalidate({ actionStatus, defaultShouldRevalidate }) {
if (actionStatus != null && actionStatus >= 400) {
// Revalidate this loader when actions return a 4xx/5xx status
return true;
}
return defaultShouldRevalidate;
}
```
## Deprecations
The `json` and `defer` methods are deprecated in favor of returning raw objects.
```diff
async function loader() {
- return json({ data });
+ return { data };
```
If you were using `json` to serialize your data to JSON, you can use the native [Response.json()][response-json] method instead.
## Upgrade to v7
Now that your app is caught up, you can simply update to v7 (theoretically!) without issue.
👉 **Install v7**
```shellscript nonumber
npm install react-router-dom@latest
```
👉 **Replace react-router-dom with react-router**
In v7 we no longer need `"react-router-dom"` as the packages have been simplified. You can import everything from `"react-router"`:
```shellscript nonumber
npm uninstall react-router-dom
npm install react-router@latest
```
Note you only need `"react-router"` in your package.json.
👉 **Update imports**
Now you should update your imports to use `react-router`:
```diff
-import { useLocation } from "react-router-dom";
+import { useLocation } from "react-router";
```
Instead of manually updating imports, you can use this command. Make sure your git working tree is clean though so you can revert if it doesn't work as expected.
```shellscript nonumber
find ./path/to/src \( -name "*.tsx" -o -name "*.ts" -o -name "*.js" -o -name "*.jsx" \) -type f -exec sed -i '' 's|from "react-router-dom"|from "react-router"|g' {} +
```
If you have GNU `sed` installed (most Linux distributions), use this command instead:
```shellscript nonumber
find ./path/to/src \( -name "*.tsx" -o -name "*.ts" -o -name "*.js" -o -name "*.jsx" \) -type f -exec sed -i 's|from "react-router-dom"|from "react-router"|g' {} +
```
👉 **Update DOM-specific imports**
`RouterProvider` and `HydratedRouter` come from a deep import because they depend on `"react-dom"`:
```diff
-import { RouterProvider } from "react-router-dom";
+import { RouterProvider } from "react-router/dom";
```
Note you should use a top-level import for non-DOM contexts, such as Jest tests:
```diff
-import { RouterProvider } from "react-router-dom";
+import { RouterProvider } from "react-router";
```
Congratulations, you're now on v7!
[react-flushsync]: https://react.dev/reference/react-dom/flushSync
[response-json]: https://developer.mozilla.org/en-US/docs/Web/API/Response/json
[data-util]: https://api.reactrouter.com/v7/functions/react-router.data.html
[transitions]: ../explanation/react-transitions