Push V1 app
This commit is contained in:
+138
@@ -0,0 +1,138 @@
|
||||
---
|
||||
title: Actions
|
||||
order: 5
|
||||
---
|
||||
|
||||
# Actions
|
||||
|
||||
[MODES: data]
|
||||
|
||||
## Defining Actions
|
||||
|
||||
Data mutations are done through Route actions defined on the `action` property of a route object. When the action completes, all loader data on the page is revalidated to keep your UI in sync with the data without writing any code to do it.
|
||||
|
||||
```tsx
|
||||
import { createBrowserRouter } from "react-router";
|
||||
import { someApi } from "./api";
|
||||
|
||||
let router = createBrowserRouter([
|
||||
{
|
||||
path: "/projects/:projectId",
|
||||
Component: Project,
|
||||
action: async ({ request }) => {
|
||||
let formData = await request.formData();
|
||||
let title = formData.get("title");
|
||||
let project = await someApi.updateProject({ title });
|
||||
return project;
|
||||
},
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
## Calling Actions
|
||||
|
||||
Actions are called declaratively through `<Form>` and imperatively through `useSubmit` (or `<fetcher.Form>` and `fetcher.submit`) by referencing the route's path and a "post" method.
|
||||
|
||||
### Calling actions with a Form
|
||||
|
||||
```tsx
|
||||
import { Form } from "react-router";
|
||||
|
||||
function SomeComponent() {
|
||||
return (
|
||||
<Form action="/projects/123" method="post">
|
||||
<input type="text" name="title" />
|
||||
<button type="submit">Submit</button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
This will cause a navigation and a new entry will be added to the browser history.
|
||||
|
||||
### Calling actions with useSubmit
|
||||
|
||||
You can submit form data to an action imperatively with `useSubmit`.
|
||||
|
||||
```tsx
|
||||
import { useCallback } from "react";
|
||||
import { useSubmit } from "react-router";
|
||||
import { useFakeTimer } from "fake-lib";
|
||||
|
||||
function useQuizTimer() {
|
||||
let submit = useSubmit();
|
||||
|
||||
let cb = useCallback(() => {
|
||||
submit(
|
||||
{ quizTimedOut: true },
|
||||
{ action: "/end-quiz", method: "post" },
|
||||
);
|
||||
}, []);
|
||||
|
||||
let tenMinutes = 10 * 60 * 1000;
|
||||
useFakeTimer(tenMinutes, cb);
|
||||
}
|
||||
```
|
||||
|
||||
This will cause a navigation and a new entry will be added to the browser history.
|
||||
|
||||
### Calling actions with a fetcher
|
||||
|
||||
Fetchers allow you to submit data to actions (and loaders) without causing a navigation (no new entries in the browser history).
|
||||
|
||||
```tsx
|
||||
import { useFetcher } from "react-router";
|
||||
|
||||
function Task() {
|
||||
let fetcher = useFetcher();
|
||||
let busy = fetcher.state !== "idle";
|
||||
|
||||
return (
|
||||
<fetcher.Form method="post" action="/update-task/123">
|
||||
<input type="text" name="title" />
|
||||
<button type="submit">
|
||||
{busy ? "Saving..." : "Save"}
|
||||
</button>
|
||||
</fetcher.Form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
They also have the imperative `submit` method.
|
||||
|
||||
```tsx
|
||||
fetcher.submit(
|
||||
{ title: "New Title" },
|
||||
{ action: "/update-task/123", method: "post" },
|
||||
);
|
||||
```
|
||||
|
||||
See the [Using Fetchers][fetchers] guide for more information.
|
||||
|
||||
## Accessing Action Data
|
||||
|
||||
Actions can return data available through `useActionData` in the route component or `fetcher.data` when using a fetcher.
|
||||
|
||||
```tsx
|
||||
function Project() {
|
||||
let actionData = useActionData();
|
||||
return (
|
||||
<div>
|
||||
<h1>Project</h1>
|
||||
<Form method="post">
|
||||
<input type="text" name="title" />
|
||||
<button type="submit">Submit</button>
|
||||
</Form>
|
||||
{actionData ? (
|
||||
<p>{actionData.title} updated</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Next: [Navigating](./navigating)
|
||||
|
||||
[fetchers]: ../../how-to/fetchers
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
---
|
||||
title: Custom Framework
|
||||
order: 8
|
||||
---
|
||||
|
||||
# Custom Framework
|
||||
|
||||
[MODES: data]
|
||||
|
||||
## Introduction
|
||||
|
||||
Instead of using `@react-router/dev`, you can integrate React Router's framework features (like loaders, actions, fetchers, etc.) into your own bundler and server abstractions with Data Mode.
|
||||
|
||||
## Client Rendering
|
||||
|
||||
### 1. Create a Router
|
||||
|
||||
The browser runtime API that enables route module APIs (loaders, actions, etc.) is `createBrowserRouter`.
|
||||
|
||||
It takes an array of route objects that support loaders, actions, error boundaries and more. The React Router Vite plugin creates one of these from `routes.ts`, but you can create one manually (or with an abstraction) and use your own bundler.
|
||||
|
||||
```tsx
|
||||
import { createBrowserRouter } from "react-router";
|
||||
|
||||
let router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
Component: Root,
|
||||
children: [
|
||||
{
|
||||
path: "shows/:showId",
|
||||
Component: Show,
|
||||
loader: ({ request, params }) =>
|
||||
fetch(`/api/show/${params.showId}.json`, {
|
||||
signal: request.signal,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
### 2. Render the Router
|
||||
|
||||
To render the router in the browser, use `<RouterProvider>`.
|
||||
|
||||
```tsx
|
||||
import {
|
||||
createBrowserRouter,
|
||||
RouterProvider,
|
||||
} from "react-router";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
createRoot(document.getElementById("root")).render(
|
||||
<RouterProvider router={router} />,
|
||||
);
|
||||
```
|
||||
|
||||
### 3. Lazy Loading
|
||||
|
||||
Routes can take most of their definition lazily with the `lazy` property.
|
||||
|
||||
```tsx
|
||||
createBrowserRouter([
|
||||
{
|
||||
path: "/show/:showId",
|
||||
lazy: {
|
||||
loader: async () =>
|
||||
(await import("./show.loader.js")).loader,
|
||||
action: async () =>
|
||||
(await import("./show.action.js")).action,
|
||||
Component: async () =>
|
||||
(await import("./show.component.js")).Component,
|
||||
},
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
## Server Rendering
|
||||
|
||||
To server render a custom setup, there are a few server APIs available for rendering and data loading.
|
||||
|
||||
This guide simply gives you some ideas about how it works. For deeper understanding, please see the [Custom Framework Example Repo](https://github.com/remix-run/custom-react-router-framework-example)
|
||||
|
||||
### 1. Define Your Routes
|
||||
|
||||
Routes are the same kinds of objects on the server as the client.
|
||||
|
||||
```tsx
|
||||
export default [
|
||||
{
|
||||
path: "/",
|
||||
Component: Root,
|
||||
children: [
|
||||
{
|
||||
path: "shows/:showId",
|
||||
Component: Show,
|
||||
loader: ({ params }) => {
|
||||
return db.loadShow(params.id);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### 2. Create a static handler
|
||||
|
||||
Turn your routes into a request handler with `createStaticHandler`:
|
||||
|
||||
```tsx
|
||||
import { createStaticHandler } from "react-router";
|
||||
import routes from "./some-routes";
|
||||
|
||||
let { query, dataRoutes } = createStaticHandler(routes);
|
||||
```
|
||||
|
||||
### 3. Get Routing Context and Render
|
||||
|
||||
React Router works with web fetch [Requests](https://developer.mozilla.org/en-US/docs/Web/API/Request), so if your server doesn't, you'll need to adapt whatever objects it uses to a web fetch `Request` object.
|
||||
|
||||
This step assumes your server receives `Request` objects.
|
||||
|
||||
```tsx
|
||||
import { renderToString } from "react-dom/server";
|
||||
import {
|
||||
createStaticHandler,
|
||||
createStaticRouter,
|
||||
StaticRouterProvider,
|
||||
} from "react-router";
|
||||
|
||||
import routes from "./some-routes.js";
|
||||
|
||||
let { query, dataRoutes } = createStaticHandler(routes);
|
||||
|
||||
export async function handler(request: Request) {
|
||||
// 1. run actions/loaders to get the routing context with `query`
|
||||
let context = await query(request);
|
||||
|
||||
// If `query` returns a Response, send it raw (a route probably a redirected)
|
||||
if (context instanceof Response) {
|
||||
return context;
|
||||
}
|
||||
|
||||
// 2. Create a static router for SSR
|
||||
let router = createStaticRouter(dataRoutes, context);
|
||||
|
||||
// 3. Render everything with StaticRouterProvider
|
||||
let html = renderToString(
|
||||
<StaticRouterProvider
|
||||
router={router}
|
||||
context={context}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Setup headers from action and loaders from deepest match
|
||||
let leaf = context.matches[context.matches.length - 1];
|
||||
let actionHeaders = context.actionHeaders[leaf.route.id];
|
||||
let loaderHeaders = context.loaderHeaders[leaf.route.id];
|
||||
let headers = new Headers(actionHeaders);
|
||||
if (loaderHeaders) {
|
||||
for (let [key, value] of loaderHeaders.entries()) {
|
||||
headers.append(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
headers.set("Content-Type", "text/html; charset=utf-8");
|
||||
|
||||
// 4. send a response
|
||||
return new Response(`<!DOCTYPE html>${html}`, {
|
||||
status: context.statusCode,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Hydrate in the browser
|
||||
|
||||
Hydration data is embedded onto `window.__staticRouterHydrationData`, use that to initialize your client side router and render a `<RouterProvider>`.
|
||||
|
||||
```tsx
|
||||
import { StrictMode } from "react";
|
||||
import { hydrateRoot } from "react-dom/client";
|
||||
import { RouterProvider } from "react-router/dom";
|
||||
import routes from "./app/routes.js";
|
||||
import { createBrowserRouter } from "react-router";
|
||||
|
||||
let router = createBrowserRouter(routes, {
|
||||
hydrationData: window.__staticRouterHydrationData,
|
||||
});
|
||||
|
||||
hydrateRoot(
|
||||
document,
|
||||
<StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</StrictMode>,
|
||||
);
|
||||
```
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: Data Loading
|
||||
order: 4
|
||||
---
|
||||
|
||||
# Data Loading
|
||||
|
||||
[MODES: data]
|
||||
|
||||
## Providing Data
|
||||
|
||||
Data is provided to route components from route loaders:
|
||||
|
||||
```tsx
|
||||
createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
loader: async () => {
|
||||
// return data from here
|
||||
return { records: await getSomeRecords() };
|
||||
},
|
||||
Component: MyRoute,
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
## Accessing Data
|
||||
|
||||
The data is available in route components with `useLoaderData`.
|
||||
|
||||
```tsx
|
||||
import { useLoaderData } from "react-router";
|
||||
|
||||
function MyRoute() {
|
||||
const { records } = useLoaderData();
|
||||
return <div>{records.length}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
As the user navigates between routes, the loaders are called before the route component is rendered.
|
||||
|
||||
---
|
||||
|
||||
Next: [Actions](./actions)
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: Data Mode
|
||||
order: 3
|
||||
---
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: Installation
|
||||
order: 1
|
||||
---
|
||||
|
||||
# Installation
|
||||
|
||||
[MODES: data]
|
||||
|
||||
## Bootstrap with a Bundler Template
|
||||
|
||||
You can start with a React template from Vite and choose "React", otherwise bootstrap your application however you prefer (Parcel, Webpack, etc).
|
||||
|
||||
```shellscript nonumber
|
||||
npx create-vite@latest
|
||||
```
|
||||
|
||||
## Install React Router
|
||||
|
||||
Next install React Router from npm:
|
||||
|
||||
```shellscript nonumber
|
||||
npm i react-router
|
||||
```
|
||||
|
||||
## Create a Router and Render
|
||||
|
||||
Create a router and pass it to `RouterProvider`:
|
||||
|
||||
```tsx lines=[3-4,6-11,16]
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { createBrowserRouter } from "react-router";
|
||||
import { RouterProvider } from "react-router/dom";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <div>Hello World</div>,
|
||||
},
|
||||
]);
|
||||
|
||||
const root = document.getElementById("root");
|
||||
|
||||
ReactDOM.createRoot(root).render(
|
||||
<RouterProvider router={router} />,
|
||||
);
|
||||
```
|
||||
|
||||
<docs-info>Data Routers should not be held in React state. You should create your router
|
||||
once outside of the React tree and pass it to `<RouterProvider>`. You can use
|
||||
`patchRoutesOnNavigation` to add additional routes programmatically.</docs-info>
|
||||
|
||||
---
|
||||
|
||||
Next: [Routing](./routing)
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
---
|
||||
title: Navigating
|
||||
order: 6
|
||||
---
|
||||
|
||||
# Navigating
|
||||
|
||||
Navigating in Data Mode is the same as Framework Mode, please see the [Navigating](../framework/navigating) guide for more information.
|
||||
|
||||
---
|
||||
|
||||
Next: [Pending UI](./pending-ui)
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
---
|
||||
title: Pending UI
|
||||
order: 7
|
||||
---
|
||||
|
||||
# Pending UI
|
||||
|
||||
Pending UI is the same as Framework Mode, please see the [Pending UI](../framework/pending-ui) guide for more information.
|
||||
|
||||
---
|
||||
|
||||
Next: [Custom Framework](./custom)
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
---
|
||||
title: Route Object
|
||||
order: 3
|
||||
---
|
||||
|
||||
# Route Object
|
||||
|
||||
[MODES: data]
|
||||
|
||||
## Introduction
|
||||
|
||||
The objects passed to `createBrowserRouter` are called Route Objects.
|
||||
|
||||
```tsx lines=[2-5]
|
||||
createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
Component: App,
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
Route modules are the foundation of React Router's data features, they define:
|
||||
|
||||
- data loading
|
||||
- actions
|
||||
- revalidation
|
||||
- error boundaries
|
||||
- and more
|
||||
|
||||
This guide is a quick overview of every route object feature.
|
||||
|
||||
## Component
|
||||
|
||||
The `Component` property in a route object defines the component that will render when the route matches.
|
||||
|
||||
```tsx lines=[4]
|
||||
createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
Component: MyRouteComponent,
|
||||
},
|
||||
]);
|
||||
|
||||
function MyRouteComponent() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Look ma!</h1>
|
||||
<p>
|
||||
I'm still using React Router after like 10 years.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## `middleware`
|
||||
|
||||
Route [middleware][middleware] runs sequentially before and after navigations. This gives you a singular place to do things like logging and authentication. The `next` function continues down the chain, and on the leaf route the `next` function executes the loaders/actions for the navigation.
|
||||
|
||||
```tsx
|
||||
createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
middleware: [loggingMiddleware],
|
||||
loader: rootLoader,
|
||||
Component: Root,
|
||||
children: [{
|
||||
path: 'auth',
|
||||
middleware: [authMiddleware],
|
||||
loader: authLoader,
|
||||
Component: Auth,
|
||||
children: [...]
|
||||
}]
|
||||
},
|
||||
]);
|
||||
|
||||
async function loggingMiddleware({ request }, next) {
|
||||
let url = new URL(request.url);
|
||||
console.log(`Starting navigation: ${url.pathname}${url.search}`);
|
||||
const start = performance.now();
|
||||
await next();
|
||||
const duration = performance.now() - start;
|
||||
console.log(`Navigation completed in ${duration}ms`);
|
||||
}
|
||||
|
||||
const userContext = createContext<User>();
|
||||
|
||||
async function authMiddleware ({ context }) {
|
||||
const userId = getUserId();
|
||||
|
||||
if (!userId) {
|
||||
throw redirect("/login");
|
||||
}
|
||||
|
||||
context.set(userContext, await getUserById(userId));
|
||||
};
|
||||
```
|
||||
|
||||
See also:
|
||||
|
||||
- [Middleware][middleware]
|
||||
|
||||
## `loader`
|
||||
|
||||
Route loaders provide data to route components before they are rendered.
|
||||
|
||||
```tsx
|
||||
import {
|
||||
useLoaderData,
|
||||
createBrowserRouter,
|
||||
} from "react-router";
|
||||
|
||||
createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
loader: loader,
|
||||
Component: MyRoute,
|
||||
},
|
||||
]);
|
||||
|
||||
async function loader({ params }) {
|
||||
return { message: "Hello, world!" };
|
||||
}
|
||||
|
||||
function MyRoute() {
|
||||
let data = useLoaderData();
|
||||
return <h1>{data.message}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
See also:
|
||||
|
||||
- [`loader` params][loader-params]
|
||||
|
||||
## `action`
|
||||
|
||||
Route actions allow server-side data mutations with automatic revalidation of all loader data on the page when called from `<Form>`, `useFetcher`, and `useSubmit`.
|
||||
|
||||
```tsx
|
||||
import {
|
||||
createBrowserRouter,
|
||||
useLoaderData,
|
||||
useActionData,
|
||||
Form,
|
||||
} from "react-router";
|
||||
import { TodoList } from "~/components/TodoList";
|
||||
|
||||
createBrowserRouter([
|
||||
{
|
||||
path: "/items",
|
||||
action: action,
|
||||
loader: loader,
|
||||
Component: Items,
|
||||
},
|
||||
]);
|
||||
|
||||
async function action({ request }) {
|
||||
const data = await request.formData();
|
||||
const todo = await fakeDb.addItem({
|
||||
title: data.get("title"),
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
// this data will be revalidated after the action completes...
|
||||
async function loader() {
|
||||
const items = await fakeDb.getItems();
|
||||
return { items };
|
||||
}
|
||||
|
||||
// ...so that the list here is updated automatically
|
||||
export default function Items() {
|
||||
let data = useLoaderData();
|
||||
return (
|
||||
<div>
|
||||
<List items={data.items} />
|
||||
<Form method="post" navigate={false}>
|
||||
<input type="text" name="title" />
|
||||
<button type="submit">Create Todo</button>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## `shouldRevalidate`
|
||||
|
||||
Loader data is automatically revalidated after certain events like navigations and form submissions.
|
||||
|
||||
This hook enables you to opt in or out of the default revalidation behavior. The default behavior is nuanced to avoid calling loaders unnecessarily.
|
||||
|
||||
A route loader is revalidated when:
|
||||
|
||||
- its own route params change
|
||||
- any change to URL search params
|
||||
- after an action is called and returns a non-error status code
|
||||
|
||||
By defining this function, you opt out of the default behavior completely and can manually control when loader data is revalidated for navigations and form submissions.
|
||||
|
||||
```tsx
|
||||
import type { ShouldRevalidateFunctionArgs } from "react-router";
|
||||
|
||||
function shouldRevalidate(
|
||||
arg: ShouldRevalidateFunctionArgs,
|
||||
) {
|
||||
return true; // false
|
||||
}
|
||||
|
||||
createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
shouldRevalidate: shouldRevalidate,
|
||||
Component: MyRoute,
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
[`ShouldRevalidateFunctionArgs` Reference Documentation ↗](https://api.reactrouter.com/v7/interfaces/react-router.ShouldRevalidateFunctionArgs.html)
|
||||
|
||||
Please note the default behavior is different in [Framework Mode](../modes).
|
||||
|
||||
## `lazy`
|
||||
|
||||
Most properties can be lazily imported to reduce the initial bundle size.
|
||||
|
||||
```tsx
|
||||
createBrowserRouter([
|
||||
{
|
||||
path: "/app",
|
||||
lazy: async () => {
|
||||
// load component and loader in parallel before rendering
|
||||
const [Component, loader] = await Promise.all([
|
||||
import("./app"),
|
||||
import("./app-loader"),
|
||||
]);
|
||||
return { Component, loader };
|
||||
},
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
## `handle`
|
||||
|
||||
Route handle allows apps to add anything to a route match in `useMatches` to create abstractions (like breadcrumbs, etc.).
|
||||
|
||||
```tsx
|
||||
createBrowserRouter([
|
||||
{
|
||||
path: "/app",
|
||||
handle: {
|
||||
breadcrumb: "App",
|
||||
},
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
See also:
|
||||
|
||||
- [`useMatches`][use-matches]
|
||||
|
||||
---
|
||||
|
||||
Next: [Data Loading](./data-loading)
|
||||
|
||||
[loader-params]: https://api.reactrouter.com/v7/interfaces/react-router.LoaderFunctionArgs
|
||||
[middleware]: ../../how-to/middleware
|
||||
[use-matches]: ../../api/hooks/useMatches
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
---
|
||||
title: Routing
|
||||
order: 2
|
||||
---
|
||||
|
||||
# Routing
|
||||
|
||||
[MODES: data]
|
||||
|
||||
## Configuring Routes
|
||||
|
||||
Routes are configured as the first argument to `createBrowserRouter`. At a minimum, you need a path and component:
|
||||
|
||||
```tsx
|
||||
import { createBrowserRouter } from "react-router";
|
||||
|
||||
function Root() {
|
||||
return <h1>Hello world</h1>;
|
||||
}
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{ path: "/", Component: Root },
|
||||
]);
|
||||
```
|
||||
|
||||
Here is a larger sample route config:
|
||||
|
||||
```ts filename=app/routes.ts
|
||||
createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
Component: Root,
|
||||
children: [
|
||||
{ index: true, Component: Home },
|
||||
{ path: "about", Component: About },
|
||||
{
|
||||
path: "auth",
|
||||
Component: AuthLayout,
|
||||
children: [
|
||||
{ path: "login", Component: Login },
|
||||
{ path: "register", Component: Register },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "concerts",
|
||||
children: [
|
||||
{ index: true, Component: ConcertsHome },
|
||||
{ path: ":city", Component: ConcertsCity },
|
||||
{ path: "trending", Component: ConcertsTrending },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
## Route Objects
|
||||
|
||||
Route objects define the behavior of a route beyond just the path and component, like data loading and actions. We'll go into more detail in the [Route Object guide](./route-object), but here's a quick example of a loader.
|
||||
|
||||
```tsx filename=app/team.tsx
|
||||
import {
|
||||
createBrowserRouter,
|
||||
useLoaderData,
|
||||
} from "react-router";
|
||||
|
||||
createBrowserRouter([
|
||||
{
|
||||
path: "/teams/:teamId",
|
||||
loader: async ({ params }) => {
|
||||
let team = await fetchTeam(params.teamId);
|
||||
return { name: team.name };
|
||||
},
|
||||
Component: Team,
|
||||
},
|
||||
]);
|
||||
|
||||
function Team() {
|
||||
let data = useLoaderData();
|
||||
return <h1>{data.name}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
## Nested Routes
|
||||
|
||||
Routes can be nested inside parent routes through `children`.
|
||||
|
||||
```ts filename=app/routes.ts
|
||||
createBrowserRouter([
|
||||
{
|
||||
path: "/dashboard",
|
||||
Component: Dashboard,
|
||||
children: [
|
||||
{ index: true, Component: Home },
|
||||
{ path: "settings", Component: Settings },
|
||||
],
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
The path of the parent is automatically included in the child, so this config creates both `"/dashboard"` and `"/dashboard/settings"` URLs.
|
||||
|
||||
Child routes are rendered through the `<Outlet/>` in the parent route.
|
||||
|
||||
```tsx filename=app/dashboard.tsx
|
||||
import { Outlet } from "react-router";
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
{/* will either be <Home> or <Settings> */}
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Layout Routes
|
||||
|
||||
Omitting the `path` in a route creates new [Nested Routes](#nested-routes) for its children without adding any segments to the URL.
|
||||
|
||||
```tsx lines=[3,16]
|
||||
createBrowserRouter([
|
||||
{
|
||||
// no path on this parent route, just the component
|
||||
Component: MarketingLayout,
|
||||
children: [
|
||||
{ index: true, Component: Home },
|
||||
{ path: "contact", Component: Contact },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
path: "projects",
|
||||
children: [
|
||||
{ index: true, Component: ProjectsHome },
|
||||
{
|
||||
// again, no path, just a component for the layout
|
||||
Component: ProjectLayout,
|
||||
children: [
|
||||
{ path: ":pid", Component: Project },
|
||||
{ path: ":pid/edit", Component: EditProject },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
Note that:
|
||||
|
||||
- `Home` and `Contact` will be rendered into the `MarketingLayout` outlet
|
||||
- `Project` and `EditProject` will be rendered into the `ProjectLayout` outlet while `ProjectsHome` will not.
|
||||
|
||||
## Index Routes
|
||||
|
||||
Index routes are defined by setting `index: true` on a route object without a path.
|
||||
|
||||
```ts
|
||||
{ index: true, Component: Home }
|
||||
```
|
||||
|
||||
Index routes render into their parent's [Outlet][outlet] at their parent's URL (like a default child route).
|
||||
|
||||
```ts lines=[4,5,10,11]
|
||||
import { createBrowserRouter } from "react-router";
|
||||
|
||||
createBrowserRouter([
|
||||
// renders at "/"
|
||||
{ index: true, Component: Home },
|
||||
{
|
||||
Component: Dashboard,
|
||||
path: "/dashboard",
|
||||
children: [
|
||||
// renders at "/dashboard"
|
||||
{ index: true, Component: DashboardHome },
|
||||
{ path: "settings", Component: DashboardSettings },
|
||||
],
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
Note that index routes can't have children.
|
||||
|
||||
## Prefix Route
|
||||
|
||||
A route with just a path and no component creates a group of routes with a path prefix.
|
||||
|
||||
```tsx lines=[3]
|
||||
createBrowserRouter([
|
||||
{
|
||||
// no component, just a path
|
||||
path: "/projects",
|
||||
children: [
|
||||
{ index: true, Component: ProjectsHome },
|
||||
{ path: ":pid", Component: Project },
|
||||
{ path: ":pid/edit", Component: EditProject },
|
||||
],
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
This creates the routes `/projects`, `/projects/:pid`, and `/projects/:pid/edit` without introducing a layout component.
|
||||
|
||||
## Dynamic Segments
|
||||
|
||||
If a path segment starts with `:` then it becomes a "dynamic segment". When the route matches the URL, the dynamic segment will be parsed from the URL and provided as `params` to other router APIs.
|
||||
|
||||
```ts lines=[2]
|
||||
{
|
||||
path: "teams/:teamId",
|
||||
loader: async ({ params }) => {
|
||||
// params are available in loaders/actions
|
||||
let team = await fetchTeam(params.teamId);
|
||||
return { name: team.name };
|
||||
},
|
||||
Component: Team,
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
import { useParams } from "react-router";
|
||||
|
||||
function Team() {
|
||||
// params are available in components through useParams
|
||||
let params = useParams();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
You can have multiple dynamic segments in one route path:
|
||||
|
||||
```ts
|
||||
{
|
||||
path: "c/:categoryId/p/:productId";
|
||||
}
|
||||
```
|
||||
|
||||
## Optional Segments
|
||||
|
||||
You can make a route segment optional by adding a `?` to the end of the segment.
|
||||
|
||||
```ts
|
||||
{
|
||||
path: ":lang?/categories";
|
||||
}
|
||||
```
|
||||
|
||||
You can have optional static segments, too:
|
||||
|
||||
```ts
|
||||
{
|
||||
path: "users/:userId/edit?";
|
||||
}
|
||||
```
|
||||
|
||||
## Splats
|
||||
|
||||
Also known as "catchall" and "star" segments. If a route path pattern ends with `/*` then it will match any characters following the `/`, including other `/` characters.
|
||||
|
||||
```ts
|
||||
{
|
||||
path: "files/*";
|
||||
loader: async ({ params }) => {
|
||||
params["*"]; // will contain the remaining URL after files/
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
You can destructure the `*`, you just have to assign it a new name. A common name is `splat`:
|
||||
|
||||
```tsx
|
||||
const { "*": splat } = params;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Next: [Route Object](./route-object)
|
||||
|
||||
[outlet]: https://api.reactrouter.com/v7/functions/react-router.Outlet.html
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Testing
|
||||
order: 9
|
||||
---
|
||||
|
||||
# Testing
|
||||
|
||||
You can use `createRoutesStub` in data and framework modes. Please refer to the [Testing Guide](../framework/testing).
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: Declarative Mode
|
||||
order: 4
|
||||
---
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: Installation
|
||||
order: 1
|
||||
---
|
||||
|
||||
# Installation
|
||||
|
||||
[MODES: declarative]
|
||||
|
||||
## Introduction
|
||||
|
||||
You can start with a React template from Vite and choose "React", otherwise bootstrap your application however you prefer.
|
||||
|
||||
```shellscript nonumber
|
||||
npx create-vite@latest
|
||||
```
|
||||
|
||||
Next install React Router from npm:
|
||||
|
||||
```shellscript nonumber
|
||||
npm i react-router
|
||||
```
|
||||
|
||||
Finally, render a `<BrowserRouter>` around your application:
|
||||
|
||||
```tsx lines=[3,9-11]
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { BrowserRouter } from "react-router";
|
||||
import App from "./app";
|
||||
|
||||
const root = document.getElementById("root");
|
||||
|
||||
ReactDOM.createRoot(root).render(
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>,
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Next: [Routing](./routing)
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
---
|
||||
title: Navigating
|
||||
order: 3
|
||||
---
|
||||
|
||||
# Navigating
|
||||
|
||||
[MODES: declarative]
|
||||
|
||||
## Introduction
|
||||
|
||||
Users navigate your application with `<Link>`, `<NavLink>`, and `useNavigate`.
|
||||
|
||||
## NavLink
|
||||
|
||||
This component is for navigation links that need to render an active state.
|
||||
|
||||
```tsx
|
||||
import { NavLink } from "react-router";
|
||||
|
||||
export function MyAppNav() {
|
||||
return (
|
||||
<nav>
|
||||
<NavLink to="/" end>
|
||||
Home
|
||||
</NavLink>
|
||||
<NavLink to="/trending" end>
|
||||
Trending Concerts
|
||||
</NavLink>
|
||||
<NavLink to="/concerts">All Concerts</NavLink>
|
||||
<NavLink to="/account">Account</NavLink>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Whenever a `NavLink` is active, it will automatically have an `.active` class name for easy styling with CSS:
|
||||
|
||||
```css
|
||||
a.active {
|
||||
color: red;
|
||||
}
|
||||
```
|
||||
|
||||
It also has callback props on `className`, `style`, and `children` with the active state for inline styling or conditional rendering:
|
||||
|
||||
```tsx
|
||||
// className
|
||||
<NavLink
|
||||
to="/messages"
|
||||
className={({ isActive }) =>
|
||||
isActive ? "text-red-500" : "text-black"
|
||||
}
|
||||
>
|
||||
Messages
|
||||
</NavLink>
|
||||
```
|
||||
|
||||
```tsx
|
||||
// style
|
||||
<NavLink
|
||||
to="/messages"
|
||||
style={({ isActive }) => ({
|
||||
color: isActive ? "red" : "black",
|
||||
})}
|
||||
>
|
||||
Messages
|
||||
</NavLink>
|
||||
```
|
||||
|
||||
```tsx
|
||||
// children
|
||||
<NavLink to="/message">
|
||||
{({ isActive }) => (
|
||||
<span className={isActive ? "active" : ""}>
|
||||
{isActive ? "👉" : ""} Tasks
|
||||
</span>
|
||||
)}
|
||||
</NavLink>
|
||||
```
|
||||
|
||||
## Link
|
||||
|
||||
Use `<Link>` when the link doesn't need active styling:
|
||||
|
||||
```tsx
|
||||
import { Link } from "react-router";
|
||||
|
||||
export function LoggedOutMessage() {
|
||||
return (
|
||||
<p>
|
||||
You've been logged out.{" "}
|
||||
<Link to="/login">Login again</Link>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## useNavigate
|
||||
|
||||
This hook allows the programmer to navigate the user to a new page without the user interacting.
|
||||
|
||||
For normal navigation, it's best to use `Link` or `NavLink`. They provide a better default user experience like keyboard events, accessibility labeling, "open in new window", right click context menus, etc.
|
||||
|
||||
Reserve usage of `useNavigate` to situations where the user is _not_ interacting but you need to navigate, for example:
|
||||
|
||||
- After a form submission completes
|
||||
- Logging them out after inactivity
|
||||
- Timed UIs like quizzes, etc.
|
||||
|
||||
```tsx
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
export function LoginPage() {
|
||||
let navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyHeader />
|
||||
<MyLoginForm
|
||||
onSuccess={() => {
|
||||
navigate("/dashboard");
|
||||
}}
|
||||
/>
|
||||
<MyFooter />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Next: [Url values](./url-values)
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
---
|
||||
title: Routing
|
||||
order: 2
|
||||
---
|
||||
|
||||
# Routing
|
||||
|
||||
[MODES: declarative]
|
||||
|
||||
## Configuring Routes
|
||||
|
||||
Routes are configured by rendering `<Routes>` and `<Route>` that couple URL segments to UI elements.
|
||||
|
||||
```tsx
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { BrowserRouter, Routes, Route } from "react-router";
|
||||
import App from "./app";
|
||||
|
||||
const root = document.getElementById("root");
|
||||
|
||||
ReactDOM.createRoot(root).render(
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<App />} />
|
||||
</Routes>
|
||||
</BrowserRouter>,
|
||||
);
|
||||
```
|
||||
|
||||
Here's a larger sample config:
|
||||
|
||||
```tsx
|
||||
<Routes>
|
||||
<Route index element={<Home />} />
|
||||
<Route path="about" element={<About />} />
|
||||
|
||||
<Route element={<AuthLayout />}>
|
||||
<Route path="login" element={<Login />} />
|
||||
<Route path="register" element={<Register />} />
|
||||
</Route>
|
||||
|
||||
<Route path="concerts">
|
||||
<Route index element={<ConcertsHome />} />
|
||||
<Route path=":city" element={<City />} />
|
||||
<Route path="trending" element={<Trending />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
## Nested Routes
|
||||
|
||||
Routes can be nested inside parent routes.
|
||||
|
||||
```tsx
|
||||
<Routes>
|
||||
<Route path="dashboard" element={<Dashboard />}>
|
||||
<Route index element={<Home />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
The path of the parent is automatically included in the child, so this config creates both `"/dashboard"` and `"/dashboard/settings"` URLs.
|
||||
|
||||
Child routes are rendered through the `<Outlet/>` in the parent route.
|
||||
|
||||
```tsx filename=app/dashboard.tsx
|
||||
import { Outlet } from "react-router";
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
{/* will either be <Home/> or <Settings/> */}
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Layout Routes
|
||||
|
||||
Routes _without_ a `path` create new nesting for their children, but they don't add any segments to the URL.
|
||||
|
||||
```tsx lines=[2,9]
|
||||
<Routes>
|
||||
<Route element={<MarketingLayout />}>
|
||||
<Route index element={<MarketingHome />} />
|
||||
<Route path="contact" element={<Contact />} />
|
||||
</Route>
|
||||
|
||||
<Route path="projects">
|
||||
<Route index element={<ProjectsHome />} />
|
||||
<Route element={<ProjectsLayout />}>
|
||||
<Route path=":pid" element={<Project />} />
|
||||
<Route path=":pid/edit" element={<EditProject />} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
## Index Routes
|
||||
|
||||
Index routes render into their parent's `<Outlet/>` at their parent's URL (like a default child route). They are configured with the `index` prop:
|
||||
|
||||
```tsx lines=[4,8]
|
||||
<Routes>
|
||||
<Route path="/" element={<Root />}>
|
||||
{/* renders into the outlet in <Root> at "/" */}
|
||||
<Route index element={<Home />} />
|
||||
|
||||
<Route path="dashboard" element={<Dashboard />}>
|
||||
{/* renders into the outlet in <Dashboard> at "/dashboard" */}
|
||||
<Route index element={<DashboardHome />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
Note that index routes can't have children. If you're expecting that behavior, you probably want a [layout route](#layout-routes).
|
||||
|
||||
## Route Prefixes
|
||||
|
||||
A `<Route path>` _without_ an `element` prop adds a path prefix to its child routes, without introducing a parent layout.
|
||||
|
||||
```tsx filename=app/routes.ts lines=[1]
|
||||
<Route path="projects">
|
||||
<Route index element={<ProjectsHome />} />
|
||||
<Route element={<ProjectsLayout />}>
|
||||
<Route path=":pid" element={<Project />} />
|
||||
<Route path=":pid/edit" element={<EditProject />} />
|
||||
</Route>
|
||||
</Route>
|
||||
```
|
||||
|
||||
## Dynamic Segments
|
||||
|
||||
If a path segment starts with `:` then it becomes a "dynamic segment". When the route matches the URL, the dynamic segment will be parsed from the URL and provided as `params` to other router APIs like `useParams`.
|
||||
|
||||
```tsx
|
||||
<Route path="teams/:teamId" element={<Team />} />
|
||||
```
|
||||
|
||||
```tsx filename=app/team.tsx
|
||||
import { useParams } from "react-router";
|
||||
|
||||
export default function Team() {
|
||||
let params = useParams();
|
||||
// params.teamId
|
||||
}
|
||||
```
|
||||
|
||||
You can have multiple dynamic segments in one route path:
|
||||
|
||||
```tsx
|
||||
<Route
|
||||
path="/c/:categoryId/p/:productId"
|
||||
element={<Product />}
|
||||
/>
|
||||
```
|
||||
|
||||
```tsx filename=app/category-product.tsx
|
||||
import { useParams } from "react-router";
|
||||
|
||||
export default function CategoryProduct() {
|
||||
let { categoryId, productId } = useParams();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
You should ensure that all dynamic segments in a given path are unique. Otherwise, as the `params` object is populated - latter dynamic segment values will override earlier values.
|
||||
|
||||
## Optional Segments
|
||||
|
||||
You can make a route segment optional by adding a `?` to the end of the segment.
|
||||
|
||||
```tsx
|
||||
<Route path=":lang?/categories" element={<Categories />} />
|
||||
```
|
||||
|
||||
You can have optional static segments, too:
|
||||
|
||||
```tsx
|
||||
<Route path="users/:userId/edit?" element={<User />} />
|
||||
```
|
||||
|
||||
## Splats
|
||||
|
||||
Also known as "catchall" and "star" segments. If a route path pattern ends with `/*` then it will match any characters following the `/`, including other `/` characters.
|
||||
|
||||
```tsx
|
||||
<Route path="files/*" element={<File />} />
|
||||
```
|
||||
|
||||
```tsx
|
||||
let params = useParams();
|
||||
// params["*"] will contain the remaining URL after files/
|
||||
let filePath = params["*"];
|
||||
```
|
||||
|
||||
You can destructure the `*`, you just have to assign it a new name. A common name is `splat`:
|
||||
|
||||
```tsx
|
||||
let { "*": splat } = useParams();
|
||||
```
|
||||
|
||||
## Linking
|
||||
|
||||
Link to routes from your UI with `Link` and `NavLink`
|
||||
|
||||
```tsx
|
||||
import { NavLink, Link } from "react-router";
|
||||
|
||||
function Header() {
|
||||
return (
|
||||
<nav>
|
||||
{/* NavLink makes it easy to show active states */}
|
||||
<NavLink
|
||||
to="/"
|
||||
className={({ isActive }) =>
|
||||
isActive ? "active" : ""
|
||||
}
|
||||
>
|
||||
Home
|
||||
</NavLink>
|
||||
|
||||
<Link to="/concerts/salt-lake-city">Concerts</Link>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Next: [Navigating](./navigating)
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: URL Values
|
||||
---
|
||||
|
||||
# URL Values
|
||||
|
||||
[MODES: declarative]
|
||||
|
||||
## Route Params
|
||||
|
||||
Route params are the parsed values from a dynamic segment.
|
||||
|
||||
```tsx
|
||||
<Route path="/concerts/:city" element={<City />} />
|
||||
```
|
||||
|
||||
In this case, `:city` is the dynamic segment. The parsed value for that city will be available from `useParams`
|
||||
|
||||
```tsx
|
||||
import { useParams } from "react-router";
|
||||
|
||||
function City() {
|
||||
let { city } = useParams();
|
||||
let data = useFakeDataLibrary(`/api/v2/cities/${city}`);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## URL Search Params
|
||||
|
||||
Search params are the values after a `?` in the URL. They are accessible from `useSearchParams`, which returns an instance of [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams)
|
||||
|
||||
```tsx
|
||||
function SearchResults() {
|
||||
let [searchParams] = useSearchParams();
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
You searched for <i>{searchParams.get("q")}</i>
|
||||
</p>
|
||||
<FakeSearchResults />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Location Object
|
||||
|
||||
React Router creates a custom `location` object with some useful information on it accessible with `useLocation`.
|
||||
|
||||
```tsx
|
||||
function useAnalytics() {
|
||||
let location = useLocation();
|
||||
useEffect(() => {
|
||||
sendFakeAnalytics(location.pathname);
|
||||
}, [location]);
|
||||
}
|
||||
|
||||
function useScrollRestoration() {
|
||||
let location = useLocation();
|
||||
useEffect(() => {
|
||||
fakeRestoreScroll(location.key);
|
||||
}, [location]);
|
||||
}
|
||||
```
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
---
|
||||
title: Actions
|
||||
order: 6
|
||||
---
|
||||
|
||||
# Actions
|
||||
|
||||
[MODES: framework]
|
||||
|
||||
## Introduction
|
||||
|
||||
Data mutations are done through Route actions. When the action completes, all loader data on the page is revalidated to keep your UI in sync with the data without writing any code to do it.
|
||||
|
||||
Route actions defined with `action` are only called on the server while actions defined with `clientAction` are run in the browser.
|
||||
|
||||
## Client Actions
|
||||
|
||||
Client actions only run in the browser and take priority over a server action when both are defined.
|
||||
|
||||
```tsx filename=app/project.tsx
|
||||
// route('/projects/:projectId', './project.tsx')
|
||||
import type { Route } from "./+types/project";
|
||||
import { Form } from "react-router";
|
||||
import { someApi } from "./api";
|
||||
|
||||
export async function clientAction({
|
||||
request,
|
||||
}: Route.ClientActionArgs) {
|
||||
let formData = await request.formData();
|
||||
let title = formData.get("title");
|
||||
let project = await someApi.updateProject({ title });
|
||||
return project;
|
||||
}
|
||||
|
||||
export default function Project({
|
||||
actionData,
|
||||
}: Route.ComponentProps) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Project</h1>
|
||||
<Form method="post">
|
||||
<input type="text" name="title" />
|
||||
<button type="submit">Submit</button>
|
||||
</Form>
|
||||
{actionData ? (
|
||||
<p>{actionData.title} updated</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Server Actions
|
||||
|
||||
Server actions only run on the server and are removed from client bundles.
|
||||
|
||||
```tsx filename=app/project.tsx
|
||||
// route('/projects/:projectId', './project.tsx')
|
||||
import type { Route } from "./+types/project";
|
||||
import { Form } from "react-router";
|
||||
import { fakeDb } from "../db";
|
||||
|
||||
export async function action({
|
||||
request,
|
||||
}: Route.ActionArgs) {
|
||||
let formData = await request.formData();
|
||||
let title = formData.get("title");
|
||||
let project = await fakeDb.updateProject({ title });
|
||||
return project;
|
||||
}
|
||||
|
||||
export default function Project({
|
||||
actionData,
|
||||
}: Route.ComponentProps) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Project</h1>
|
||||
<Form method="post">
|
||||
<input type="text" name="title" />
|
||||
<button type="submit">Submit</button>
|
||||
</Form>
|
||||
{actionData ? (
|
||||
<p>{actionData.title} updated</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Calling Actions
|
||||
|
||||
Actions are called declaratively through `<Form>` and imperatively through `useSubmit` (or `<fetcher.Form>` and `fetcher.submit`) by referencing the route's path and a "post" method.
|
||||
|
||||
### Calling actions with a Form
|
||||
|
||||
```tsx
|
||||
import { Form } from "react-router";
|
||||
|
||||
function SomeComponent() {
|
||||
return (
|
||||
<Form action="/projects/123" method="post">
|
||||
<input type="text" name="title" />
|
||||
<button type="submit">Submit</button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
This will cause a navigation and a new entry will be added to the browser history.
|
||||
|
||||
### Calling actions with useSubmit
|
||||
|
||||
You can submit form data to an action imperatively with `useSubmit`.
|
||||
|
||||
```tsx
|
||||
import { useCallback } from "react";
|
||||
import { useSubmit } from "react-router";
|
||||
import { useFakeTimer } from "fake-lib";
|
||||
|
||||
function useQuizTimer() {
|
||||
let submit = useSubmit();
|
||||
|
||||
let cb = useCallback(() => {
|
||||
submit(
|
||||
{ quizTimedOut: true },
|
||||
{ action: "/end-quiz", method: "post" },
|
||||
);
|
||||
}, []);
|
||||
|
||||
let tenMinutes = 10 * 60 * 1000;
|
||||
useFakeTimer(tenMinutes, cb);
|
||||
}
|
||||
```
|
||||
|
||||
This will cause a navigation and a new entry will be added to the browser history.
|
||||
|
||||
### Calling actions with a fetcher
|
||||
|
||||
Fetchers allow you to submit data to actions (and loaders) without causing a navigation (no new entries in the browser history).
|
||||
|
||||
```tsx
|
||||
import { useFetcher } from "react-router";
|
||||
|
||||
function Task() {
|
||||
let fetcher = useFetcher();
|
||||
let busy = fetcher.state !== "idle";
|
||||
|
||||
return (
|
||||
<fetcher.Form method="post" action="/update-task/123">
|
||||
<input type="text" name="title" />
|
||||
<button type="submit">
|
||||
{busy ? "Saving..." : "Save"}
|
||||
</button>
|
||||
</fetcher.Form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
They also have the imperative `submit` method.
|
||||
|
||||
```tsx
|
||||
fetcher.submit(
|
||||
{ title: "New Title" },
|
||||
{ action: "/update-task/123", method: "post" },
|
||||
);
|
||||
```
|
||||
|
||||
See the [Using Fetchers][fetchers] guide for more information.
|
||||
|
||||
---
|
||||
|
||||
Next: [Navigating](./navigating)
|
||||
|
||||
[fetchers]: ../../how-to/fetchers
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
---
|
||||
title: Data Loading
|
||||
order: 5
|
||||
---
|
||||
|
||||
# Data Loading
|
||||
|
||||
[MODES: framework]
|
||||
|
||||
## Introduction
|
||||
|
||||
Data is provided to the route component from `loader` and `clientLoader`.
|
||||
|
||||
Loader data is automatically serialized from loaders and deserialized in components. In addition to primitive values like strings and numbers, loaders can return promises, maps, sets, dates and more.
|
||||
|
||||
The type for the `loaderData` prop is [automatically generated][type-safety].
|
||||
|
||||
<docs-info>We try to support the same set of [serializable types][serializable-types] that React permits server components to pass as props to client components. This future proofs your application for any eventual migration to [RSC][rsc].</docs-info>
|
||||
|
||||
## Client Data Loading
|
||||
|
||||
`clientLoader` is used to fetch data on the client. This is useful for pages or full projects that you'd prefer to fetch data from the browser only.
|
||||
|
||||
```tsx filename=app/product.tsx
|
||||
// route("products/:pid", "./product.tsx");
|
||||
import type { Route } from "./+types/product";
|
||||
|
||||
export async function clientLoader({
|
||||
params,
|
||||
}: Route.ClientLoaderArgs) {
|
||||
const res = await fetch(`/api/products/${params.pid}`);
|
||||
const product = await res.json();
|
||||
return product;
|
||||
}
|
||||
|
||||
// HydrateFallback is rendered while the client loader is running
|
||||
export function HydrateFallback() {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
export default function Product({
|
||||
loaderData,
|
||||
}: Route.ComponentProps) {
|
||||
const { name, description } = loaderData;
|
||||
return (
|
||||
<div>
|
||||
<h1>{name}</h1>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Server Data Loading
|
||||
|
||||
When server rendering, `loader` is used for both initial page loads and client navigations. Client navigations call the loader through an automatic `fetch` by React Router from the browser to your server.
|
||||
|
||||
```tsx filename=app/product.tsx
|
||||
// route("products/:pid", "./product.tsx");
|
||||
import type { Route } from "./+types/product";
|
||||
import { fakeDb } from "../db";
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
const product = await fakeDb.getProduct(params.pid);
|
||||
return product;
|
||||
}
|
||||
|
||||
export default function Product({
|
||||
loaderData,
|
||||
}: Route.ComponentProps) {
|
||||
const { name, description } = loaderData;
|
||||
return (
|
||||
<div>
|
||||
<h1>{name}</h1>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Note that the `loader` function is removed from client bundles so you can use server only APIs without worrying about them being included in the browser.
|
||||
|
||||
## Static Data Loading
|
||||
|
||||
When pre-rendering, loaders are used to fetch data during the production build.
|
||||
|
||||
```tsx filename=app/product.tsx
|
||||
// route("products/:pid", "./product.tsx");
|
||||
import type { Route } from "./+types/product";
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
let product = await getProductFromCSVFile(params.pid);
|
||||
return product;
|
||||
}
|
||||
|
||||
export default function Product({
|
||||
loaderData,
|
||||
}: Route.ComponentProps) {
|
||||
const { name, description } = loaderData;
|
||||
return (
|
||||
<div>
|
||||
<h1>{name}</h1>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
The URLs to pre-render are specified in `react-router.config.ts`:
|
||||
|
||||
```ts filename=react-router.config.ts
|
||||
import type { Config } from "@react-router/dev/config";
|
||||
|
||||
export default {
|
||||
async prerender() {
|
||||
let products = await readProductsFromCSVFile();
|
||||
return products.map(
|
||||
(product) => `/products/${product.id}`,
|
||||
);
|
||||
},
|
||||
} satisfies Config;
|
||||
```
|
||||
|
||||
Note that when server rendering, any URLs that aren't pre-rendered will be server rendered as usual, allowing you to pre-render some data at a single route while still server rendering the rest.
|
||||
|
||||
## Using Both Loaders
|
||||
|
||||
`loader` and `clientLoader` can be used together. The `loader` will be used on the server for initial SSR (or pre-rendering) and the `clientLoader` will be used on subsequent client-side navigations.
|
||||
|
||||
```tsx filename=app/product.tsx
|
||||
// route("products/:pid", "./product.tsx");
|
||||
import type { Route } from "./+types/product";
|
||||
import { fakeDb } from "../db";
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
return fakeDb.getProduct(params.pid);
|
||||
}
|
||||
|
||||
export async function clientLoader({
|
||||
serverLoader,
|
||||
params,
|
||||
}: Route.ClientLoaderArgs) {
|
||||
const res = await fetch(`/api/products/${params.pid}`);
|
||||
const serverData = await serverLoader();
|
||||
return { ...serverData, ...(await res.json()) };
|
||||
}
|
||||
|
||||
export default function Product({
|
||||
loaderData,
|
||||
}: Route.ComponentProps) {
|
||||
const { name, description } = loaderData;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{name}</h1>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
You can also force the client loader to run during hydration and before the page renders by setting the `hydrate` property on the function. In this situation you will want to render a `HydrateFallback` component to show a fallback UI while the client loader runs.
|
||||
|
||||
```tsx filename=app/product.tsx
|
||||
export async function loader() {
|
||||
/* ... */
|
||||
}
|
||||
|
||||
export async function clientLoader() {
|
||||
/* ... */
|
||||
}
|
||||
|
||||
// force the client loader to run during hydration
|
||||
clientLoader.hydrate = true as const; // `as const` for type inference
|
||||
|
||||
export function HydrateFallback() {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
export default function Product() {
|
||||
/* ... */
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Next: [Actions][actions]
|
||||
|
||||
See also:
|
||||
|
||||
- [Streaming with Suspense][streaming]
|
||||
- [Client Data][client-data]
|
||||
- [Using Fetchers][fetchers]
|
||||
|
||||
[type-safety]: ../../explanation/type-safety
|
||||
[serializable-types]: https://react.dev/reference/rsc/use-client#serializable-types
|
||||
[rsc]: ../../how-to/react-server-components
|
||||
[actions]: ./actions
|
||||
[streaming]: ../../how-to/suspense
|
||||
[client-data]: ../../how-to/client-data
|
||||
[fetchers]: ../../how-to/fetchers#loading-data
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: Deploying
|
||||
order: 10
|
||||
---
|
||||
|
||||
# Deploying
|
||||
|
||||
[MODES: framework]
|
||||
|
||||
## Introduction
|
||||
|
||||
React Router can be deployed two ways:
|
||||
|
||||
- Fullstack Hosting
|
||||
- Static Hosting
|
||||
|
||||
The official [React Router templates](https://github.com/remix-run/react-router-templates) can help you bootstrap an application or be used as a reference for your own application.
|
||||
|
||||
When deploying to static hosting, you can deploy React Router the same as any other single page application with React.
|
||||
|
||||
## Templates
|
||||
|
||||
After running the `create-react-router` command, make sure to follow the instructions in the README.
|
||||
|
||||
### Node.js with Docker
|
||||
|
||||
```
|
||||
npx create-react-router@latest --template remix-run/react-router-templates/default
|
||||
```
|
||||
|
||||
- Server Rendering
|
||||
- Tailwind CSS
|
||||
|
||||
The containerized application can be deployed to any platform that supports Docker, including:
|
||||
|
||||
- AWS ECS
|
||||
- Google Cloud Run
|
||||
- Azure Container Apps
|
||||
- Digital Ocean App Platform
|
||||
- Fly.io
|
||||
- Railway
|
||||
|
||||
### Node with Docker (Custom Server)
|
||||
|
||||
```
|
||||
npx create-react-router@latest --template remix-run/react-router-templates/node-custom-server
|
||||
```
|
||||
|
||||
- Server Rendering
|
||||
- Tailwind CSS
|
||||
- Custom express server for more control
|
||||
|
||||
The containerized application can be deployed to any platform that supports Docker, including:
|
||||
|
||||
- AWS ECS
|
||||
- Google Cloud Run
|
||||
- Azure Container Apps
|
||||
- Digital Ocean App Platform
|
||||
- Fly.io
|
||||
- Railway
|
||||
|
||||
### Node with Docker and Postgres
|
||||
|
||||
```
|
||||
npx create-react-router@latest --template remix-run/react-router-templates/node-postgres
|
||||
```
|
||||
|
||||
- Server Rendering
|
||||
- Postgres Database with Drizzle
|
||||
- Tailwind CSS
|
||||
- Custom express server for more control
|
||||
|
||||
The containerized application can be deployed to any platform that supports Docker, including:
|
||||
|
||||
- AWS ECS
|
||||
- Google Cloud Run
|
||||
- Azure Container Apps
|
||||
- Digital Ocean App Platform
|
||||
- Fly.io
|
||||
- Railway
|
||||
|
||||
### Vercel
|
||||
|
||||
Vercel maintains their own template for React Router. Checkout the [Vercel Guide](https://vercel.com/templates/react-router/react-router-boilerplate) for more information.
|
||||
|
||||
### Cloudflare Workers
|
||||
|
||||
Cloudflare maintains their own template for React Router. Checkout the [Cloudflare Guide](https://developers.cloudflare.com/workers/framework-guides/web-apps/react-router/) for more information.
|
||||
|
||||
### Netlify
|
||||
|
||||
Netlify maintains their own template for React Router. Checkout the [Netlify Guide](https://docs.netlify.com/build/frameworks/framework-setup-guides/react-router/) for more information.
|
||||
|
||||
### EdgeOne Pages
|
||||
|
||||
EdgeOne Pages maintains their own template for React Router. Checkout the [EdgeOne Pages Guide](https://pages.edgeone.ai/document/framework-react-router) for more information.
|
||||
|
||||
### DeployHQ
|
||||
|
||||
DeployHQ maintains their own guide for deploying React Router v7 to your own server. Checkout the [DeployHQ Guide](https://www.deployhq.com/guides/deploy-react-router-from-github) for more information.
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: Framework Mode
|
||||
order: 2
|
||||
---
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: Installation
|
||||
order: 1
|
||||
---
|
||||
|
||||
# Installation
|
||||
|
||||
[MODES: framework]
|
||||
|
||||
## Introduction
|
||||
|
||||
Most projects start with a template. Let's use a basic template maintained by React Router:
|
||||
|
||||
```shellscript nonumber
|
||||
npx create-react-router@latest my-react-router-app
|
||||
```
|
||||
|
||||
Now change into the new directory and start the app
|
||||
|
||||
```shellscript nonumber
|
||||
cd my-react-router-app
|
||||
npm i
|
||||
npm run dev
|
||||
```
|
||||
|
||||
You can now open your browser to `http://localhost:5173`
|
||||
|
||||
You can [view the template on GitHub][default-template] to see how to manually set up your project.
|
||||
|
||||
We also have a number of [ready to deploy templates][react-router-templates] available for you to get started with:
|
||||
|
||||
```shellscript nonumber
|
||||
npx create-react-router@latest --template remix-run/react-router-templates/<template-name>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Next: [Routing](./routing)
|
||||
|
||||
[default-template]: https://github.com/remix-run/react-router-templates/tree/main/default
|
||||
[react-router-templates]: https://github.com/remix-run/react-router-templates
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
---
|
||||
title: Navigating
|
||||
order: 6
|
||||
---
|
||||
|
||||
# Navigating
|
||||
|
||||
[MODES: framework]
|
||||
|
||||
## Introduction
|
||||
|
||||
Users navigate your application with `<Link>`, `<NavLink>`, `<Form>`, `redirect`, and `useNavigate`.
|
||||
|
||||
## NavLink
|
||||
|
||||
This component is for navigation links that need to render active and pending states.
|
||||
|
||||
```tsx
|
||||
import { NavLink } from "react-router";
|
||||
|
||||
export function MyAppNav() {
|
||||
return (
|
||||
<nav>
|
||||
<NavLink to="/" end>
|
||||
Home
|
||||
</NavLink>
|
||||
<NavLink to="/trending" end>
|
||||
Trending Concerts
|
||||
</NavLink>
|
||||
<NavLink to="/concerts">All Concerts</NavLink>
|
||||
<NavLink to="/account">Account</NavLink>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
`NavLink` renders default class names for different states for easy styling with CSS:
|
||||
|
||||
```css
|
||||
a.active {
|
||||
color: red;
|
||||
}
|
||||
|
||||
a.pending {
|
||||
animate: pulse 1s infinite;
|
||||
}
|
||||
|
||||
a.transitioning {
|
||||
/* css transition is running */
|
||||
}
|
||||
```
|
||||
|
||||
It also has callback props on `className`, `style`, and `children` with the states for inline styling or conditional rendering:
|
||||
|
||||
```tsx
|
||||
// className
|
||||
<NavLink
|
||||
to="/messages"
|
||||
className={({ isActive, isPending, isTransitioning }) =>
|
||||
[
|
||||
isPending ? "pending" : "",
|
||||
isActive ? "active" : "",
|
||||
isTransitioning ? "transitioning" : "",
|
||||
].join(" ")
|
||||
}
|
||||
>
|
||||
Messages
|
||||
</NavLink>
|
||||
```
|
||||
|
||||
```tsx
|
||||
// style
|
||||
<NavLink
|
||||
to="/messages"
|
||||
style={({ isActive, isPending, isTransitioning }) => {
|
||||
return {
|
||||
fontWeight: isActive ? "bold" : "",
|
||||
color: isPending ? "red" : "black",
|
||||
viewTransitionName: isTransitioning ? "slide" : "",
|
||||
};
|
||||
}}
|
||||
>
|
||||
Messages
|
||||
</NavLink>
|
||||
```
|
||||
|
||||
```tsx
|
||||
// children
|
||||
<NavLink to="/tasks">
|
||||
{({ isActive, isPending, isTransitioning }) => (
|
||||
<span className={isActive ? "active" : ""}>Tasks</span>
|
||||
)}
|
||||
</NavLink>
|
||||
```
|
||||
|
||||
## Link
|
||||
|
||||
Use `<Link>` when the link doesn't need active styling:
|
||||
|
||||
```tsx
|
||||
import { Link } from "react-router";
|
||||
|
||||
export function LoggedOutMessage() {
|
||||
return (
|
||||
<p>
|
||||
You've been logged out.{" "}
|
||||
<Link to="/login">Login again</Link>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Form
|
||||
|
||||
The form component can be used to navigate with `URLSearchParams` provided by the user.
|
||||
|
||||
```tsx
|
||||
<Form action="/search">
|
||||
<input type="text" name="q" />
|
||||
</Form>
|
||||
```
|
||||
|
||||
If the user enters "journey" into the input and submits it, they will navigate to:
|
||||
|
||||
```
|
||||
/search?q=journey
|
||||
```
|
||||
|
||||
Forms with `<Form method="post" />` will also navigate to the action prop but will submit the data as `FormData` instead of `URLSearchParams`. However, it is more common to `useFetcher()` to POST form data. See [Using Fetchers](../../how-to/fetchers).
|
||||
|
||||
## redirect
|
||||
|
||||
Inside of route loaders and actions, you can return a `redirect` to another URL.
|
||||
|
||||
```tsx
|
||||
import { redirect } from "react-router";
|
||||
|
||||
export async function loader({ request }) {
|
||||
let user = await getUser(request);
|
||||
if (!user) {
|
||||
return redirect("/login");
|
||||
}
|
||||
return { userName: user.name };
|
||||
}
|
||||
```
|
||||
|
||||
It is common to redirect to a new record after it has been created:
|
||||
|
||||
```tsx
|
||||
import { redirect } from "react-router";
|
||||
|
||||
export async function action({ request }) {
|
||||
let formData = await request.formData();
|
||||
let project = await createProject(formData);
|
||||
return redirect(`/projects/${project.id}`);
|
||||
}
|
||||
```
|
||||
|
||||
## useNavigate
|
||||
|
||||
This hook allows the programmer to navigate the user to a new page without the user interacting. Usage of this hook should be uncommon. It's recommended to use the other APIs in this guide when possible.
|
||||
|
||||
Reserve usage of `useNavigate` to situations where the user is _not_ interacting but you need to navigate, for example:
|
||||
|
||||
- Logging them out after inactivity
|
||||
- Timed UIs like quizzes, etc.
|
||||
|
||||
```tsx
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
export function useLogoutAfterInactivity() {
|
||||
let navigate = useNavigate();
|
||||
|
||||
useFakeInactivityHook(() => {
|
||||
navigate("/logout");
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Next: [Pending UI](./pending-ui)
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
---
|
||||
title: Pending UI
|
||||
order: 7
|
||||
---
|
||||
|
||||
# Pending UI
|
||||
|
||||
[MODES: framework]
|
||||
|
||||
## Introduction
|
||||
|
||||
When the user navigates to a new route, or submits data to an action, the UI should immediately respond to the user's actions with a pending or optimistic state. Application code is responsible for this.
|
||||
|
||||
## Global Pending Navigation
|
||||
|
||||
When the user navigates to a new url, the loaders for the next page are awaited before the next page renders. You can get the pending state from `useNavigation`.
|
||||
|
||||
```tsx
|
||||
import { useNavigation } from "react-router";
|
||||
|
||||
export default function Root() {
|
||||
const navigation = useNavigation();
|
||||
const isNavigating = Boolean(navigation.location);
|
||||
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
{isNavigating && <GlobalSpinner />}
|
||||
<Outlet />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Local Pending Navigation
|
||||
|
||||
Pending indicators can also be localized to the link. NavLink's children, className, and style props can be functions that receive the pending state.
|
||||
|
||||
```tsx
|
||||
import { NavLink } from "react-router";
|
||||
|
||||
function Navbar() {
|
||||
return (
|
||||
<nav>
|
||||
<NavLink to="/home">
|
||||
{({ isPending }) => (
|
||||
<span>Home {isPending && <Spinner />}</span>
|
||||
)}
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/about"
|
||||
style={({ isPending }) => ({
|
||||
color: isPending ? "gray" : "black",
|
||||
})}
|
||||
>
|
||||
About
|
||||
</NavLink>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Pending Form Submission
|
||||
|
||||
When a form is submitted, the UI should immediately respond to the user's actions with a pending state. This is easiest to do with a [fetcher][use_fetcher] form because it has its own independent state (whereas normal forms cause a global navigation).
|
||||
|
||||
```tsx filename=app/project.tsx lines=[10-12]
|
||||
import { useFetcher } from "react-router";
|
||||
|
||||
function NewProjectForm() {
|
||||
const fetcher = useFetcher();
|
||||
|
||||
return (
|
||||
<fetcher.Form method="post">
|
||||
<input type="text" name="title" />
|
||||
<button type="submit">
|
||||
{fetcher.state !== "idle"
|
||||
? "Submitting..."
|
||||
: "Submit"}
|
||||
</button>
|
||||
</fetcher.Form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
For non-fetcher form submissions, pending states are available on `useNavigation`.
|
||||
|
||||
```tsx filename=app/projects/new.tsx
|
||||
import { useNavigation, Form } from "react-router";
|
||||
|
||||
function NewProjectForm() {
|
||||
const navigation = useNavigation();
|
||||
|
||||
return (
|
||||
<Form method="post" action="/projects/new">
|
||||
<input type="text" name="title" />
|
||||
<button type="submit">
|
||||
{navigation.formAction === "/projects/new"
|
||||
? "Submitting..."
|
||||
: "Submit"}
|
||||
</button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Optimistic UI
|
||||
|
||||
When the future state of the UI is known by the form submission data, an optimistic UI can be implemented for instant UX.
|
||||
|
||||
```tsx filename=app/project.tsx lines=[4-7]
|
||||
function Task({ task }) {
|
||||
const fetcher = useFetcher();
|
||||
|
||||
let isComplete = task.status === "complete";
|
||||
if (fetcher.formData) {
|
||||
isComplete =
|
||||
fetcher.formData.get("status") === "complete";
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>{task.title}</div>
|
||||
<fetcher.Form method="post">
|
||||
<button
|
||||
name="status"
|
||||
value={isComplete ? "incomplete" : "complete"}
|
||||
>
|
||||
{isComplete ? "Mark Incomplete" : "Mark Complete"}
|
||||
</button>
|
||||
</fetcher.Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Next: [Testing](./testing)
|
||||
|
||||
[use_fetcher]: https://api.reactrouter.com/v7/functions/react-router.useFetcher.html
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: Rendering Strategies
|
||||
order: 4
|
||||
---
|
||||
|
||||
# Rendering Strategies
|
||||
|
||||
[MODES: framework]
|
||||
|
||||
## Introduction
|
||||
|
||||
There are three rendering strategies in React Router:
|
||||
|
||||
- Client Side Rendering
|
||||
- Server Side Rendering
|
||||
- Static Pre-rendering
|
||||
|
||||
## Client Side Rendering
|
||||
|
||||
Routes are always client side rendered as the user navigates around the app. If you're looking to build a Single Page App, disable server rendering:
|
||||
|
||||
```ts filename=react-router.config.ts
|
||||
import type { Config } from "@react-router/dev/config";
|
||||
|
||||
export default {
|
||||
ssr: false,
|
||||
} satisfies Config;
|
||||
```
|
||||
|
||||
## Server Side Rendering
|
||||
|
||||
```ts filename=react-router.config.ts
|
||||
import type { Config } from "@react-router/dev/config";
|
||||
|
||||
export default {
|
||||
ssr: true,
|
||||
} satisfies Config;
|
||||
```
|
||||
|
||||
Server side rendering requires a deployment that supports it. Though it's a global setting, individual routes can still be statically pre-rendered. Routes can also use client data loading with `clientLoader` to avoid server rendering/fetching for their portion of the UI.
|
||||
|
||||
## Static Pre-rendering
|
||||
|
||||
```ts filename=react-router.config.ts
|
||||
import type { Config } from "@react-router/dev/config";
|
||||
|
||||
export default {
|
||||
// return a list of URLs to prerender at build time
|
||||
async prerender() {
|
||||
return ["/", "/about", "/contact"];
|
||||
},
|
||||
} satisfies Config;
|
||||
```
|
||||
|
||||
Pre-rendering is a build-time operation that generates static HTML and client navigation data payloads for a list of URLs. This is useful for SEO and performance, especially for deployments without server rendering. When pre-rendering, route module loaders are used to fetch data at build time.
|
||||
|
||||
---
|
||||
|
||||
Next: [Data Loading](./data-loading)
|
||||
+527
@@ -0,0 +1,527 @@
|
||||
---
|
||||
title: Route Module
|
||||
order: 3
|
||||
---
|
||||
|
||||
# Route Module
|
||||
|
||||
[MODES: framework]
|
||||
|
||||
## Introduction
|
||||
|
||||
The files referenced in `routes.ts` are called Route Modules.
|
||||
|
||||
```tsx filename=app/routes.ts
|
||||
route("teams/:teamId", "./team.tsx"),
|
||||
// route module ^^^^^^^^
|
||||
```
|
||||
|
||||
Route modules are the foundation of React Router's framework features, they define:
|
||||
|
||||
- automatic code-splitting
|
||||
- data loading
|
||||
- actions
|
||||
- revalidation
|
||||
- error boundaries
|
||||
- and more
|
||||
|
||||
This guide is a quick overview of every route module feature. The rest of the getting started guides will cover these features in more detail.
|
||||
|
||||
## Component (`default`)
|
||||
|
||||
The `default` export in a route module defines the component that will render when the route matches.
|
||||
|
||||
```tsx filename=app/routes/my-route.tsx
|
||||
export default function MyRouteComponent() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Look ma!</h1>
|
||||
<p>
|
||||
I'm still using React Router after like 10 years.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Props passed to the Component
|
||||
|
||||
When the component is rendered, it is provided the props defined in `Route.ComponentProps` that React Router will automatically generate for you. These props include:
|
||||
|
||||
1. `loaderData`: The data returned from the `loader` function in this route module
|
||||
2. `actionData`: The data returned from the `action` function in this route module
|
||||
3. `params`: An object containing the route parameters (if any).
|
||||
4. `matches`: An array of all the matches in the current route tree.
|
||||
|
||||
You can use these props in place of hooks like `useLoaderData` or `useParams`. This may be preferable because they will be automatically typed correctly for the route.
|
||||
|
||||
### Using props
|
||||
|
||||
```tsx filename=app/routes/my-route-with-default-params.tsx
|
||||
import type { Route } from "./+types/route-name";
|
||||
|
||||
export default function MyRouteComponent({
|
||||
loaderData,
|
||||
actionData,
|
||||
params,
|
||||
matches,
|
||||
}: Route.ComponentProps) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Welcome to My Route with Props!</h1>
|
||||
<p>Loader Data: {JSON.stringify(loaderData)}</p>
|
||||
<p>Action Data: {JSON.stringify(actionData)}</p>
|
||||
<p>Route Parameters: {JSON.stringify(params)}</p>
|
||||
<p>Matched Routes: {JSON.stringify(matches)}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## `middleware`
|
||||
|
||||
Route [middleware][middleware] runs sequentially on the server before and after document and
|
||||
data requests. This gives you a singular place to do things like logging,
|
||||
authentication, and post-processing of responses. The `next` function continues down the chain, and on the leaf route the `next` function executes the loaders/actions for the navigation.
|
||||
|
||||
Here's an example middleware to log requests on the server:
|
||||
|
||||
```tsx filename=root.tsx
|
||||
async function loggingMiddleware(
|
||||
{ request, context },
|
||||
next,
|
||||
) {
|
||||
console.log(
|
||||
`${new Date().toISOString()} ${request.method} ${request.url}`,
|
||||
);
|
||||
const start = performance.now();
|
||||
const response = await next();
|
||||
const duration = performance.now() - start;
|
||||
console.log(
|
||||
`${new Date().toISOString()} Response ${response.status} (${duration}ms)`,
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
export const middleware = [loggingMiddleware];
|
||||
```
|
||||
|
||||
Here's an example middleware to check for logged in users and set the user in
|
||||
`context` you can then access from loaders:
|
||||
|
||||
```tsx filename=routes/_auth.tsx
|
||||
async function authMiddleware({ request, context }) {
|
||||
const session = await getSession(request);
|
||||
const userId = session.get("userId");
|
||||
|
||||
if (!userId) {
|
||||
throw redirect("/login");
|
||||
}
|
||||
|
||||
const user = await getUserById(userId);
|
||||
context.set(userContext, user);
|
||||
}
|
||||
|
||||
export const middleware = [authMiddleware];
|
||||
```
|
||||
|
||||
<docs-warning>Please make sure you understand [when middleware runs][when-middleware-runs] to make sure your application will behave the way you intend when adding middleware to your routes.</docs-warning>
|
||||
|
||||
See also:
|
||||
|
||||
- [`middleware` params][middleware-params]
|
||||
- [Middleware][middleware]
|
||||
|
||||
## `clientMiddleware`
|
||||
|
||||
This is the client-side equivalent of `middleware` and runs in the browser during client navigations. The only difference from server middleware is that client middleware doesn't return Responses because they're not wrapping an HTTP request on the server.
|
||||
|
||||
Here's an example middleware to log requests on the client:
|
||||
|
||||
```tsx filename=root.tsx
|
||||
async function loggingMiddleware(
|
||||
{ request, context },
|
||||
next,
|
||||
) {
|
||||
console.log(
|
||||
`${new Date().toISOString()} ${request.method} ${request.url}`,
|
||||
);
|
||||
const start = performance.now();
|
||||
await next(); // 👈 No Response returned
|
||||
const duration = performance.now() - start;
|
||||
console.log(
|
||||
`${new Date().toISOString()} (${duration}ms)`,
|
||||
);
|
||||
// ✅ No need to return anything
|
||||
}
|
||||
|
||||
export const clientMiddleware = [loggingMiddleware];
|
||||
```
|
||||
|
||||
See also:
|
||||
|
||||
- [Middleware][middleware]
|
||||
- [Client Data][client-data]
|
||||
|
||||
## `loader`
|
||||
|
||||
Route loaders provide data to route components before they are rendered. They are only called on the server when server rendering or during the build with pre-rendering.
|
||||
|
||||
```tsx
|
||||
export async function loader() {
|
||||
return { message: "Hello, world!" };
|
||||
}
|
||||
|
||||
export default function MyRoute({ loaderData }) {
|
||||
return <h1>{loaderData.message}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
See also:
|
||||
|
||||
- [`loader` params][loader-params]
|
||||
|
||||
## `clientLoader`
|
||||
|
||||
Called only in the browser, route client loaders provide data to route components in addition to, or in place of, route loaders.
|
||||
|
||||
```tsx
|
||||
export async function clientLoader({ serverLoader }) {
|
||||
// call the server loader
|
||||
const serverData = await serverLoader();
|
||||
// And/or fetch data on the client
|
||||
const data = getDataFromClient();
|
||||
// Return the data to expose through useLoaderData()
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
Client loaders can participate in initial page load hydration of server rendered pages by setting the `hydrate` property on the function:
|
||||
|
||||
```tsx
|
||||
export async function clientLoader() {
|
||||
// ...
|
||||
}
|
||||
clientLoader.hydrate = true as const;
|
||||
```
|
||||
|
||||
<docs-info>
|
||||
|
||||
By using `as const`, TypeScript will infer that the type for `clientLoader.hydrate` is `true` instead of `boolean`.
|
||||
That way, React Router can derive types for `loaderData` based on the value of `clientLoader.hydrate`.
|
||||
|
||||
</docs-info>
|
||||
|
||||
See also:
|
||||
|
||||
- [`clientLoader` params][client-loader-params]
|
||||
- [Client Data][client-data]
|
||||
|
||||
## `action`
|
||||
|
||||
Route actions allow server-side data mutations with automatic revalidation of all loader data on the page when called from `<Form>`, `useFetcher`, and `useSubmit`.
|
||||
|
||||
```tsx
|
||||
// route("/list", "./list.tsx")
|
||||
import { Form } from "react-router";
|
||||
import { TodoList } from "~/components/TodoList";
|
||||
|
||||
// this data will be loaded after the action completes...
|
||||
export async function loader() {
|
||||
const items = await fakeDb.getItems();
|
||||
return { items };
|
||||
}
|
||||
|
||||
// ...so that the list here is updated automatically
|
||||
export default function Items({ loaderData }) {
|
||||
return (
|
||||
<div>
|
||||
<List items={loaderData.items} />
|
||||
<Form method="post" navigate={false} action="/list">
|
||||
<input type="text" name="title" />
|
||||
<button type="submit">Create Todo</button>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function action({ request }) {
|
||||
const data = await request.formData();
|
||||
const todo = await fakeDb.addItem({
|
||||
title: data.get("title"),
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
```
|
||||
|
||||
See also:
|
||||
|
||||
- [`action` params][action-params]
|
||||
|
||||
## `clientAction`
|
||||
|
||||
Like route actions but only called in the browser.
|
||||
|
||||
```tsx
|
||||
export async function clientAction({ serverAction }) {
|
||||
fakeInvalidateClientSideCache();
|
||||
// can still call the server action if needed
|
||||
const data = await serverAction();
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
See also:
|
||||
|
||||
- [`clientAction` params][client-action-params]
|
||||
- [Client Data][client-data]
|
||||
|
||||
## `ErrorBoundary`
|
||||
|
||||
When other route module APIs throw, the route module `ErrorBoundary` will render instead of the route component.
|
||||
|
||||
```tsx
|
||||
import {
|
||||
isRouteErrorResponse,
|
||||
useRouteError,
|
||||
} from "react-router";
|
||||
|
||||
export function ErrorBoundary() {
|
||||
const error = useRouteError();
|
||||
|
||||
if (isRouteErrorResponse(error)) {
|
||||
return (
|
||||
<div>
|
||||
<h1>
|
||||
{error.status} {error.statusText}
|
||||
</h1>
|
||||
<p>{error.data}</p>
|
||||
</div>
|
||||
);
|
||||
} else if (error instanceof Error) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Error</h1>
|
||||
<p>{error.message}</p>
|
||||
<p>The stack trace is:</p>
|
||||
<pre>{error.stack}</pre>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <h1>Unknown Error</h1>;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See also:
|
||||
|
||||
- [`useRouteError`][use-route-error]
|
||||
- [`isRouteErrorResponse`][is-route-error-response]
|
||||
|
||||
## `HydrateFallback`
|
||||
|
||||
On initial page load, the route component renders only after the client loader is finished. If exported, a `HydrateFallback` can render immediately in place of the route component.
|
||||
|
||||
```tsx filename=routes/client-only-route.tsx
|
||||
export async function clientLoader() {
|
||||
const data = await fakeLoadLocalGameData();
|
||||
return data;
|
||||
}
|
||||
|
||||
export function HydrateFallback() {
|
||||
return <p>Loading Game...</p>;
|
||||
}
|
||||
|
||||
export default function Component({ loaderData }) {
|
||||
return <Game data={loaderData} />;
|
||||
}
|
||||
```
|
||||
|
||||
## `headers`
|
||||
|
||||
The route `headers` function defines the HTTP headers to be sent with the response when server rendering.
|
||||
|
||||
```tsx
|
||||
export function headers() {
|
||||
return {
|
||||
"X-Stretchy-Pants": "its for fun",
|
||||
"Cache-Control": "max-age=300, s-maxage=3600",
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
See also:
|
||||
|
||||
- [`Headers`][headers]
|
||||
|
||||
## `handle`
|
||||
|
||||
Route handle allows apps to add anything to a route match in `useMatches` to create abstractions (like breadcrumbs, etc.).
|
||||
|
||||
```tsx
|
||||
export const handle = {
|
||||
its: "all yours",
|
||||
};
|
||||
```
|
||||
|
||||
See also:
|
||||
|
||||
- [`useMatches`][use-matches]
|
||||
|
||||
## `links`
|
||||
|
||||
Route links define [`<link>` element][link-element]s to be rendered in the document `<head>`.
|
||||
|
||||
```tsx
|
||||
export function links() {
|
||||
return [
|
||||
{
|
||||
rel: "icon",
|
||||
href: "/favicon.png",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
rel: "stylesheet",
|
||||
href: "https://example.com/some/styles.css",
|
||||
},
|
||||
{
|
||||
rel: "preload",
|
||||
href: "/images/banner.jpg",
|
||||
as: "image",
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
All routes links will be aggregated and rendered through the `<Links />` component, usually rendered in your app root:
|
||||
|
||||
```tsx
|
||||
import { Links } from "react-router";
|
||||
|
||||
export default function Root() {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<Links />
|
||||
</head>
|
||||
|
||||
<body />
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
See also:
|
||||
|
||||
- [Styling][styling]
|
||||
|
||||
## `meta`
|
||||
|
||||
Route meta defines [meta tags][meta-element] to be rendered in the `<Meta />` component, usually placed in the `<head>`.
|
||||
|
||||
<docs-warning>
|
||||
|
||||
Since React 19, [using the built-in `<meta>` element](https://react.dev/reference/react-dom/components/meta) is recommended over the use of the route module's `meta` export.
|
||||
|
||||
Here is an example of how to use it and the `<title>` element:
|
||||
|
||||
```tsx
|
||||
export default function MyRoute() {
|
||||
return (
|
||||
<div>
|
||||
<title>Very cool app</title>
|
||||
<meta property="og:title" content="Very cool app" />
|
||||
<meta
|
||||
name="description"
|
||||
content="This app is the best"
|
||||
/>
|
||||
{/* The rest of your route content... */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
</docs-warning>
|
||||
|
||||
```tsx filename=app/product.tsx
|
||||
export function meta() {
|
||||
return [
|
||||
{ title: "Very cool app" },
|
||||
{
|
||||
property: "og:title",
|
||||
content: "Very cool app",
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
content: "This app is the best",
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
```tsx filename=app/root.tsx
|
||||
import { Meta } from "react-router";
|
||||
|
||||
export default function Root() {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<Meta />
|
||||
</head>
|
||||
|
||||
<body />
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
The meta of the last matching route is used, allowing you to override parent routes' meta. It's important to note that the entire meta descriptor array is replaced, not merged. This gives you the flexibility to build your own meta composition logic across pages at different levels.
|
||||
|
||||
**See also**
|
||||
|
||||
- [`meta` params][meta-params]
|
||||
- [`meta` function return types][meta-function]
|
||||
|
||||
## `shouldRevalidate`
|
||||
|
||||
In framework mode with SSR, route loaders are automatically revalidated after all navigations and form submissions (this is different from [Data Mode][data-mode-should-revalidate]). This enables middleware and loaders to share a request context and optimize in different ways than they would in Data Mode.
|
||||
|
||||
Defining this function allows you to opt out of revalidation for a route loader for navigations and form submissions.
|
||||
|
||||
```tsx
|
||||
import type { ShouldRevalidateFunctionArgs } from "react-router";
|
||||
|
||||
export function shouldRevalidate(
|
||||
arg: ShouldRevalidateFunctionArgs,
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
When using [SPA Mode][spa-mode], there are no server loaders to call on navigations, so `shouldRevalidate` behaves the same as it does in [Data Mode][data-mode-should-revalidate].
|
||||
|
||||
[`ShouldRevalidateFunctionArgs` Reference Documentation ↗](https://api.reactrouter.com/v7/interfaces/react-router.ShouldRevalidateFunctionArgs.html)
|
||||
|
||||
---
|
||||
|
||||
Next: [Rendering Strategies](./rendering)
|
||||
|
||||
[middleware-params]: https://api.reactrouter.com/v7/types/react-router.MiddlewareFunction.html
|
||||
[middleware]: ../../how-to/middleware
|
||||
[when-middleware-runs]: ../../how-to/middleware#when-middleware-runs
|
||||
[loader-params]: https://api.reactrouter.com/v7/interfaces/react-router.LoaderFunctionArgs
|
||||
[client-loader-params]: https://api.reactrouter.com/v7/types/react-router.ClientLoaderFunctionArgs
|
||||
[action-params]: https://api.reactrouter.com/v7/interfaces/react-router.ActionFunctionArgs
|
||||
[client-action-params]: https://api.reactrouter.com/v7/types/react-router.ClientActionFunctionArgs
|
||||
[use-route-error]: ../../api/hooks/useRouteError
|
||||
[is-route-error-response]: ../../api/utils/isRouteErrorResponse
|
||||
[headers]: https://developer.mozilla.org/en-US/docs/Web/API/Response/headers
|
||||
[use-matches]: ../../api/hooks/useMatches
|
||||
[link-element]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link
|
||||
[meta-element]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta
|
||||
[meta-params]: https://api.reactrouter.com/v7/interfaces/react-router.MetaArgs
|
||||
[meta-function]: https://api.reactrouter.com/v7/types/react-router.MetaDescriptor.html
|
||||
[data-mode-should-revalidate]: ../data/route-object#shouldrevalidate
|
||||
[spa-mode]: ../../how-to/spa
|
||||
[client-data]: ../../how-to/client-data
|
||||
[styling]: ../../explanation/styling
|
||||
+362
@@ -0,0 +1,362 @@
|
||||
---
|
||||
title: Routing
|
||||
order: 2
|
||||
---
|
||||
|
||||
# Routing
|
||||
|
||||
[MODES: framework]
|
||||
|
||||
## Configuring Routes
|
||||
|
||||
Routes are configured in `app/routes.ts`. Each route has two required parts: a URL pattern to match the URL, and a file path to the route module that defines its behavior.
|
||||
|
||||
```ts filename=app/routes.ts
|
||||
import {
|
||||
type RouteConfig,
|
||||
route,
|
||||
} from "@react-router/dev/routes";
|
||||
|
||||
export default [
|
||||
route("some/path", "./some/file.tsx"),
|
||||
// pattern ^ ^ module file
|
||||
] satisfies RouteConfig;
|
||||
```
|
||||
|
||||
Here is a larger sample route config:
|
||||
|
||||
```ts filename=app/routes.ts
|
||||
import {
|
||||
type RouteConfig,
|
||||
route,
|
||||
index,
|
||||
layout,
|
||||
prefix,
|
||||
} from "@react-router/dev/routes";
|
||||
|
||||
export default [
|
||||
index("./home.tsx"),
|
||||
route("about", "./about.tsx"),
|
||||
|
||||
layout("./auth/layout.tsx", [
|
||||
route("login", "./auth/login.tsx"),
|
||||
route("register", "./auth/register.tsx"),
|
||||
]),
|
||||
|
||||
...prefix("concerts", [
|
||||
index("./concerts/home.tsx"),
|
||||
route(":city", "./concerts/city.tsx"),
|
||||
route("trending", "./concerts/trending.tsx"),
|
||||
]),
|
||||
] satisfies RouteConfig;
|
||||
```
|
||||
|
||||
If you prefer to define your routes via file naming conventions rather than configuration, the `@react-router/fs-routes` package provides a [file system routing convention][file-route-conventions]. You can even combine different routing conventions if you like:
|
||||
|
||||
```ts filename=app/routes.ts
|
||||
import {
|
||||
type RouteConfig,
|
||||
route,
|
||||
} from "@react-router/dev/routes";
|
||||
import { flatRoutes } from "@react-router/fs-routes";
|
||||
|
||||
export default [
|
||||
route("/", "./home.tsx"),
|
||||
|
||||
...(await flatRoutes()),
|
||||
] satisfies RouteConfig;
|
||||
```
|
||||
|
||||
## Route Modules
|
||||
|
||||
The files referenced in `routes.ts` define each route's behavior:
|
||||
|
||||
```tsx filename=app/routes.ts
|
||||
route("teams/:teamId", "./team.tsx"),
|
||||
// route module ^^^^^^^^
|
||||
```
|
||||
|
||||
Here's a sample route module:
|
||||
|
||||
```tsx filename=app/team.tsx
|
||||
// provides type safety/inference
|
||||
import type { Route } from "./+types/team";
|
||||
|
||||
// provides `loaderData` to the component
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
let team = await fetchTeam(params.teamId);
|
||||
return { name: team.name };
|
||||
}
|
||||
|
||||
// renders after the loader is done
|
||||
export default function Component({
|
||||
loaderData,
|
||||
}: Route.ComponentProps) {
|
||||
return <h1>{loaderData.name}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
Route modules have more features like actions, headers, and error boundaries, but they will be covered in the next guide: [Route Modules](./route-module)
|
||||
|
||||
## Nested Routes
|
||||
|
||||
Routes can be nested inside parent routes.
|
||||
|
||||
```ts filename=app/routes.ts
|
||||
import {
|
||||
type RouteConfig,
|
||||
route,
|
||||
index,
|
||||
} from "@react-router/dev/routes";
|
||||
|
||||
export default [
|
||||
// parent route
|
||||
route("dashboard", "./dashboard.tsx", [
|
||||
// child routes
|
||||
index("./home.tsx"),
|
||||
route("settings", "./settings.tsx"),
|
||||
]),
|
||||
] satisfies RouteConfig;
|
||||
```
|
||||
|
||||
The path of the parent is automatically included in the child, so this config creates both `"/dashboard"` and `"/dashboard/settings"` URLs.
|
||||
|
||||
Child routes are rendered through the `<Outlet/>` in the parent route.
|
||||
|
||||
```tsx filename=app/dashboard.tsx
|
||||
import { Outlet } from "react-router";
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
{/* will either be home.tsx or settings.tsx */}
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Root Route
|
||||
|
||||
Every route in `routes.ts` is nested inside the special `app/root.tsx` module.
|
||||
|
||||
## Layout Routes
|
||||
|
||||
Using `layout`, layout routes create new nesting for their children, but they don't add any segments to the URL. It's like the root route but they can be added at any level.
|
||||
|
||||
```tsx filename=app/routes.ts lines=[10,16]
|
||||
import {
|
||||
type RouteConfig,
|
||||
route,
|
||||
layout,
|
||||
index,
|
||||
prefix,
|
||||
} from "@react-router/dev/routes";
|
||||
|
||||
export default [
|
||||
layout("./marketing/layout.tsx", [
|
||||
index("./marketing/home.tsx"),
|
||||
route("contact", "./marketing/contact.tsx"),
|
||||
]),
|
||||
...prefix("projects", [
|
||||
index("./projects/home.tsx"),
|
||||
layout("./projects/project-layout.tsx", [
|
||||
route(":pid", "./projects/project.tsx"),
|
||||
route(":pid/edit", "./projects/edit-project.tsx"),
|
||||
]),
|
||||
]),
|
||||
] satisfies RouteConfig;
|
||||
```
|
||||
|
||||
Note that:
|
||||
|
||||
- `home.tsx` and `contact.tsx` will be rendered into the `marketing/layout.tsx` outlet without creating any new URL paths
|
||||
- `project.tsx` and `edit-project.tsx` will be rendered into the `projects/project-layout.tsx` outlet at `/projects/:pid` and `/projects/:pid/edit` while `projects/home.tsx` will not.
|
||||
|
||||
## Index Routes
|
||||
|
||||
```ts
|
||||
index(componentFile),
|
||||
```
|
||||
|
||||
Index routes render into their parent's [Outlet][outlet] at their parent's URL (like a default child route).
|
||||
|
||||
```ts filename=app/routes.ts
|
||||
import {
|
||||
type RouteConfig,
|
||||
route,
|
||||
index,
|
||||
} from "@react-router/dev/routes";
|
||||
|
||||
export default [
|
||||
// renders into the root.tsx Outlet at /
|
||||
index("./home.tsx"),
|
||||
route("dashboard", "./dashboard.tsx", [
|
||||
// renders into the dashboard.tsx Outlet at /dashboard
|
||||
index("./dashboard-home.tsx"),
|
||||
route("settings", "./dashboard-settings.tsx"),
|
||||
]),
|
||||
] satisfies RouteConfig;
|
||||
```
|
||||
|
||||
Note that index routes can't have children.
|
||||
|
||||
## Route Prefixes
|
||||
|
||||
Using `prefix`, you can add a path prefix to a set of routes without needing to introduce a parent route.
|
||||
|
||||
```tsx filename=app/routes.ts lines=[14]
|
||||
import {
|
||||
type RouteConfig,
|
||||
route,
|
||||
layout,
|
||||
index,
|
||||
prefix,
|
||||
} from "@react-router/dev/routes";
|
||||
|
||||
export default [
|
||||
layout("./marketing/layout.tsx", [
|
||||
index("./marketing/home.tsx"),
|
||||
route("contact", "./marketing/contact.tsx"),
|
||||
]),
|
||||
...prefix("projects", [
|
||||
index("./projects/home.tsx"),
|
||||
layout("./projects/project-layout.tsx", [
|
||||
route(":pid", "./projects/project.tsx"),
|
||||
route(":pid/edit", "./projects/edit-project.tsx"),
|
||||
]),
|
||||
]),
|
||||
] satisfies RouteConfig;
|
||||
```
|
||||
|
||||
Note that this does not introduce a new route into the route tree. Instead, it merely modifies the paths of its children.
|
||||
|
||||
For example, these two sets of routes are equivalent:
|
||||
|
||||
```ts filename=app/routes.ts
|
||||
// This usage of `prefix`...
|
||||
prefix("parent", [
|
||||
route("child1", "./child1.tsx"),
|
||||
route("child2", "./child2.tsx"),
|
||||
])
|
||||
|
||||
// ...is equivalent to this:
|
||||
[
|
||||
route("parent/child1", "./child1.tsx"),
|
||||
route("parent/child2", "./child2.tsx"),
|
||||
]
|
||||
```
|
||||
|
||||
## Dynamic Segments
|
||||
|
||||
If a path segment starts with `:` then it becomes a "dynamic segment". When the route matches the URL, the dynamic segment will be parsed from the URL and provided as `params` to other router APIs.
|
||||
|
||||
```ts filename=app/routes.ts
|
||||
route("teams/:teamId", "./team.tsx"),
|
||||
```
|
||||
|
||||
```tsx filename=app/team.tsx
|
||||
import type { Route } from "./+types/team";
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
// ^? { teamId: string }
|
||||
}
|
||||
|
||||
export default function Component({
|
||||
params,
|
||||
}: Route.ComponentProps) {
|
||||
params.teamId;
|
||||
// ^ string
|
||||
}
|
||||
```
|
||||
|
||||
You can have multiple dynamic segments in one route path:
|
||||
|
||||
```ts filename=app/routes.ts
|
||||
route("c/:categoryId/p/:productId", "./product.tsx"),
|
||||
```
|
||||
|
||||
```tsx filename=app/product.tsx
|
||||
import type { Route } from "./+types/product";
|
||||
|
||||
async function loader({ params }: LoaderArgs) {
|
||||
// ^? { categoryId: string; productId: string }
|
||||
}
|
||||
```
|
||||
|
||||
## Optional Segments
|
||||
|
||||
You can make a route segment optional by adding a `?` to the end of the segment.
|
||||
|
||||
```ts filename=app/routes.ts
|
||||
route(":lang?/categories", "./categories.tsx"),
|
||||
```
|
||||
|
||||
You can have optional static segments, too:
|
||||
|
||||
```ts filename=app/routes.ts
|
||||
route("users/:userId/edit?", "./user.tsx");
|
||||
```
|
||||
|
||||
## Splats
|
||||
|
||||
Also known as "catchall" and "star" segments. If a route path pattern ends with `/*` then it will match any characters following the `/`, including other `/` characters.
|
||||
|
||||
```ts filename=app/routes.ts
|
||||
route("files/*", "./files.tsx"),
|
||||
```
|
||||
|
||||
```tsx filename=app/files.tsx
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
// params["*"] will contain the remaining URL after files/
|
||||
}
|
||||
```
|
||||
|
||||
You can destructure the `*`, you just have to assign it a new name. A common name is `splat`:
|
||||
|
||||
```tsx
|
||||
const { "*": splat } = params;
|
||||
```
|
||||
|
||||
You can also use a splat to catch requests that don't match any route:
|
||||
|
||||
```ts filename=app/routes.ts
|
||||
route("*", "./catchall.tsx"); // catchall route,
|
||||
```
|
||||
|
||||
```tsx filename=app/catchall.tsx
|
||||
export function loader() {
|
||||
throw new Response("Page not found", { status: 404 });
|
||||
}
|
||||
```
|
||||
|
||||
## Component Routes
|
||||
|
||||
You can also use components that match the URL to elements anywhere in the component tree:
|
||||
|
||||
```tsx
|
||||
import { Routes, Route } from "react-router";
|
||||
|
||||
function Wizard() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Some Wizard with Steps</h1>
|
||||
<Routes>
|
||||
<Route index element={<StepOne />} />
|
||||
<Route path="step-2" element={<StepTwo />} />
|
||||
<Route path="step-3" element={<StepThree />} />
|
||||
</Routes>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Note that these routes do not participate in data loading, actions, code splitting, or any other route module features, so their use cases are more limited than those of the route module.
|
||||
|
||||
---
|
||||
|
||||
Next: [Route Module](./route-module)
|
||||
|
||||
[file-route-conventions]: ../../how-to/file-route-conventions
|
||||
[outlet]: https://api.reactrouter.com/v7/functions/react-router.Outlet.html
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
---
|
||||
title: Testing
|
||||
order: 9
|
||||
---
|
||||
|
||||
# Testing
|
||||
|
||||
[MODES: framework, data]
|
||||
|
||||
## Introduction
|
||||
|
||||
When components use things like `useLoaderData`, `<Link>`, etc, they are required to be rendered in context of a React Router app. The `createRoutesStub` function creates that context to test components in isolation.
|
||||
|
||||
Consider a login form component that relies on `useActionData`
|
||||
|
||||
```tsx
|
||||
import { useActionData } from "react-router";
|
||||
|
||||
export function LoginForm() {
|
||||
const actionData = useActionData();
|
||||
const errors = actionData?.errors;
|
||||
return (
|
||||
<Form method="post">
|
||||
<label>
|
||||
<input type="text" name="username" />
|
||||
{errors?.username && <div>{errors.username}</div>}
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="password" name="password" />
|
||||
{errors?.password && <div>{errors.password}</div>}
|
||||
</label>
|
||||
|
||||
<button type="submit">Login</button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
We can test this component with `createRoutesStub`. It takes an array of objects that resemble route modules with loaders, actions, and components.
|
||||
|
||||
```tsx
|
||||
import { createRoutesStub } from "react-router";
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { LoginForm } from "./LoginForm";
|
||||
|
||||
test("LoginForm renders error messages", async () => {
|
||||
const USER_MESSAGE = "Username is required";
|
||||
const PASSWORD_MESSAGE = "Password is required";
|
||||
|
||||
const Stub = createRoutesStub([
|
||||
{
|
||||
path: "/login",
|
||||
Component: LoginForm,
|
||||
action() {
|
||||
return {
|
||||
errors: {
|
||||
username: USER_MESSAGE,
|
||||
password: PASSWORD_MESSAGE,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// render the app stub at "/login"
|
||||
render(<Stub initialEntries={["/login"]} />);
|
||||
|
||||
// simulate interactions
|
||||
userEvent.click(screen.getByText("Login"));
|
||||
await waitFor(() => screen.findByText(USER_MESSAGE));
|
||||
await waitFor(() => screen.findByText(PASSWORD_MESSAGE));
|
||||
});
|
||||
```
|
||||
|
||||
## Using with Framework Mode Types
|
||||
|
||||
It's important to note that `createRoutesStub` is designed for _unit_ testing of reusable components in your application that rely on contextual router information (i.e., `loaderData`, `actionData`, `matches`). These components usually obtain this information via the hooks (`useLoaderData`, `useActionData`, `useMatches`) or via props passed down from the ancestor route component. We **strongly** recommend limiting your usage of `createRoutesStub` to unit testing of these types of reusable components.
|
||||
|
||||
`createRoutesStub` is _not designed_ for (and is arguably incompatible with) direct testing of Route components using the [`Route.\*`](../../explanation/type-safety) types available in Framework Mode. This is because the `Route.*` types are derived from your actual application - including the real `loader`/`action` functions as well as the structure of your route tree structure (which defines the `matches` type). When you use `createRoutesStub`, you are providing stubbed values for `loaderData`, `actionData`, and even your `matches` based on the route tree you pass to `createRoutesStub`. Therefore, the types won't align with the `Route.*` types and you'll get type issues trying to use a route component in a route stub.
|
||||
|
||||
```tsx filename=routes/login.tsx
|
||||
export default function Login({
|
||||
actionData,
|
||||
}: Route.ComponentProps) {
|
||||
return <Form method="post">...</Form>;
|
||||
}
|
||||
```
|
||||
|
||||
```tsx filename=routes/login.test.tsx
|
||||
import LoginRoute from "./login";
|
||||
|
||||
test("LoginRoute renders error messages", async () => {
|
||||
const Stub = createRoutesStub([
|
||||
{
|
||||
path: "/login",
|
||||
Component: LoginRoute,
|
||||
// ^ ❌ Types of property 'matches' are incompatible.
|
||||
action() {
|
||||
/*...*/
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
These type errors are generally accurate if you try to setup your tests like this. As long as your stubbed `loader`/`action` functions match your real implementations, then the types for `loaderData`/`actionData` will be correct, but if they differ your types will be lying to you.
|
||||
|
||||
`matches` is more complicated since you don't usually stub out all of the ancestor routes. In this example, there is no `root` route so `matches` will only contain your test route, while it will contain the root route and any other ancestors at runtime. There's no great way to automatically align the typegen types with the runtime types in your test.
|
||||
|
||||
Therefore, if you need to test Route level components, we recommend you do that via an Integration/E2E test (Playwright, Cypress, etc.) against a running application because you're venturing out of unit testing territory when testing your route as a whole.
|
||||
|
||||
If you _need_ to write a unit test against the route, you can add a `@ts-expect-error` comment in your test to silence the TypeScript error:
|
||||
|
||||
```tsx
|
||||
const Stub = createRoutesStub([
|
||||
{
|
||||
path: "/login",
|
||||
// @ts-expect-error: `matches` won't align between test code and app code
|
||||
Component: LoginRoute,
|
||||
action() {
|
||||
/*...*/
|
||||
},
|
||||
},
|
||||
]);
|
||||
```
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: Getting Started
|
||||
order: 1
|
||||
---
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
---
|
||||
title: Picking a Mode
|
||||
order: 1
|
||||
---
|
||||
|
||||
# Picking a Mode
|
||||
|
||||
React Router is a multi-strategy router for React. There are three primary ways, or "modes", to use it in your app. Across the docs you'll see these icons indicating which mode the content is relevant to:
|
||||
|
||||
[MODES: framework, data, declarative]
|
||||
|
||||
<p></p>
|
||||
|
||||
The features available in each mode are additive, so moving from Declarative to Data to Framework simply adds more features at the cost of architectural control. So pick your mode based on how much control or how much help you want from React Router.
|
||||
|
||||
The mode depends on which "top level" router API you're using:
|
||||
|
||||
## Declarative
|
||||
|
||||
Declarative mode enables basic routing features like matching URLs to components, navigating around the app, and providing active states with APIs like `<Link>`, `useNavigate`, and `useLocation`.
|
||||
|
||||
```tsx
|
||||
import { BrowserRouter } from "react-router";
|
||||
|
||||
ReactDOM.createRoot(root).render(
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>,
|
||||
);
|
||||
```
|
||||
|
||||
## Data
|
||||
|
||||
By moving route configuration outside of React rendering, Data Mode adds data loading, actions, pending states and more with APIs like `loader`, `action`, and `useFetcher`.
|
||||
|
||||
```tsx
|
||||
import {
|
||||
createBrowserRouter,
|
||||
RouterProvider,
|
||||
} from "react-router";
|
||||
|
||||
let router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
Component: Root,
|
||||
loader: loadRootData,
|
||||
},
|
||||
]);
|
||||
|
||||
ReactDOM.createRoot(root).render(
|
||||
<RouterProvider router={router} />,
|
||||
);
|
||||
```
|
||||
|
||||
## Framework
|
||||
|
||||
Framework Mode wraps Data Mode with a Vite plugin to add the full React Router experience with:
|
||||
|
||||
- type-safe `href`
|
||||
- type-safe Route Module API
|
||||
- intelligent code splitting
|
||||
- SPA, SSR, and static rendering strategies
|
||||
- and more
|
||||
|
||||
```ts filename=routes.ts
|
||||
import { index, route } from "@react-router/dev/routes";
|
||||
|
||||
export default [
|
||||
index("./home.tsx"),
|
||||
route("products/:pid", "./product.tsx"),
|
||||
];
|
||||
```
|
||||
|
||||
You'll then have access to the Route Module API with type-safe params, loaderData, code splitting, SPA/SSR/SSG strategies, and more.
|
||||
|
||||
```ts filename=product.tsx
|
||||
import { Route } from "./+types/product.tsx";
|
||||
|
||||
export async function loader({ params }: Route.LoaderArgs) {
|
||||
let product = await getProduct(params.pid);
|
||||
return { product };
|
||||
}
|
||||
|
||||
export default function Product({
|
||||
loaderData,
|
||||
}: Route.ComponentProps) {
|
||||
return <div>{loaderData.product.name}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
## Decision Advice
|
||||
|
||||
Every mode supports any architecture and deployment target, so the question isn't really about if you want SSR, SPA, etc. It's about how much you want to do yourself.
|
||||
|
||||
**Use Framework Mode if you:**
|
||||
|
||||
- are too new to have an opinion
|
||||
- are considering Next.js, Solid Start, SvelteKit, Astro, TanStack Start, etc. and want to compare
|
||||
- just want to build something with React
|
||||
- might want to server render, might not
|
||||
- are coming from Remix (React Router v7 is the "next version" after Remix v2)
|
||||
- are migrating from Next.js
|
||||
|
||||
[→ Get Started with Framework Mode](./framework/installation).
|
||||
|
||||
**Use Data Mode if you:**
|
||||
|
||||
- want data features but also want to have control over bundling, data, and server abstractions
|
||||
- started a data router in v6.4 and are happy with it
|
||||
|
||||
[→ Get Started with Data Mode](./data/custom).
|
||||
|
||||
**Use Declarative Mode if you:**
|
||||
|
||||
- want to use React Router as simply as possible
|
||||
- are coming from v6 and are happy with the `<BrowserRouter>`
|
||||
- have a data layer that either skips pending states (like local first, background data replication/sync) or has its own abstractions for them
|
||||
- are coming from Create React App (you may want to consider framework mode though)
|
||||
|
||||
[→ Get Started with Declarative Mode](./declarative/installation).
|
||||
|
||||
## API + Mode Availability Table
|
||||
|
||||
This is mostly for the LLMs, but knock yourself out:
|
||||
|
||||
| API | Framework | Data | Declarative |
|
||||
| ------------------------------ | --------- | ---- | ----------- |
|
||||
| Await | ✅ | ✅ | |
|
||||
| Form | ✅ | ✅ |
|
||||
| Link | ✅ | ✅ | ✅ |
|
||||
| `<Link discover>` | ✅ | | |
|
||||
| `<Link prefetch>` | ✅ | | |
|
||||
| `<Link preventScrollReset>` | ✅ | ✅ | |
|
||||
| Links | ✅ | | |
|
||||
| Meta | ✅ | | |
|
||||
| NavLink | ✅ | ✅ | ✅ |
|
||||
| `<NavLink discover>` | ✅ | | |
|
||||
| `<NavLink prefetch>` | ✅ | | |
|
||||
| `<NavLink preventScrollReset>` | ✅ | ✅ | |
|
||||
| NavLink `isPending` | ✅ | ✅ | |
|
||||
| Navigate | ✅ | ✅ | ✅ |
|
||||
| Outlet | ✅ | ✅ | ✅ |
|
||||
| PrefetchPageLinks | ✅ | | |
|
||||
| Route | ✅ | ✅ | ✅ |
|
||||
| Routes | ✅ | ✅ | ✅ |
|
||||
| Scripts | ✅ | | |
|
||||
| ScrollRestoration | ✅ | ✅ | |
|
||||
| ServerRouter | ✅ | | |
|
||||
| usePrompt | ✅ | ✅ | |
|
||||
| useActionData | ✅ | ✅ | |
|
||||
| useAsyncError | ✅ | ✅ | |
|
||||
| useAsyncValue | ✅ | ✅ | |
|
||||
| useBeforeUnload | ✅ | ✅ | ✅ |
|
||||
| useBlocker | ✅ | ✅ | |
|
||||
| useFetcher | ✅ | ✅ | |
|
||||
| useFetchers | ✅ | ✅ | |
|
||||
| useFormAction | ✅ | ✅ | |
|
||||
| useHref | ✅ | ✅ | ✅ |
|
||||
| useInRouterContext | ✅ | ✅ | ✅ |
|
||||
| useLinkClickHandler | ✅ | ✅ | ✅ |
|
||||
| useLoaderData | ✅ | ✅ | |
|
||||
| useLocation | ✅ | ✅ | ✅ |
|
||||
| useMatch | ✅ | ✅ | ✅ |
|
||||
| useMatches | ✅ | ✅ | |
|
||||
| useNavigate | ✅ | ✅ | ✅ |
|
||||
| useNavigation | ✅ | ✅ | |
|
||||
| useNavigationType | ✅ | ✅ | ✅ |
|
||||
| useOutlet | ✅ | ✅ | ✅ |
|
||||
| useOutletContext | ✅ | ✅ | ✅ |
|
||||
| useParams | ✅ | ✅ | ✅ |
|
||||
| useResolvedPath | ✅ | ✅ | ✅ |
|
||||
| useRevalidator | ✅ | ✅ | |
|
||||
| useRouteError | ✅ | ✅ | |
|
||||
| useRouteLoaderData | ✅ | ✅ | |
|
||||
| useRoutes | ✅ | ✅ | ✅ |
|
||||
| useSearchParams | ✅ | ✅ | ✅ |
|
||||
| useSubmit | ✅ | ✅ | |
|
||||
| useViewTransitionState | ✅ | ✅ | |
|
||||
| isCookieFunction | ✅ | ✅ | |
|
||||
| isSessionFunction | ✅ | ✅ | |
|
||||
| createCookie | ✅ | ✅ | |
|
||||
| createCookieSessionStorage | ✅ | ✅ | |
|
||||
| createMemorySessionStorage | ✅ | ✅ | |
|
||||
| createPath | ✅ | ✅ | ✅ |
|
||||
| createRoutesFromElements | | ✅ | |
|
||||
| createRoutesStub | ✅ | ✅ | |
|
||||
| createSearchParams | ✅ | ✅ | ✅ |
|
||||
| data | ✅ | ✅ | |
|
||||
| generatePath | ✅ | ✅ | ✅ |
|
||||
| href | ✅ | | |
|
||||
| isCookie | ✅ | ✅ | |
|
||||
| isRouteErrorResponse | ✅ | ✅ | |
|
||||
| isSession | ✅ | ✅ | |
|
||||
| matchPath | ✅ | ✅ | ✅ |
|
||||
| matchRoutes | ✅ | ✅ | ✅ |
|
||||
| parsePath | ✅ | ✅ | ✅ |
|
||||
| redirect | ✅ | ✅ | |
|
||||
| redirectDocument | ✅ | ✅ | |
|
||||
| renderMatches | ✅ | ✅ | ✅ |
|
||||
| replace | ✅ | ✅ | |
|
||||
| resolvePath | ✅ | ✅ | ✅ |
|
||||
Reference in New Issue
Block a user