Skip to content

Commit

Permalink
doc: add action md
Browse files Browse the repository at this point in the history
  • Loading branch information
gongph committed Dec 7, 2024
1 parent 54728a0 commit 155d54d
Show file tree
Hide file tree
Showing 6 changed files with 473 additions and 6 deletions.
13 changes: 7 additions & 6 deletions .vitepress/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default defineConfig({
{ text: "指南", link: "/home" },
{ text: "参考", link: "https://api.reactrouter.com/v7/" },
],
outline: [2, 3],
sidebar: [
{
text: "指南",
Expand All @@ -32,12 +33,12 @@ export default defineConfig({
{ text: "路由", link: "/framework/routing" },
{ text: "路由模块", link: "/framework/route-module" },
{ text: "渲染策略", link: "/framework/rendering-strategies" },
{ text: "数据加载", link: "/framework/routing" },
{ text: "Actions", link: "/framework/routing" },
{ text: "导航", link: "/framework/routing" },
{ text: "数据加载", link: "/framework/data-loading" },
{ text: "Actions", link: "/framework/actions" },
{ text: "导航", link: "/framework/navigating" },
{
text: "加载中的用户界面",
link: "/framework/routing",
text: "待处理的UI",
link: "/framework/pending-ui",
},
{ text: "测试", link: "/framework/routing" },
{ text: "自定义框架", link: "/framework/routing" },
Expand Down Expand Up @@ -76,7 +77,7 @@ export default defineConfig({
items: [
{ text: "错误边界", link: "/framework/routing" },
{ text: "错误上报", link: "/framework/routing" },
{ text: "使用Fetchers", link: "/framework/routing" },
{ text: "使用Fetchers", link: "/how-tos/fetchers" },
{
text: "File Route 规范",
link: "/framework/routing",
Expand Down
150 changes: 150 additions & 0 deletions src/framework/actions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Actions

数据变更通过路由动作(Route actions)来完成。当路由动作执行完毕后,页面上所有由加载器获取的数据都会被重新验证(revalidated),这样就能在无需编写额外代码的情况下,确保用户界面与数据保持同步。

通过 `action` 定义的路由动作只会在服务器端被调用,而通过 `clientAction` 定义的路由动作则是在浏览器(也就是客户端)中运行。

## 客户端 Actions

`clientAction` 仅在浏览器端运行,并且当同时定义了 action 和 clientAction 时,clientAction 会优先被执行。

::: code-group

```tsx [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 = await 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>
);
}
```

:::

## 服务端 Actions

`action` 仅在服务器端运行,并且会从客户端的打包代码包中被移除。

::: code-group

```tsx [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 = await 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>
);
}
```

:::

## 调用 Actions 的方式

Actions 可以通过声明式 `<Form>` 组件和钩子函数 `useSubmit`(或者使用 `<fetcher.Form>``fetcher.submit`)被调用,使用时传递一个路由 path 和一个 “post” 方法:

### 使用 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>
);
}
```

这种方式会造成一次浏览器导航,并且会在浏览器的历史记录中添加一个新的记录。

### 使用 useSubmit 调用

你可以通过使用 `useSubmit` 钩子函数来调用 action:

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

同样,这种方式也会造成一次浏览器导航,并且会在浏览器的历史记录中添加一个新的记录。

### 使用 fetcher 调用

Fetchers 能够让你向 Actions 以及 Loaders 提交数据,同时不会引发导航行为,也就是说不会在浏览器历史记录中添加新的记录。

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

它也有一个编程式 `submit` 方法:

```ts
fetcher.submit(
{ title: "New Title" },
{ action: "/update-task/123", method: "post" }
);
```

阅读 [使用 Fetchers](../how-tos/fetchers.md) 以了解更多关于 Fetchers 的信息。
146 changes: 146 additions & 0 deletions src/framework/data-loading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# 数据加载

数据是通过加载器(`loader`)和客户端加载器(`clientLoader`)提供给路由组件的。

加载器数据会自动从加载器中进行序列化操作,然后在组件中进行反序列化。除了像字符串、数字这类基本数据类型的值以外,加载器还能够返回 promises、maps、sets、dates 等多种类型的数据。

## 客户端数据加载

客户端加载器(`clientLoader`)用于在客户端(也就是浏览器端)获取数据。对于那些你更倾向于仅从浏览器去获取数据的页面或者整个项目而言,它是非常有用的。

::: code-group

```tsx [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;
}

export default function Product({ loaderData }: Route.ComponentProps) {
const { name, description } = loaderData;
return (
<div>
<h1>{name}</h1>
<p>{description}</p>
</div>
);
}
```

:::

## 服务器端数据加载

在服务器端渲染的情况下,加载器(`loader`)既用于初始页面加载,也用于客户端导航(client navigations)。在客户端导航期间,加载器会通过 React Router 自动从浏览器向服务器发起获取数据的 `fetch` 请求。

::: code-group

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

:::

需要注意的是,`loader` 函数会从客户端的打包代码包(bundles)中移除,这样一来,你就能够使用仅在服务器端可用的应用程序接口(APIs),而无需担心它们会被包含在浏览器端的代码中。

## 静态数据加载

在进行预渲染时,加载器(loaders)会被用于在生产构建阶段获取数据。

::: code-group

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

:::

`react-router.config.ts` 文件中指定需要预渲染的 URL:

::: code-group

```ts [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;
```

:::

需要注意的是,在服务器端渲染时,任何未进行预渲染的 URL 对应的页面将会像往常一样进行服务器端渲染。这一机制使得你能够针对单个路由预渲染部分数据,同时对其余部分仍采用服务器端渲染的方式来处理。

## 同时使用两种加载器

加载器(`loader`)与客户端加载器(`clientLoader`)是可以一起使用的。加载器会在服务器端用于初始的服务器端渲染(Server Side Rendering,简称 SSR)或者静态预渲染(Pre-rendering)操作,而 `clientLoader` 则会在后续的客户端导航(client-side navigations)过程中被使用。

::: code-group

```tsx [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({ params }: Route.ClientLoader) {
const res = await fetch(`/api/products/${params.pid}`);
return res.json();
}

export default function Product({ loaderData }: Route.ComponentProps) {
const { name, description } = loaderData;

return (
<div>
<h1>{name}</h1>
<p>{description}</p>
</div>
);
}
```

:::
Loading

0 comments on commit 155d54d

Please sign in to comment.