Multiple routes example with index query param
Example route structure: a parent route 'projects' at ./pages/projects.tsx with a child index route at ./pages/projects/index.tsx creates two routes matching /projects. Form submission to /projects targets the parent action, while /projects?index targets the index action.
Index routes share URL with parent route
Index routes match the same URL as their parent route. For example, both a parent route at `/projects` and an index route at `/projects` match the same URL path.
Index query param appears in URLs with nested routes
When submitting forms in applications with nested routes, a `?index` query parameter may appear in the URL. This happens because multiple routes in the hierarchy can match the same URL.
Index param works with Form, useSubmit, and useFetcher
The `?index` param can be used to control form submission routing across multiple APIs: the Form component, the useSubmit hook, and the useFetcher hook. All support both explicit `action` props with `?index` and automatic appending when rendered in context.
Form in index route automatically appends ?index param
When a Form component is rendered in an index route without an explicit action prop, React Router automatically appends the `?index` query parameter so the form posts to the index route's action instead of the parent route's action.
Index query param disambiguates form submissions between parent and index routes
Because multiple routes can match the same URL, the `?index` param lets you disambiguate which route's action should handle a form submission. Without `?index`, the form submits to the parent route; with `?index`, it submits to the index route.
routes.ts file requirement
routes.ts is a required file that contains route configuration mapping URLs to components.
Route.ComponentProps type for default export
The `Route.ComponentProps` type provides type safety for the default component export, including typed `loaderData` that matches the return type of the loader function.
Automatic type generation during development
When running `react-router dev` or when a custom server calls `vite.createServer`, React Router's Vite plugin automatically generates up-to-date types without requiring manual `typegen` command execution.
react-router typegen command
The `react-router typegen` command manually generates type definition files. It can be run with the `--watch` flag to automatically regenerate types as files change.
Route-specific type imports with +types prefix
React Router generates route-specific types that can be imported using the pattern `import type { Route } from "./+types/<route-file>"`. These generated types provide type safety for route module exports including loaders, actions, and components.
Generated route types
React Router's typegen command generates the following types for each route: LoaderArgs, ClientLoaderArgs, ActionArgs, ClientActionArgs, HydrateFallbackProps, ComponentProps (for the default export), and ErrorBoundaryProps.
React Router type generation process
React Router's type generation executes the route config file (app/routes.ts by default) to determine all routes in the app, then generates a `+types/<route-file>.d.ts` type definition file for each route in the `.react-router/types/` directory. With `rootDirs` configured in TypeScript, these files can be imported as if they were next to the route modules.
Route.LoaderArgs type includes route params
When importing `Route` types from the generated `+types` directory, the `Route.LoaderArgs` type automatically includes typed parameters from the route path. For a route like `products/:id`, the params object is typed as `{ id: string }`.
Error boundaries catch route errors automatically
Route modules automatically catch errors in your code and render the closest ErrorBoundary. This prevents rendering an empty page to users.
Example throwing data in loader with status code
import { data } from "react-router";
export async function loader({ params }) {
let record = await fakeDb.getRecord(params.id);
if (!record) {
throw data("Record Not Found", { status: 404 });
}
return record;
}
This example shows how to throw data with a 404 status code when a record is not found in a loader.
Data sent with throw data() is not sanitized
Data sent with throw data(yourData) is not sanitized in production because that data is intended to be rendered to the user.
Closest error boundary renders when error is thrown
When an error is thrown, the closest error boundary in the route hierarchy will be rendered to handle that error.
Example Data Mode root error boundary
import { useRouteError } from "react-router";
let router = createBrowserRouter([
{
path: "/",
ErrorBoundary: RootErrorBoundary,
Component: Root,
},
]);
function Root() {
/* ... */
}
function RootErrorBoundary() {
let error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<>
<h1>
{error.status} {error.statusText}
</h1>
<p>{error.data}</p>
</>
);
} else if (error instanceof Error) {
return (
<div>
<h1>Error</h1>
<p>{error.message}</p>
<p>The stack trace is:</p>
<pre>{error.stack}</pre>
</div>
);
} else {
return <h1>Unknown Error</h1>;
}
}
This example shows how to handle three error cases in Data Mode using useRouteError hook.
Throw data with status codes for expected errors
Use throw data() with a proper status code to intentionally pass errors to the closest error boundary when your loader or action cannot complete its task, such as when returning a 404 for a not found record. This is an exception to the rule against intentionally throwing errors.
Do not intentionally throw errors for control flow
It is not recommended to intentionally throw errors to force the error boundary to render as a means of control flow. Error Boundaries are primarily for catching unintentional errors in your code.
Framework Mode error sanitization in production
In Framework Mode when building for production, any errors that happen on the server are automatically sanitized before being sent to the browser to prevent leaking sensitive server information like stack traces. A thrown Error will have a generic message and no stack trace in production in the browser, while the original error is untouched on the server.
Error boundaries work across all route module APIs
Error boundaries catch errors not just from loaders and actions, but from all route module APIs: loaders, actions, components, headers, links, and meta.
Example Framework Mode root error boundary
import { Route } from "./+types/root";
export function ErrorBoundary({
error,
}: Route.ErrorBoundaryProps) {
if (isRouteErrorResponse(error)) {
return (
<>
<h1>
{error.status} {error.statusText}
</h1>
<p>{error.data}</p>
</>
);
} else if (error instanceof Error) {
return (
<div>
<h1>Error</h1>
<p>{error.message}</p>
<p>The stack trace is:</p>
<pre>{error.stack}</pre>
</div>
);
} else {
return <h1>Unknown Error</h1>;
}
}
This example shows how to handle three error cases in Framework Mode: RouteErrorResponse objects, Error instances, and unknown values.
Framework Mode error boundary receives error as prop
In Framework Mode, errors are passed to the route-level error boundary as a prop via Route.ErrorBoundaryProps, so you do not need to use a hook to grab it. The ErrorBoundary component receives an object with an error property.
Data Mode error boundary uses useRouteError hook
In Data Mode, the ErrorBoundary does not receive props. Instead, use the useRouteError hook to access the error within the error boundary component.
Error boundaries are not for form validation or error reporting
Error boundaries should not be used for rendering form validation errors or error reporting. Use Form Validation and Error Reporting instead for those purposes.
Root error boundary handles three error cases
All applications should export a root error boundary that handles three main cases: thrown data with a status code and text, instances of errors with a stack trace, and randomly thrown values.
useFetcher hook basic usage
Import and call useFetcher to create a fetcher instance: let fetcher = useFetcher();. The fetcher provides a Form component and state tracking for managing data interactions without navigation.
What are fetchers in React Router
Fetchers are useful for creating complex, dynamic user interfaces that require multiple, concurrent data interactions without causing a navigation. Fetchers track their own independent state and can be used to load data, mutate data, submit forms, and generally interact with loaders and actions.
Fetcher action validation example
Example showing error handling and validation in a fetcher action: export async function clientAction({ request }) { let data = await request.formData(); let title = data.get("title") as string; if (title.trim() === "") { return { ok: false, error: "Title cannot be empty" }; } localStorage.setItem("title", title); return { ok: true, error: null }; } export default function Component() { let fetcher = useFetcher(); return <fetcher.Form method="post"><input type="text" name="title" />{fetcher.data?.error && <p style={{ color: "red" }}>{fetcher.data.error}</p>}</fetcher.Form>; }
fetcher.Form component
The fetcher.Form component renders a form that submits to a route action or loader without causing navigation. It accepts method and action props. Example: <fetcher.Form method="post"><input type="text" name="title" /></fetcher.Form>
Fetcher for calling actions example
Example showing fetcher with action and optimistic UI: export async function clientAction({ request }) { let data = await request.formData(); localStorage.setItem("title", data.get("title")); return { ok: true }; } export default function Component() { let data = useLoaderData(); let fetcher = useFetcher(); let title = fetcher.formData?.get("title") || data.title; return <div><h1>{title}</h1><fetcher.Form method="post"><input type="text" name="title" />{fetcher.state !== "idle" && <p>Saving...</p>}</fetcher.Form></div>; }
fetcher.submit method
Fetchers can be submitted programmatically with fetcher.submit(). Pass the form element as the first argument to submit the form data: fetcher.submit(event.currentTarget.form). The fetcher will read the form's attributes and serialize the data from its elements.
Type inference with useFetcher
Use useFetcher with a type parameter to get type inference from a loader: import type { loader } from "./search-users"; let fetcher = useFetcher<typeof loader>();. Always use import type to import only the types, not the runtime code.
fetcher.data property
The fetcher.data property contains data returned from an action. This is primarily useful for returning error messages to the user for a failed mutation. The data is available after the action completes.
fetcher.formData property
The fetcher.formData property contains the serialized form data from the current submission. It is available immediately after form submission and can be used to implement optimistic UI updates before the action completes. Example: let title = fetcher.formData?.get("title") || data.title;
Fetcher for data loading with search example
Example showing fetcher for loading data from a search route: export async function loader({ request }) { let url = new URL(request.url); let query = url.searchParams.get("q"); return users.filter((user) => user.name.toLowerCase().includes(query.toLowerCase())); } export function UserSearchCombobox() { let fetcher = useFetcher<typeof loader>(); return <div><fetcher.Form method="get" action="/search-users"><input type="text" name="q" onChange={(event) => { fetcher.submit(event.currentTarget.form); }} /></fetcher.Form>{fetcher.data && <ul style={{opacity: fetcher.state === "idle" ? 1 : 0.25}}>{fetcher.data.map((user) => <li key={user.id}>{user.name}</li>)}</ul>}</div>; }
Fetcher state property
The fetcher.state property indicates the current state of the async work. It can be checked to render pending UI. Example: {fetcher.state !== "idle" && <p>Saving...</p>}
Folder route organization rules
When using folder-based routes, the route path is completely defined by the folder name, not by files inside the folder. Only the route.tsx file in the folder is treated as a route module; other files in the folder can be components, utilities, or other assets specific to that route.
Escaping special characters in filenames
Special route convention characters can be escaped with [] brackets to include them literally in the URL. For example, sitemap[.]xml.tsx matches /sitemap.xml, and reports.$id[.pdf].ts matches /reports/123.pdf.
Catch-all route with $.tsx
A file named $.tsx in the routes directory creates a catch-all route that matches any requests that don't match other defined routes. This is commonly used for 404 pages.
Accessing splat route matched path
Splat route matched paths are accessible in loaders via params["*"]. For example, in a files.$.tsx route, access the matched path with params["*"].
Optional segments matching behavior with multiple dynamic params
When an optional dynamic param segment is followed by another dynamic param, optional segments match eagerly. For example, ($lang)._index.tsx will match /american-flag-speedo instead of ($lang).$productId.tsx. In this case, check params.lang in the loader and redirect if needed.
Optional segments with parentheses
Wrapping a route segment in parentheses makes the segment optional. For example, ($lang)._index.tsx creates routes that match both / and /en or /fr. Optional segments match eagerly.
Pathless routes with leading underscore
A leading underscore (e.g., _auth.login.tsx) creates a pathless route. These routes share a layout with other routes in the group without adding path segments to the URL. The leading underscore hides the filename from the URL path.
Trailing underscore opts out of layout nesting
A trailing underscore on the parent segment (e.g., concerts_.mine.tsx) creates a path segment but does not create layout nesting. The route will nest with the root instead of the parent route.
Nested routes best practice: add index routes
When creating nested routes, you typically want to add an index route (e.g., concerts._index.tsx) so that something renders inside the parent's outlet when users visit the parent URL directly.
Multiple dynamic segments
Routes can have multiple dynamic segments. For example, concerts.$city.$date creates a route where both params.date and params.city are accessible in loaders and actions.
Dynamic segments with $ prefix
Dynamic segments are created with the $ prefix. For example, concerts.$city.tsx matches /concerts/salt-lake-city and /concerts/san-diego. The $ prefix is replaced with the actual segment value.
Dot delimiters create nesting hierarchy
Dot delimiters in filenames create both URL paths and layout nesting. If the filename before the . matches another route filename, it automatically becomes a child route.
Dot delimiters create nested URL paths
Adding a . to a route filename creates a / in the URL. For example, concerts.trending.tsx matches /concerts/trending.
_index.tsx is the index route for root
_index.tsx files are index routes. At the root level, app/routes/_index.tsx is the index route for the root route and matches the / URL.
Splat routes match rest of URL including slashes
Splat routes use $ and match the rest of a URL including slashes, unlike dynamic segments which only match a single segment. For example, $.tsx matches /beef/and/cheese, and files.$.tsx matches /files/talks/react-conf_old.pdf.
File extensions for routes
Route files can use .js, .jsx, .ts, or .tsx file extensions.
rootDirectory option for custom route folder
The rootDirectory option on flatRoutes() configures where to look for routes. It defaults to app/routes but is relative to your app directory. For example: flatRoutes({ rootDirectory: "file-routes" }).
ignoredRouteFiles option
The ignoredRouteFiles option on flatRoutes() allows you to specify files that should not be included as routes. For example: flatRoutes({ ignoredRouteFiles: ["home.tsx"] }).
Using folders with route.tsx for code organization
Routes can be folders with a route.tsx file inside. Other files in the folder will not become routes, allowing code organization closer to the routes that use them. For example, app/routes/app._index/route.tsx is equivalent to app/routes/app._index.tsx.
Setting up file-based routes in app/routes.ts
To enable file-based routing, import flatRoutes from @react-router/fs-routes and RouteConfig from @react-router/dev/routes, then export flatRoutes() satisfies RouteConfig in your app/routes.ts file.
@react-router/fs-routes package installation
The @react-router/fs-routes package enables file-convention based route config. Install it with npm i @react-router/fs-routes.