Push V1 app
This commit is contained in:
+50
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
+137
@@ -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
@@ -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()`)
|
||||
+86
@@ -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
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: Explanations
|
||||
order: 5
|
||||
---
|
||||
+78
@@ -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
@@ -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 -->
|
||||
+150
@@ -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 ways–once for JavaScript and once without–it'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
@@ -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.
|
||||
+160
@@ -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
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Route Matching
|
||||
hidden: true
|
||||
# want to explain how the matching algorithm works with any potential gotchas
|
||||
---
|
||||
|
||||
# Route Matching
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: Server vs. Client Code Execution
|
||||
hidden: true
|
||||
---
|
||||
+465
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
```
|
||||
Reference in New Issue
Block a user