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).
|
||||
Reference in New Issue
Block a user