diff --git a/llm.txt b/llm.txt deleted file mode 100644 index 5b3d9f7004..0000000000 --- a/llm.txt +++ /dev/null @@ -1,15107 +0,0 @@ ---- -title: Server vs. Client Code Execution -hidden: true ---- ---- -title: Sessions and Cookies ---- - -# Sessions and Cookies - -## 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( - { - // 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 more all the 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 ( -
- {error ?
{error}
: null} -
-
-

Please sign in

-
- - -
-
- ); -} -``` - -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 ( - <> -

Are you sure you want to log out?

-
- -
- Never mind - - ); -} -``` - -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. - -### 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 `
` 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 ( -
- {loaderData.showBanner ? ( -
- Don't miss our sale! - - - - -
- ) : null} -

Welcome!

-
- ); -} -``` - -### 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 ---- -title: Progressive Enhancement ---- - -# Progressive Enhancement - -> 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. - -- [Wikipedia][wikipedia] - -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 ``. These render an `` 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 ( -
- - -
- ); -} -``` - -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 ( - - - - - ); -} -``` - -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 `
` 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 ( - - - - - ); -} -``` - -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 ( -
- - {isSearching ? : } - - ); -} -``` - -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 ---- -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 `` 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()`) ---- -title: Location Object -hidden: true ---- - - ---- -title: Route Matching -hidden: true -# want to explain how the matching algorithm works with any potential gotchas ---- - -# Route Matching ---- -title: Automatic Code Splitting ---- - -# Automatic Code Splitting - -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 ensures 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
{loaderData.message}
; -} -``` - -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 ---- -title: Race Conditions ---- - -# Race Conditions - -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 ``, 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 ( - - - - // submit the form onChange to get the list of cities - fetcher.submit(event.target.form) - } - /> - - {fetcher.data ? ( - - {fetcher.data.length > 0 ? ( - - {fetcher.data.map((city) => ( - - ))} - - ) : ( - No results found - )} - - ) : null} - - - ); -} -``` - -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. ---- -title: State Management ---- - -# State Management - -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:** A predictable state container for JavaScript apps. -- **React Query:** Hooks for fetching, caching, and updating asynchronous data in React. -- **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 ( -
-
- - -
- {view === "list" ? : } -
- ); -} -``` - -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 ( -
-
- - -
- {view === "list" ? : } -
- ); -} -``` - -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 ( -
-
- - -
- {view === "list" ? : } -
- ); -} -``` - -### 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 ( -
- - -
- ); -} -``` - -#### 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 ( -
- - -
- ); -} -``` - -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 ( -
- - - - -
- ); -} -``` - -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 ( -
{ - event.preventDefault(); - handleSubmit(); - }} - > -

- { - // Synchronizing form state for the fetch - setUserName(event.target.value); - }} - /> - {userNameError ? {userNameError} : null} -

- -

- { - // Synchronizing form state for the fetch - setPassword(event.target.value); - }} - /> - {passwordError ? {passwordError} : null} -

- - - - {isSubmitting ? : null} - - ); -} -``` - -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 ( -
-

- - {userNameError ? {userNameError} : null} -

- -

- - {passwordError ? {passwordError} : null} -

- - - - {isSubmitting ? : null} - - ); -} -``` - -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. - -[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 ---- -title: Special Files ---- - -# Special Files - -There are a few special files that React Router looks for in your project. Not all of these files are required - -## react-router.config.ts - -**This file is optional** - -The config file is used to configure certain aspects of your app, such as whether you are using server-side rendering, where certain directories are located, and more. - -```tsx filename=react-router.config.ts -import type { Config } from "@react-router/dev/config"; - -export default { - // Config options... -} satisfies Config; -``` - -See the details on [react-router config API][react-router-config] for more information. - -## root.tsx - -**This file is required** - -The "root" route (`app/root.tsx`) is the only _required_ route in your React Router application because it is the parent to all routes in your `routes/` directory and is in charge of rendering the root `` document. - -Because the root route manages your document, it is the proper place to render a handful of "document-level" components React Router provides. These components are to be used once inside your root route and they include everything React Router figured out or built in order for your page to render properly. - -```tsx filename=app/root.tsx -import type { LinksFunction } from "react-router"; -import { - Links, - Meta, - Outlet, - Scripts, - ScrollRestoration, -} from "react-router"; - -import "./global-styles.css"; - -export default function App() { - return ( - - - - - - {/* All `meta` exports on all routes will render here */} - - - {/* All `link` exports on all routes will render here */} - - - - {/* Child routes render here */} - - - {/* Manages scroll position for client-side transitions */} - {/* If you use a nonce-based content security policy for scripts, you must provide the `nonce` prop. Otherwise, omit the nonce prop as shown here. */} - - - {/* Script tags go here */} - {/* If you use a nonce-based content security policy for scripts, you must provide the `nonce` prop. Otherwise, omit the nonce prop as shown here. */} - - - - ); -} -``` - -### Layout export - -The root route supports all [route module exports][route-module]. - -The root route also supports an additional optional `Layout` export. The `Layout` component serves 2 purposes: - -1. Avoid duplicating your document's "app shell" across your root component, `HydrateFallback`, and `ErrorBoundary` -2. Prevent React from re-mounting your app shell elements when switching between the root component/`HydrateFallback`/`ErrorBoundary` which can cause a FOUC if React removes and re-adds `` tags from your `` component. - -```tsx filename=app/root.tsx lines=[10-31] -export function Layout({ children }) { - return ( - - - - - - - - - {/* children will be the root Component, ErrorBoundary, or HydrateFallback */} - {children} - - - - - ); -} - -export default function App() { - return ; -} - -export function ErrorBoundary() {} -``` - -**A note on `useLoaderData`in the `Layout` Component** - -`useLoaderData` is not permitted to be used in `ErrorBoundary` components because it is intended for the happy-path route rendering, and its typings have a built-in assumption that the `loader` ran successfully and returned something. That assumption doesn't hold in an `ErrorBoundary` because it could have been the `loader` that threw and triggered the boundary! In order to access loader data in `ErrorBoundary`'s, you can use `useRouteLoaderData` which accounts for the loader data potentially being `undefined`. - -Because your `Layout` component is used in both success and error flows, this same restriction holds. If you need to fork logic in your `Layout` depending on if it was a successful request or not, you can use `useRouteLoaderData("root")` and `useRouteError()`. - -Because your `` component is used for rendering the `ErrorBoundary`, you should be _very defensive_ to ensure that you can render your `ErrorBoundary` without encountering any render errors. If your `Layout` throws another error trying to render the boundary, then it can't be used and your UI will fall back to the very minimal built-in default `ErrorBoundary`. - -```tsx filename=app/root.tsx lines=[6-7,19-29,32-34] -export function Layout({ - children, -}: { - children: React.ReactNode; -}) { - const data = useRouteLoaderData("root"); - const error = useRouteError(); - - return ( - - - - - - -