Push V1 app

This commit is contained in:
jlacoste
2026-06-26 11:54:29 +02:00
parent 8b7caa1a5a
commit 9d1990523f
3881 changed files with 1291493 additions and 1 deletions
+138
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,4 @@
---
title: Data Mode
order: 3
---
+56
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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).