├── .gitattributes
├── .github
└── workflows
│ └── deploy.yml
├── .gitignore
├── LICENSE
├── README.md
├── docs
└── .vitepress
│ ├── config.mts
│ └── theme
│ ├── custom.css
│ └── index.mts
├── package.json
├── pnpm-lock.yaml
└── src
├── components
├── await.md
├── form.md
├── link-native.md
├── link.md
├── nav-link.md
├── navigate.md
├── outlet.md
├── route.md
├── routes.md
└── scroll-restoration.md
├── fetch
├── json.md
├── redirect-document.md
└── redirect.md
├── guides
├── api-development-strategy.md
├── contributing.md
├── data-libs.md
├── deferred.md
├── form-data.md
├── index-search-param.md
└── ssr.md
├── hooks
├── use-action-data.md
├── use-async-error.md
├── use-async-value.md
├── use-before-unload.md
├── use-blocker.md
├── use-fetcher.md
├── use-fetchers.md
├── use-form-action.md
├── use-href.md
├── use-in-router-context.md
├── use-link-click-handler.md
├── use-link-press-handler.md
├── use-loader-data.md
├── use-location.md
├── use-match.md
├── use-matches.md
├── use-navigate.md
├── use-navigation-type.md
├── use-navigation.md
├── use-outlet-context.md
├── use-outlet.md
├── use-params.md
├── use-prompt.md
├── use-resolved-path.md
├── use-revalidator.md
├── use-route-error.md
├── use-route-loader-data.md
├── use-routes.md
├── use-search-params-rn.md
├── use-search-params.md
├── use-submit.md
└── use-view-transition-state.md
├── index.md
├── public
├── favicon.ico
├── logo.svg
└── logo_dark.svg
├── route
├── action.md
├── error-element.md
├── hydrate-fallback-element.md
├── lazy.md
├── loader.md
├── route.md
└── should-revalidate.md
├── router-components
├── browser-router.md
├── hash-router.md
├── memory-router.md
├── native-router.md
├── router.md
└── static-router.md
├── routers
├── create-browser-router.md
├── create-hash-router.md
├── create-memory-router.md
├── create-static-handler.md
├── create-static-router.md
├── picking-a-router.md
├── router-provider.md
└── static-router-provider.md
├── start
├── concepts.md
├── faqs.md
├── overview.md
└── tutorial.md
├── upgrading
├── reach.md
├── v5.md
└── v6-data.md
└── utils
├── create-routes-from-children.md
├── create-routes-from-elements.md
├── create-search-params.md
├── defer.md
├── generate-path.md
├── is-route-error-response.md
├── location.md
├── match-path.md
├── match-routes.md
├── render-matches.md
└── resolve-path.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.md linguist-language=TypeScript
2 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: deploy
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | push-to-gh-pages:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout
13 | uses: actions/checkout@v3
14 | with:
15 | persist-credentials: false
16 |
17 | - name: Install pnpm
18 | uses: pnpm/action-setup@v2
19 | with:
20 | version: 8
21 | run_install: false
22 |
23 | - name: use Node.js
24 | uses: actions/setup-node@v3
25 | with:
26 | node-version: '20.x'
27 |
28 | - name: Build
29 | env:
30 | NODE_OPTIONS: '--max_old_space_size=4096'
31 | run: |
32 | pnpm install
33 | pnpm run docs:build
34 |
35 | - name: Deploy
36 | uses: JamesIves/github-pages-deploy-action@v4.3.3
37 | with:
38 | ACCESS_TOKEN: ${{ secrets.DEPLOY_KEY }}
39 | BRANCH: gh-pages
40 | FOLDER: ./dist
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | node_modules/
3 | docs/.vitepress/cache/
4 |
5 | .git
6 | .idea
7 | .vscode
8 | .DS_Store
9 |
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 baimingxuan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
React Router6 中文文档(v6.21.1)
3 |
文档内容详细翻译自官方英文文档
4 |
7 |
8 |
9 |
10 | ### 开发
11 |
12 | ```sh
13 | # 克隆项目
14 | git clone https://github.com/baimingxuan/react-router6-doc.git
15 |
16 | # 安装依赖
17 | pnpm install
18 |
19 | # 开发
20 | pnpm run docs:dev
21 | ```
22 |
23 | 项目基于 [vitepress](https://github.com/vuejs/vitepress)
24 |
25 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/custom.css:
--------------------------------------------------------------------------------
1 | .logo {
2 | margin-right: 0 !important;
3 | }
--------------------------------------------------------------------------------
/docs/.vitepress/theme/index.mts:
--------------------------------------------------------------------------------
1 | import DefaultTheme from 'vitepress/theme'
2 | import './custom.css'
3 |
4 | export default DefaultTheme
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-router6-doc",
3 | "version": "1.0.0",
4 | "description": "react router v6.21.1 chinese document",
5 | "repository": "https://github.com/baimingxuan/react-router6-doc.git",
6 | "author": "baimingxuan",
7 | "license": "MIT License",
8 | "scripts": {
9 | "docs:dev": "vitepress dev docs --host",
10 | "docs:build": "vitepress build docs",
11 | "docs:preview": "vitepress preview docs"
12 | },
13 | "devDependencies": {
14 | "vitepress": "1.0.0-rc.24"
15 | }
16 | }
--------------------------------------------------------------------------------
/src/components/await.md:
--------------------------------------------------------------------------------
1 | # ``
2 |
3 | 用于呈现具有自动错误处理的功能[延迟值](../utils/defer)。请务必查看[延迟数据指南](../guides/deferred),因为有一些 API 与此组件配合使用。
4 |
5 | ```jsx
6 | import { Await, useLoaderData } from "react-router-dom";
7 |
8 | function Book() {
9 | const { book, reviews } = useLoaderData();
10 | return (
11 |
12 |
{book.title}
13 |
{book.description}
14 |
}>
15 |
Could not load reviews 😬
19 | }
20 | children={(resolvedReviews) => (
21 |
22 | )}
23 | />
24 |
25 |
26 | );
27 | }
28 | ```
29 |
30 | **注意:** `` 期望在 `` 或 `` 父级中呈现,以启用回退用户界面。
31 |
32 | ## 类型声明
33 |
34 | ```ts
35 | declare function Await(
36 | props: AwaitProps
37 | ): React.ReactElement;
38 |
39 | interface AwaitProps {
40 | children: React.ReactNode | AwaitResolveRenderFunction;
41 | errorElement?: React.ReactNode;
42 | resolve: TrackedPromise | any;
43 | }
44 |
45 | interface AwaitResolveRenderFunction {
46 | (data: Awaited): React.ReactElement;
47 | }
48 | ```
49 |
50 | ## `children`
51 |
52 | 可以是React元素或函数。
53 |
54 | 使用函数时,值是唯一的参数。
55 |
56 | ```jsx
57 |
58 | {(resolvedReviews) => }
59 |
60 | ```
61 |
62 | 使用React元素时,[`useAsyncValue`](../hooks/use-async-value)将提供数据:
63 |
64 | ```jsx
65 |
66 |
67 | ;
68 |
69 | function Reviews() {
70 | const resolvedReviews = useAsyncValue();
71 | return {/* ... */}
;
72 | }
73 | ```
74 |
75 | ## `errorElement`
76 |
77 | 当Promise被拒绝时,错误元素会渲染,而不是子元素。您可以通过[`useAsyncError`](../hooks/use-async-error)访问错误。
78 |
79 | 如果Promise被拒绝,您可以提供一个可选的 `errorElement` ,通过 `useAsyncError` 钩子在上下文用户界面中处理该错误。
80 |
81 | ```jsx
82 | }
85 | >
86 |
87 | ;
88 |
89 | function ReviewsError() {
90 | const error = useAsyncError();
91 | return {error.message}
;
92 | }
93 | ```
94 |
95 | 如果不提供 errorElement,被拒绝的值将上升到最近的路由级[`errorElement`](../route/error-element),并可通过[`useRouteError`](../hooks/use-route-error)钩子访问。
96 |
97 | ## `resolve`
98 |
99 | 获取从[延迟](../utils/defer) [加载器](../route/loader)返回的 `promise`值,并进行解析和渲染。
100 |
101 | ```jsx
102 | import {
103 | defer,
104 | Route,
105 | useLoaderData,
106 | Await,
107 | } from "react-router-dom";
108 |
109 | // given this route
110 | {
112 | let book = await getBook();
113 | let reviews = getReviews(); // not awaited
114 | return defer({
115 | book,
116 | reviews, // this is a promise
117 | });
118 | }}
119 | element={}
120 | />;
121 |
122 | function Book() {
123 | const {
124 | book,
125 | reviews, // this is the same promise
126 | } = useLoaderData();
127 | return (
128 |
129 |
{book.title}
130 |
{book.description}
131 |
}>
132 |
136 |
137 |
138 |
139 |
140 | );
141 | }
142 | ```
--------------------------------------------------------------------------------
/src/components/form.md:
--------------------------------------------------------------------------------
1 | # `
88 | ;
89 | ```
90 |
91 | 如果当前 URL 是 `"/projects/123"` ,那么子路由 `ProjectsPage` 内的表单就会如你所想的那样有一个默认操作: `"/projects/123"` 。在这种情况下,当路由是最深匹配路由时, `
101 | ```
102 |
103 | **另请参阅:**
104 |
105 | - [索引搜索参数](../guides/index-search-param)(索引与父路由消歧对比)
106 |
107 | > NOTE
108 | >
109 | > 请参阅 `useResolvedPath` 文档中的 [Splat Paths](../hooks/use-resolved-path#splat-paths) 部分,了解 `future.v7_relativeSplatPath` future 标志在 `splat` 路由中相对 `useNavigate()` 的行为。
110 |
111 | ## `method`
112 |
113 | 这决定了要使用的[HTTP verb](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods)。与纯 HTML[表单方法](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-method)相似,除 "get "和 "post "外,它还支持 "put"、"patch "和 "delete"。默认为 "get"。
114 |
115 | ### GET提交
116 |
117 | 默认方法为“get”。Get 提交*不会调用`action`*。Get 提交与普通导航(用户点击链接)相同,只是用户可以提供搜索参数,从表单进入 URL。
118 |
119 | ```jsx
120 |
128 | ```
129 |
130 | 假设用户键入“running shoes”并提交表单。React Router 模拟浏览器,将表单序列化为 [URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams),然后将用户导航到 `"/products?q=running+shoes"` 。这就好比你以开发者的身份渲染了一个 `` ,而不是让用户动态提供查询字符串。
131 |
132 | 路由`loader`访问这些值最方便的方法是从 `request.url` 创建一个新的[`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL),然后加载数据。
133 |
134 | ```jsx
135 | {
138 | let url = new URL(request.url);
139 | let searchTerm = url.searchParams.get("q");
140 | return fakeSearchProducts(searchTerm);
141 | }}
142 | />
143 | ```
144 |
145 | ### 突变提交
146 |
147 | 所有其他方法都是 "突变提交",这意味着您打算通过 POST、PUT、PATCH 或 DELETE 来更改数据。请注意,纯 HTML 表单只支持 "post "和 "get",我们也倾向于使用这两种方法。
148 |
149 | 当用户提交表单时,React Router 会将 `action` 与应用程序的路由相匹配,并调用带有序列化[`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)的 `` 。操作完成后,页面上的所有`loader`数据将自动重新验证,以保持用户界面与数据同步。
150 |
151 | 该方法可在[`request.method`](https://developer.mozilla.org/en-US/docs/Web/API/Request/method)上调用路由操作。您可以使用该方法向您的数据抽象指示提交的意图。
152 |
153 | ```jsx
154 | }
157 | loader={async ({ params }) => {
158 | return fakeLoadProject(params.id);
159 | }}
160 | action={async ({ request, params }) => {
161 | switch (request.method) {
162 | case "PUT": {
163 | let formData = await request.formData();
164 | let name = formData.get("projectName");
165 | return fakeUpdateProject(name);
166 | }
167 | case "DELETE": {
168 | return fakeDeleteProject(params.id);
169 | }
170 | default: {
171 | throw new Response("", { status: 405 });
172 | }
173 | }
174 | }}
175 | />;
176 |
177 | function Project() {
178 | let project = useLoaderData();
179 |
180 | return (
181 | <>
182 |
190 |
191 |
194 | >
195 | );
196 | }
197 | ```
198 |
199 | 正如您所看到的,两个表单提交到相同的路径,但您可以使用 `request.method` 来分支您打算做的事情。操作完成后, `loader` 将重新验证,用户界面将自动与新数据同步。
200 |
201 | ## `navigate`
202 |
203 | 您可以指定 `
40 |
41 | );
42 | }
43 | ```
--------------------------------------------------------------------------------
/src/components/route.md:
--------------------------------------------------------------------------------
1 | # 路由 API
2 |
3 | 由于 `` 的应用程序接口和用例包括数据加载、突变等,因此 `` 有自己的文档类别。
4 |
5 | 请参阅:
6 |
7 | - [``](../route/route)
8 | - [`loader`](../route/loader)
9 | - [`action`](../route/action)
10 | - [`errorElement`](../route/error-element)
11 | - [`shouldRevalidate`](../route/should-revalidate)
--------------------------------------------------------------------------------
/src/components/routes.md:
--------------------------------------------------------------------------------
1 | # ``
2 |
3 | 在应用程序中的任何地方, `` 都会匹配当前[位置](../utils/location)的一组子路由。
4 |
5 | ```tsx
6 | interface RoutesProps {
7 | children?: React.ReactNode;
8 | location?: Partial | string;
9 | }
10 |
11 |
12 |
13 | ;
14 | ```
15 |
16 | > NOTE
17 | >
18 | > 如果您使用的是[`createBrowserRouter`](../routers/create-browser-router)这样的数据路由器,使用该组件的情况并不常见,因为作为 `` 树的后代的一部分定义的路由无法利用[`RouterProvider`](../routers/router-provider)应用程序可用的[数据 API](../routers/picking-a-router#data-apis)。[在迁移过程中](../upgrading/v6-data),您可以也应该在 `RouterProvider` 应用程序中使用该组件。
19 |
20 | 每当位置发生变化时, `` 就会查看其所有子路由,找出最匹配的路由,并渲染用户界面的该分支。 `` 元素可以嵌套,以表示嵌套的用户界面,这也与嵌套的 URL 路径相对应。父路由通过呈现 [`
24 | }>
25 | }
28 | />
29 | } />
30 |
31 | } />
32 |
33 | ```
--------------------------------------------------------------------------------
/src/components/scroll-restoration.md:
--------------------------------------------------------------------------------
1 | # ``
2 |
3 | 该组件将在加载程序完成后,模拟浏览器在位置更改时的滚动恢复功能,以确保滚动位置恢复到正确位置,甚至跨域滚动。
4 |
5 | > IMPORTANT
6 | >
7 | > 此功能只有在使用数据路由器时才有效,请参阅["选择路由"](../routers/picking-a-router)
8 |
9 | 只需呈现其中一个,建议在应用程序的根路由中呈现:
10 |
11 | ```jsx
12 | import { ScrollRestoration } from "react-router-dom";
13 |
14 | function RootRouteComponent() {
15 | return (
16 |
17 | {/* ... */}
18 |
19 |
20 | );
21 | }
22 | ```
23 |
24 | ## `getKey`
25 |
26 | 可选属性,用于定义 React Router 恢复滚动位置时应使用的键。
27 |
28 | ```jsx
29 | {
31 | // default behavior
32 | return location.key;
33 | }}
34 | />
35 | ```
36 |
37 | 默认情况下,它使用 `location.key` ,在没有客户端路由的情况下模拟浏览器的默认行为。用户可以在堆栈中多次导航到相同的 URL,每个条目都有自己的滚动位置来还原。
38 |
39 | 有些应用可能希望覆盖这一行为,并根据其他内容恢复位置。例如,一个社交应用程序有四个主要页面:
40 |
41 | - "/home"
42 | - "/messages"
43 | - "/notifications"
44 | - "/search"
45 |
46 | 如果用户从"/home "开始,向下滚动一点,点击导航菜单中的 "信息",然后点击导航菜单中的 "主页"(而不是返回按钮!),历史堆栈中就会出现三个条目:
47 |
48 | ```js
49 | 1. /home
50 | 2. /messages
51 | 3. /home
52 | ```
53 |
54 | 默认情况下,React Router(和浏览器)会为 `1` 和 `3` 存储两个不同的滚动位置,即使它们的 URL 相同。这意味着当用户从 `2` → `3` 浏览时,滚动位置会移到顶部,而不是恢复到 `1` 中的位置。
55 |
56 | 这里一个可靠的产品决策是,无论用户如何到达(返回按钮或新链接点击),都要保持他们在主页上的滚动位置。为此,您需要使用 `location.pathname` 作为关键字。
57 |
58 | ```jsx
59 | {
61 | return location.pathname;
62 | }}
63 | />
64 | ```
65 |
66 | 或者,您可能只想对某些路径使用路径名,而对其他路径使用正常行为:
67 |
68 | ```jsx
69 | {
71 | const paths = ["/home", "/notifications"];
72 | return paths.includes(location.pathname)
73 | ? // home and notifications restore by pathname
74 | location.pathname
75 | : // everything else by location like the browser
76 | location.key;
77 | }}
78 | />
79 | ```
80 |
81 | ## 防止滚动重置
82 |
83 | 当导航创建新的滚动键时,滚动位置会重置为页面顶部。您可以防止链接和表单出现 "滚动到顶部 "行为:
84 |
85 | ```jsx
86 |
87 |
88 | ```
89 |
90 | 另请参阅:[``](../components/link#preventscrollreset),[``](../components/form#preventscrollreset)
91 |
92 | ## 滚动闪烁
93 |
94 | 如果没有 [Remix](https://remix.run/) 这样的服务器端渲染框架,在初始页面加载时可能会出现一些滚动闪烁。这是因为 React Router 无法还原滚动位置,直到您的 JS 捆绑包下载完毕、数据加载完毕、整个页面渲染完毕(如果您正在渲染一个旋转器,视口很可能不是保存滚动位置时的大小)。
95 |
96 | 服务器渲染框架可以防止滚动闪烁,因为它们可以在首次加载时发送一个完整的文档,因此可以在页面首次渲染时恢复滚动。
--------------------------------------------------------------------------------
/src/fetch/json.md:
--------------------------------------------------------------------------------
1 | # `json`
2 |
3 | 快捷方式:
4 |
5 | ```jsx
6 | new Response(JSON.stringify(someValue), {
7 | headers: {
8 | "Content-Type": "application/json; utf-8",
9 | },
10 | });
11 | ```
12 |
13 | 通常用于`loader`:
14 |
15 | ```jsx
16 | import { json } from "react-router-dom";
17 |
18 | const loader = async () => {
19 | const data = getSomeData();
20 | return json(data);
21 | };
22 | ```
23 |
24 | 另请参阅:
25 |
26 | - [从`loader`返回响应](../route/loader#returning-responses)
--------------------------------------------------------------------------------
/src/fetch/redirect-document.md:
--------------------------------------------------------------------------------
1 | # `redirectDocument`
2 |
3 | 这是[`redirect`](../fetch/redirect) 的一个小封装,它将触发文档级重定向到新位置,而不是客户端导航。
4 |
5 | 当您的 React Router 应用程序与同一域名上的另一个应用程序相邻,并且需要通过 `window.location` 而不是 React Router 导航从 React Router 应用程序重定向到另一个应用程序时,这一点最为有用:
6 |
7 | ```jsx
8 | import { redirectDocument } from "react-router-dom";
9 |
10 | const loader = async () => {
11 | const user = await getUser();
12 | if (!user) {
13 | return redirectDocument("/otherapp/login");
14 | }
15 | return null;
16 | };
17 | ```
18 |
19 | ## 类型声明
20 |
21 | ```ts
22 | type RedirectFunction = (
23 | url: string,
24 | init?: number | ResponseInit
25 | ) => Response;
26 | ```
27 |
28 | ## `url`
29 |
30 | 重定向到的 URL。
31 |
32 | ```jsx
33 | redirectDocument("/otherapp/login");
34 | ```
35 |
36 | ## `init`
37 |
38 | 要在响应中使用的[响应](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response)选项。
--------------------------------------------------------------------------------
/src/fetch/redirect.md:
--------------------------------------------------------------------------------
1 | # `redirect`
2 |
3 | 由于可以在`loaders`和`actions`中返回或抛出响应,因此可以使用 `redirect` 重定向到另一个路由。
4 |
5 | ```jsx
6 | import { redirect } from "react-router-dom";
7 |
8 | const loader = async () => {
9 | const user = await getUser();
10 | if (!user) {
11 | return redirect("/login");
12 | }
13 | return null;
14 | };
15 | ```
16 |
17 | 这其实只是一个快捷方式:
18 |
19 | ```jsx
20 | new Response("", {
21 | status: 302,
22 | headers: {
23 | Location: someUrl,
24 | },
25 | });
26 | ```
27 |
28 | 当重定向是为了响应数据时,建议在加载器和操作中使用 `redirect` ,而不是在组件中使用 `useNavigate` 。
29 |
30 | 另请参阅:
31 |
32 | - [从`loader`返回响应](../route/loader#returning-responses)
33 |
34 | ## 类型声明
35 |
36 | ```ts
37 | type RedirectFunction = (
38 | url: string,
39 | init?: number | ResponseInit
40 | ) => Response;
41 | ```
42 |
43 | ## `url`
44 |
45 | 重定向到的 URL。
46 |
47 | ```jsx
48 | redirect("/login");
49 | ```
50 |
51 | ## `init`
52 |
53 | 要在响应中使用的[响应](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response)选项。
--------------------------------------------------------------------------------
/src/guides/api-development-strategy.md:
--------------------------------------------------------------------------------
1 | # API开发战略
2 |
3 | 让我们直奔主题--主要版本升级可能会很麻烦。尤其是像框架或路由器这样的基础应用。对于 Remix 和 React Router,我们希望尽最大努力为您提供最顺畅的升级体验。
4 |
5 | > NOTE
6 | >
7 | > 我们的 "[Future Flags](https://remix.run/blog/future-flags) "博文对这一策略进行了更详细的讨论,如果你想了解更多信息,请在本文档末尾阅读该博文!
8 |
9 | ## 目标
10 |
11 | 我们发布 Remix 和 React Router 的主要目标是
12 |
13 | - 开发人员可以在SemVer-major功能*发布*时逐一选择采用这些功能,而不必等到NPM推出新的主要版本时才一次性全部采用。
14 | - 在提前选择功能后,开发人员只需在一个短命分支/提交中(几小时,而不是几周)就能升级到新的主要版本
15 |
16 | ## 实施情况
17 |
18 | 我们计划通过我们称之为 "Future Flags" 的方式来实现这一目标,您将在初始化[数据路由](../routers/picking-a-router)时提供这些标志。可以将其视为未来功能的功能标志。当我们实现新功能时,我们总是尝试以一种向后兼容的方式来实现它们。但是,当需要进行突破性更改时,我们不会将该功能放在 v7 版本的最终版本中。相反,我们会添加一个"Future Flags",并在 v6 小版本中与当前行为一起实现新功能。这样,用户就可以立即开始使用该功能、提供反馈并报告错误。
19 |
20 | 这样一来,用户不仅可以逐步采用新功能(而且无需大幅提升版本),我们还能在 *v7 版本发布前*逐步解决所有问题。最后,我们还会在 v6 版本中添加弃用警告,提示用户使用新行为。如果在 v6 发布时,应用程序已经选择了所有`future flags`并更新了代码,那么他们只需将其依赖关系更新到 v7,删除`future flags`,然后在几分钟内就能在 v7 上运行。
21 |
22 | ## 不稳定与 V7 标志
23 |
24 | `future flags`有两种形式:
25 |
26 | **`future.unstable_feature`**
27 |
28 | `unstable_` 标记允许我们与早期用户一起迭代 API,就像我们在`v0.x.x`版本中一样,但针对的是特定功能。这避免了为所有用户反复修改 API,从而在最终版本中获得更好的 API。这并不意味着我们认为该功能漏洞百出!我们绝对希望早期用户能够开始使用这些功能,这样我们才能对 API 进行迭代(和/或获得信心)。
29 |
30 | **`future.v7_feature`**
31 |
32 | `v7_` 表示对 v6 行为进行了突破性更改,并意味着:(1)API 被认为是稳定的,不会再有任何突破性更改;(2)API 将成为 v7 的默认行为。` v7_`标志并不意味着该功能没有错误--任何软件都是如此!我们建议您在有时间的情况下升级到 v7 标志,因为这将使您的 v7 升级更加顺利。
33 |
34 | ### 新功能流程示例
35 |
36 | 新功能的决策流程是这样的(注意,此图与 Remix v1/v2 有关,但也适用于 React Router v6/v7):
37 |
38 | 
39 |
40 | 因此,生命周期要么是:
41 |
42 | - 非破坏性 + 稳定的应用程序接口功能 -> 在 v6 中落地
43 | - 非断裂 + 不稳定 API -> `future.unstable_` 标志 -> 在 v6 中落地
44 | - 破坏 + 稳定 API 功能 -> `future.v7_` 标志 -> 在 v7 中落地
45 | - 破坏 + 不稳定的应用程序接口 -> `future.unstable_` 标志 -> `future.v7_` 标志 -> 在 v7 中落地
46 |
47 | ## 当前的 Future Flags
48 |
49 | 以下是 React Router v6 目前的`future flags`。
50 |
51 | ### `@remix-run/router` Future Flags
52 |
53 | 这些标志仅在使用[数据路由](../routers/picking-a-router)时适用,并在创建 `router` 实例时传递:
54 |
55 | ```jsx
56 | const router = createBrowserRouter(routes, {
57 | future: {
58 | v7_normalizeFormMethod: true,
59 | },
60 | });
61 | ```
62 |
63 | | Flag | 说明 |
64 | | ------------------------------------------------------------ | ------------------------------------------------------ |
65 | | `v7_fetcherPersist` | 延迟活动的fetcher清理,直到它们返回到 `idle` 状态 |
66 | | `v7_normalizeFormMethod` | 将 `useNavigation().formMethod` 规范化为大写 HTTP 方法 |
67 | | [`v7_partialHydration`](../routers/create-browser-router#partial-hydration-data) | 支持服务端渲染应用程序的部分水合功能 |
68 | | `v7_prependBasename` | 将路由基名作为导航/获取路径的前缀 |
69 | | [`v7_relativeSplatPath`](../hooks/use-resolved-path#splat-paths) | 修复 splat 路由中相对路径解析的错误 |
70 |
71 | ### 路由 Future Flags React
72 |
73 | 这些标记同时适用于数据路由和非数据路由,并会传递给渲染的 React 组件:
74 |
75 | ```jsx
76 |
77 | {/*...*/}
78 |
79 | ```
80 |
81 | ```jsx
82 |
86 | ```
87 |
88 | | Flag | 说明 |
89 | | -------------------- | ------------------------------------------------------------ |
90 | | `v7_startTransition` | 将所有路由状态更新包裹着在[`React.startTransition`](https://react.dev/reference/react/startTransition)中 |
--------------------------------------------------------------------------------
/src/guides/contributing.md:
--------------------------------------------------------------------------------
1 | # 为 React Router做出贡献
2 |
3 | 感谢您的贡献,您太棒了!
4 |
5 | 说到开放源代码,可以做出许多不同类型的贡献,所有这些贡献都很有价值。以下是一些指导原则,希望对您准备贡献有所帮助。
6 |
7 | ## Setup
8 |
9 | 在为代码库做出贡献之前,您需要 fork 代码库。根据您的贡献类型,这一点会有所不同:
10 |
11 | - 所有新功能、错误修复或**任何涉及 `react-router` 代码**的内容都应从 `dev` 分支中分离出来,并合并到 `dev` 分支中。
12 | - 只涉及文档的改动可以从 `main` 分支出来,合并到 `main` 分支中。
13 |
14 | 以下步骤将帮助您设置好如何向该版本库贡献更改:
15 |
16 | 1. `Fork`这个`repo`(点击[本页面](https://github.com/remix-run/react-router)右上角的`Fork`按钮)
17 | 2. 克隆您的分支到本地
18 |
19 | ```bash
20 | # in a terminal, cd to parent directory where you want your clone to be, then
21 | git clone https://github.com//react-router.git
22 | cd react-router
23 |
24 | # if you are making *any* code changes, make sure to checkout the dev branch
25 | git checkout dev
26 | ```
27 |
28 | 3. 安装依赖项并构建。React Router 使用[`yarn`(版本1)](https://classic.yarnpkg.com/lang/en/docs/install),所以你也应该这样做。如果使用 `npm` 安装,则会生成不必要的 `package-lock.json` 文件。
29 |
30 | ## 您认为找到了错误?
31 |
32 | 请遵守问题模板,并通过代码示例提供明确的重现路径。最好是带有失败测试的`pull`请求。其次是 CodeSandbox 或说明错误的版本库链接。
33 |
34 | ## 添加示例?
35 |
36 | 示例可直接添加到主分支中。在本地克隆的主分支上创建一个分支。完成后,创建一个拉取请求并概述你的示例。
37 |
38 | ## 提出新的或更改后的 API?
39 |
40 | 请提供深思熟虑的评论和一些示例代码,说明您想在应用程序中使用 React Router 做什么。如果您能在得出需要更改和/或添加什么的结论之前,先向我们展示您是如何受限于当前的 API 的,这将有助于对话的进行。
41 |
42 | 我们的经验告诉我们,小的应用程序接口通常更好,因此我们可能不太愿意添加新内容,除非当前的应用程序接口存在明显的限制。尽管如此,我们还是非常希望听到一些我们以前没有考虑过的情况,所以请不要害羞! :)
43 |
44 | ## 问题没有得到关注?
45 |
46 | 如果你需要修复一个错误,但没有人在修复它,你最好的办法就是提供一个修复程序,并提出[pull 请求](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request)。开放源代码属于我们所有人,推动它向前发展是我们所有人的责任。
47 |
48 | ## 发起Pull 请求?
49 |
50 | `pull`请求只需得到两个或两个以上合作者的批准即可合并;当 PR 作者是合作者时,也算一个合作者。
51 |
52 | > IMPORTANT
53 | >
54 | > 在 GitHub 创建 PR 时,请确保将基本分支设置为正确的分支。如果您提交的 PR 涉及到任何代码,那么它应该是 `dev` 分支。在 GitHub 上创建 PR 时,可通过 "比较更改 "标题下的下拉菜单来设置基准分支:
55 |
56 | ### 测试
57 |
58 | 所有修复错误或添加功能的提交都需要测试。
59 |
60 | 不要合并未经测试的代码!
61 |
62 | ### 文档+示例
63 |
64 | 所有更改或添加 API 的提交都必须在拉取请求中完成,同时更新所有相关示例和文档。
65 |
66 | ## 开发
67 |
68 | ### Packages
69 |
70 | React Router 使用 monorepo 来托管多个软件包的代码。这些软件包位于 `packages` 目录中。
71 |
72 | 我们使用[ Yarn 工作区](https://classic.yarnpkg.com/en/docs/workspaces/)来管理依赖项的安装和各种脚本的运行。要安装所有脚本,请确保已安装[Yarn(版本1)](https://classic.yarnpkg.com/lang/en/docs/install),然后从 repo 根目录运行 `yarn` 或 `yarn install` 。
73 |
74 | ### 构建
75 |
76 | 从根目录调用 `yarn build` 将运行编译,整个过程只需几秒钟。由于 `react-router-dom` 和 `react-router-native` 都使用 `react-router` 作为依赖关系,因此必须一起构建所有软件包。
77 |
78 | ### 测试
79 |
80 | 在运行测试之前,您需要运行构建。构建后,从根目录运行 `yarn test` 将运行每个软件包的测试。如果要运行特定软件包的测试,请使用 `yarn test --projects packages/` :
81 |
82 | ```bash
83 | # Test all packages
84 | yarn test
85 |
86 | # Test only react-router-dom
87 | yarn test --projects packages/react-router-dom
88 | ```
89 |
90 | ## 仓库分支
91 |
92 | 该 repo 为不同的目的维护不同的分支。它们看起来是这样的:
93 |
94 | ```bash
95 | - main > the most recent release and current docs
96 | - dev > code under active development between stable releases
97 | - v5 > the most recent code for a specific major release
98 | ```
99 |
100 | 可能还有其他分支用于各种功能和实验,但所有神奇的事情都发生在这些分支中。
101 |
102 | ## 新版本发布
103 |
104 | 当需要发布新版本时,我们会根据不同类型的版本,按照分支策略制定相应的流程。
105 |
106 | ### `react-router@next`发布
107 |
108 | 我们根据 `dev` 分支的当前状态创建实验版本。您可以使用 `@next` 标签安装它们:
109 |
110 | ```bash
111 | yarn add react-router-dom@next
112 | # or
113 | npm install react-router-dom@next
114 | ```
115 |
116 | 当 PR 合并到 `dev` 分支时,这些版本将自动发布。
117 |
118 | ### 最新主要版本发布
119 |
120 | ```bash
121 | # Start from the dev branch.
122 | git checkout dev
123 |
124 | # Merge the main branch into dev to ensure that any hotfixes and
125 | # docs updates are available in the release.
126 | git merge main
127 |
128 | # Create a new release branch from dev.
129 | git checkout -b release/v6.1.0
130 |
131 | # Create a new tag and update version references throughout the
132 | # codebase.
133 | yarn run version minor # | "patch" | "major"
134 |
135 | # Push the release branch along with the new release tag.
136 | git push origin release/v6.1.0 --follow-tags
137 |
138 | # Wait for GitHub actions to run all tests. If the tests pass, the
139 | # release is ready to go! Merge the release branch into main and dev.
140 | git checkout main
141 | git merge release/v6.1.0
142 | git checkout dev
143 | git merge release/v6.1.0
144 |
145 | # The release branch can now be deleted.
146 | git branch -D release/v6.1.0
147 | git push origin --delete release/v6.1.0
148 |
149 | # Now go to GitHub and create the release from the new tag. Let
150 | # GitHub Actions take care of the rest!
151 | ```
152 |
153 | ### 热修复发布
154 |
155 | 有时,我们会遇到需要立即修补的关键错误。如果错误影响到最新版本,我们可以直接从 `main` (或存在错误的相关主要版本分支)创建一个新版本:
156 |
157 | ```bash
158 | # From the main branch, make sure to run the build and all tests
159 | # before creating a new release.
160 | yarn && yarn build && yarn test
161 |
162 | # Assuming the tests pass, create the release tag and update
163 | # version references throughout the codebase.
164 | yarn run version patch
165 |
166 | # Push changes along with the new release tag.
167 | git push origin main --follow-tags
168 |
169 | # In GitHub, create the release from the new tag and it will be
170 | # published via GitHub actions
171 |
172 | # When the hot-fix is done, merge the changes into dev and clean
173 | # up conflicts as needed.
174 | git checkout dev
175 | git merge main
176 | git push origin dev
177 | ```
--------------------------------------------------------------------------------
/src/guides/data-libs.md:
--------------------------------------------------------------------------------
1 | # 数据仓库集成
2 |
3 | 自 v6.4 发布以来,有些人怀疑 React Router 是否试图取代[React Query](https://tanstack.com/query/v4/)、[useSwr](https://swr.vercel.app/)等库。
4 |
5 | 答案是“不!”。
6 |
7 | React Router 的数据 API 是关于何时加载、变异和重新验证数据,而不是如何加载、变异和重新验证数据。它涉及的是数据生命周期,而不是数据获取、变异、存储和缓存的实际实现。
8 |
9 | 考虑到 `` 和 `` 都是导航事件,而且都与数据相关(显示哪些数据或更改哪些数据),因此客户端路由器可以帮助您处理这两个元素的导航状态。但实际的数据实现则取决于您。
10 |
11 | 此处的示例改编自[TkDodo 的博客](https://tkdodo.eu/blog/react-query-meets-react-router),感谢您的精彩文章!
12 |
13 | ## 加载数据
14 |
15 | 您可以在加载器中使用数据抽象,而不是在组件中加载数据。请注意,这种加载发生在 React 渲染生命周期之外,因此不能使用 React Query 的 `useQuery` 等钩子,而需要直接使用查询客户端的方法。
16 |
17 | ```jsx
18 | import { queryClient } from "./query-client";
19 |
20 | export const loader = ({ params }) => {
21 | return queryClient.fetchQuery(queryKey, queryFn, {
22 | staleTime: 10000,
23 | });
24 | };
25 | ```
26 |
27 | 如果查询客户端能正确抛出错误,那么 React Router 的[`errorElement`](../route/error-element)也能正常工作。
28 |
29 | 当然,您可以使用数据仓库的所有功能,例如缓存。缓存数据可以确保当用户点击返回按钮进入已经看过的页面时,数据会立即从缓存中加载。有时缓存是正确的选择,有时你总是希望数据是新鲜的,但这并不在 React Router 数据 API 的决定范围之内。
30 |
31 | React Router 只保留*当前页面的 `loaderData`*。如果用户点击“返回”,所有加载器都会再次被调用。如果没有 React Query 这样的数据缓存库(或在 JSON API 上添加 HTTP 缓存头以使用浏览器自己的 HTTP 缓存),您的应用将再次重新获取所有数据。
32 |
33 | 因此,React Router 与*时间*有关,而 React Query 与*缓存*有关。
34 |
35 | ## 访问组件中的数据
36 |
37 | 虽然 React Router 的 `useLoaderData` 会返回您从`loader`返回的内容,但您可以使用数据抽象的钩子来访问该包的全部功能集。
38 |
39 | ```jsx
40 | export default function SomeRouteComponent() {
41 | - const data = useLoaderData();
42 | + const { data } = useQuery(someQueryKey);
43 | }
44 | ```
45 |
46 | ## 使突变中的数据无效
47 |
48 | 由于这些库中的大多数都有某种缓存机制,因此您需要在某些时候使这些缓存失效。
49 |
50 | React Router[操作](../route/action)就是使缓存失效的最佳场所。
51 |
52 | ```jsx
53 | import { queryClient } from "./query-client";
54 |
55 | export const action = async ({ request, params }) => {
56 | const formData = await request.formData();
57 | const updates = Object.fromEntries(formData);
58 | await updateContact(params.contactId, updates);
59 | await queryClient.invalidateQueries(["contacts"]);
60 | return redirect(`/contacts/${params.contactId}`);
61 | };
62 | ```
63 |
64 | ## 使用`defer`
65 |
66 | 同样,您也可以利用延迟 API:
67 |
68 | ```jsx
69 | function loader() {
70 | return defer({
71 | // no await!
72 | someData: queryClient.fetchQuery("someKey", fn),
73 | });
74 | }
75 |
76 | function Comp() {
77 | // *do* useLoaderData for promise
78 | const { someData } = useLoaderData();
79 | return (
80 |
81 |
Something
82 |
Oops! }
85 | >
86 |
87 |
88 |
89 | );
90 | }
91 |
92 | function SomeView() {
93 | // instead of accessing with useAsyncValue
94 | // const someData = useAsyncValue();
95 | // `useQuery` as usual
96 | const { data } = useQuery("someKey");
97 | // ...
98 | }
99 | ```
100 |
101 | ## 重合部分
102 |
103 | 像 `useQuery` 这样的钩子经常返回 pending 和 error 状态,您可以使用它们来拆分您的 UI。使用 React Router,您可以将所有这些分支逻辑放在[`errorElement`](../route/error-element)、[`useNavigation`](../hooks/use-navigation)和[`Await`](../components/await)中,避免在正常路径组件中处理分支逻辑。
104 |
105 | ## 结论
106 |
107 | 有了所有这些 API 的协同工作,您现在就可以使用来自 React Router 的[`useNavigation`](../hooks/use-navigation)来构建待处理状态、优化的用户界面等。使用 React Router 为数据加载、突变和导航状态计时,然后使用 React Query 等库实际实现加载、失效、存储和缓存。
--------------------------------------------------------------------------------
/src/guides/deferred.md:
--------------------------------------------------------------------------------
1 | # 延迟数据指南
2 |
3 | ## 问题
4 |
5 | 想象一下这样一种场景:您的某个路径`loader`需要检索一些数据,而由于某种原因,检索速度相当慢。例如,您要向用户显示一个包裹的位置,该包裹正被送往用户家中:
6 |
7 | ```jsx
8 | import { json, useLoaderData } from "react-router-dom";
9 | import { getPackageLocation } from "./api/packages";
10 |
11 | async function loader({ params }) {
12 | const packageLocation = await getPackageLocation(
13 | params.packageId
14 | );
15 |
16 | return json({ packageLocation });
17 | }
18 |
19 | function PackageRoute() {
20 | const data = useLoaderData();
21 | const { packageLocation } = data;
22 |
23 | return (
24 |
25 | Let's locate your package
26 |
27 | Your package is at {packageLocation.latitude} lat
28 | and {packageLocation.longitude} long.
29 |
30 |
31 | );
32 | }
33 | ```
34 |
35 | 我们假设 `getPackageLocation` 速度较慢。这将导致初始页面加载时间和路径转换时间与最慢的数据一样长。有几种方法可以优化这种情况并改善用户体验:
36 |
37 | - 让缓慢的事情加速(😅)。
38 | - 使用 `Promise.all` 实现数据加载的并行化(在我们的示例中没有什么需要并行化的,但在其他情况下可能会有点帮助)。
39 | - 添加全局过渡旋转器(对用户体验有一定帮助)。
40 | - 添加本地化的骨架用户界面(对用户体验有一定帮助)。
41 |
42 | 如果这些方法效果不佳,那么您可能不得不将慢速数据从 `loader` 移到组件获取中(并在加载时显示骨架回退 UI)。在这种情况下,您需要在挂载时渲染后备 UI,然后启动数据获取。从 DX 的角度来看,这其实并不可怕,这要归功于[`useFetcher`](../hooks/use-fetcher)。从用户体验的角度来看,这改善了客户端转换和初始页面加载的加载体验。因此,这似乎确实解决了问题。
43 |
44 | 但由于以下两个原因,在大多数情况下(尤其是在对路由组件进行代码拆分的情况下),它仍然不是最佳选择:
45 |
46 | 1. 客户端获取将数据请求置于瀑布式流程中:文档 -> JavaScript -> 懒加载路由 -> 数据获取
47 | 2. 您的代码无法在组件获取和路由获取之间轻松切换(稍后将详细介绍)。
48 |
49 | ## 解决方案
50 |
51 | React Router 使用 [`defer`响应](../utils/defer) 实用程序和 [``](../components/await) 组件/[ `useAsyncValue`](../hooks/use-async-value) 钩子,并利用 React 18 的 Suspense 来获取数据。通过使用这些 API,您可以解决这两个问题:
52 |
53 | 1. 您的数据不再是瀑布式的:文档 -> JavaScript -> 懒加载路径和数据(并行)。
54 | 2. 您的代码可以在渲染回退和等待数据之间轻松切换
55 |
56 | 让我们深入了解一下如何做到这一点。
57 |
58 | ### 使用`defer`
59 |
60 | 首先,为您的慢速数据请求添加 `` ,在这种情况下,您更希望呈现一个回调 UI。让我们在上面的示例中这样做:
61 |
62 | ```jsx
63 | import {
64 | Await,
65 | defer,
66 | useLoaderData,
67 | } from "react-router-dom";
68 | import { getPackageLocation } from "./api/packages";
69 |
70 | async function loader({ params }) {
71 | const packageLocationPromise = getPackageLocation(
72 | params.packageId
73 | );
74 |
75 | return defer({
76 | packageLocation: packageLocationPromise,
77 | });
78 | }
79 |
80 | export default function PackageRoute() {
81 | const data = useLoaderData();
82 |
83 | return (
84 |
85 | Let's locate your package
86 | Loading package location...}
88 | >
89 | Error loading package location!
93 | }
94 | >
95 | {(packageLocation) => (
96 |
97 | Your package is at {packageLocation.latitude}{" "}
98 | lat and {packageLocation.longitude} long.
99 |
100 | )}
101 |
102 |
103 |
104 | );
105 | }
106 | ```
107 |
108 | 或者,也可以使用 `useAsyncValue` 钩子:
109 |
110 | 如果你不喜欢使用 render 属性,你可以使用一个 Hook,但你必须把代码拆分成另一个组件:
111 |
112 | ```jsx
113 | export default function PackageRoute() {
114 | const data = useLoaderData();
115 |
116 | return (
117 |
118 | Let's locate your package
119 | Loading package location...}
121 | >
122 | Error loading package location!
126 | }
127 | >
128 |
129 |
130 |
131 |
132 | );
133 | }
134 |
135 | function PackageLocation() {
136 | const packageLocation = useAsyncValue();
137 | return (
138 |
139 | Your package is at {packageLocation.latitude} lat and{" "}
140 | {packageLocation.longitude} long.
141 |
142 | );
143 | }
144 | ```
145 |
146 | ## 评估解决方案
147 |
148 | 因此,我们不会在触发获取请求前等待组件,而是在用户开始转换到新路由时,立即启动对慢速数据的请求。这可以大大加快较慢网络的用户体验。
149 |
150 | 此外,React Router 为此提供的 API 非常人性化。您可以根据是否包含 `await` 关键字,在是否延迟之间进行切换:
151 |
152 | ```jsx
153 | return defer({
154 | // not deferred:
155 | packageLocation: await packageLocationPromise,
156 | // deferred:
157 | packageLocation: packageLocationPromise,
158 | });
159 | ```
160 |
161 | 因此,您可以进行 A/B 延迟测试,甚至可以根据用户或请求的数据来决定是否延迟:
162 |
163 | ```jsx
164 | async function loader({ request, params }) {
165 | const packageLocationPromise = getPackageLocation(
166 | params.packageId
167 | );
168 | const shouldDefer = shouldDeferPackageLocation(
169 | request,
170 | params.packageId
171 | );
172 |
173 | return defer({
174 | packageLocation: shouldDefer
175 | ? packageLocationPromise
176 | : await packageLocationPromise,
177 | });
178 | }
179 | ```
180 |
181 | `shouldDeferPackageLocation` 可以用来检查提出请求的用户、软件包位置数据是否在缓存中、A/B 测试的状态或其他任何你想要的信息。这真是太贴心了 🍭
182 |
183 | ## 常问问题
184 |
185 | ### 为什么不默认推迟一切?
186 |
187 | eact Router defer API 是 React Router 提供的另一个工具,它为您提供了一种在权衡之间做出选择的好方法。你想让页面渲染得更快吗?那就延迟吧。你想要更低的 CLS(内容布局偏移)?不要延迟。你想要更快的渲染速度,但也想要更低的 CLS?那就只延迟那些慢且不重要的内容。
188 |
189 | 这都是权衡的结果,而 API 设计的精妙之处在于,它非常适合你进行简单的实验,看看哪种权衡方式能为你的真实世界关键指标带来更好的结果。
190 |
191 | ### ``回退何时渲染?
192 |
193 | `` 组件只会在初始呈现 `` 组件时,在 `` 边界上抛出`promise`,且`promise`未确定。如果属性发生变化,它不会重新渲染回调。实际上,这意味着当用户提交表单并重新验证`loader`数据时,不会呈现回调。当用户使用不同的参数导航到相同的路径时(在上述示例中,如果用户从左侧的套餐列表中选择在右侧找到自己的位置),就会呈现回调。
194 |
195 | 一开始,我们可能会觉得这与直觉相悖,但请别急,我们已经仔细考虑过这个问题,而且这种工作方式非常重要。让我们想象一下没有延迟 API 的世界。在这种情况下,您可能需要为表单提交/重新验证实现优化用户界面。
196 |
197 | 当您决定尝试 `defer` 的权衡时,我们不希望您必须更改或移除这些优化,因为我们希望您能在推迟和不推迟某些数据之间轻松切换。因此,我们确保您现有的乐观状态以同样的方式运行。如果我们不这样做,您可能会体验到我们所说的 "Popcorn UI",即提交数据会触发回调加载状态,而不是您辛苦开发的优化的用户界面。
198 |
199 | 因此,请记住这一点:**延迟 100% 只涉及路由及其参数的初始加载**。
200 |
201 | ### 为什么加载器返回的响应对象不再起作用了?
202 |
203 | 当您使用 `defer` 时,您是在告诉 React Router 立即加载页面,而不使用延迟数据。在返回 `Response` 对象之前,页面已经加载完毕,因此响应的自动处理方式与使用 `return fetch(url)` 时不同。
204 |
205 | 因此,您需要处理自己的 `Response` 进程,并使用数据而不是 `Response` 实例来解决您的延迟 Promise 问题。
206 |
207 | ```jsx
208 | async function loader({ request, params }) {
209 | return defer({
210 | // Broken! Resolves with a Response
211 | // broken: fetch(url),
212 |
213 | // Fixed! Resolves with the response data
214 | data: fetch(url).then((res) => res.json()),
215 | });
216 | }
217 | ```
218 |
219 | 或者考虑一下我们的延迟数据会返回重定向 `Response` 的情况。您可以检测重定向并将状态代码和位置作为数据发送回来,然后您可以通过 `useEffect` 和 `useNavigate` 在组件中执行客户端重定向。
220 |
221 | ```jsx
222 | async function loader({ request, params }) {
223 | let data = fetch(url).then((res) => {
224 | if (res.status == 301) {
225 | return {
226 | isRedirect: true,
227 | status: res.status,
228 | location: res.headers.get("Location"),
229 | };
230 | }
231 | return res.json();
232 | });
233 |
234 | return defer({ data });
235 | }
236 | ```
--------------------------------------------------------------------------------
/src/guides/form-data.md:
--------------------------------------------------------------------------------
1 | # 使用 FormData
2 |
3 | > NOTE
4 | >
5 | > TODO:本文档为存根
6 |
7 | 常见的技巧是使用[`Object.fromEntries`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries)将整个 formData 转换为对象:
8 |
9 | ```jsx
10 | const data = Object.fromEntries(await request.formData());
11 | data.songTitle;
12 | data.lyrics;
13 | ```
14 |
15 |
--------------------------------------------------------------------------------
/src/guides/index-search-param.md:
--------------------------------------------------------------------------------
1 | # 索引查询参数
2 |
3 | 在提交表单时,您可能会发现应用程序的 URL 中出现一个乱码 `?index` 。
4 |
5 | 由于存在嵌套路由,路由层次结构中的多个路由都能与 URL 匹配。与导航不同的是,导航会调用所有匹配的路由加载器来构建用户界面,而提交表单时*只会调用一个操作*。
6 |
7 | 由于索引路由与其父路由共享相同的 URL,因此 `?index` 参数可帮助您区分两者。
8 |
9 | 例如,请看下面的路由和表单:
10 |
11 | ```jsx
12 | createBrowserRouter([
13 | {
14 | path: "/projects",
15 | element: ,
16 | action: ProjectsLayout.action,
17 | children: [
18 | {
19 | index: true,
20 | element: ,
21 | action: ProjectsPage.action,
22 | },
23 | ],
24 | },
25 | ]);
26 |
27 | ;
28 | ;
29 | ```
30 |
31 | `?index` 参数将提交到索引路由,不带索引参数的操作将提交到父路由。
32 |
33 | 当 `` 在没有 `action` 的索引路由中呈现时,将自动附加 `?index` 参数,以便表单发布到索引路由。下面的表单在提交后会发布到 `/projects?index` ,因为它是在项目索引路由的上下文中呈现的:
34 |
35 | ```jsx
36 | function ProjectsIndex() {
37 | return ;
38 | }
39 | ```
40 |
41 | 如果您将代码移至 `ProjectsLayout` 路由,它将转而发布到 `/projects` 。
42 |
43 | 这适用于 `` 及其所有同类产品:
44 |
45 | ```jsx
46 | let submit = useSubmit();
47 | submit({}, { action: "/projects" });
48 | submit({}, { action: "/projects?index" });
49 |
50 | let fetcher = useFetcher();
51 | fetcher.submit({}, { action: "/projects" });
52 | fetcher.submit({}, { action: "/projects?index" });
53 | ;
54 | ;
55 | ; // defaults to the route in context
56 | ```
57 |
58 |
--------------------------------------------------------------------------------
/src/guides/ssr.md:
--------------------------------------------------------------------------------
1 | # 服务器端渲染
2 |
3 | React Router 中最基本的服务器呈现非常简单。不过,除了获取正确的路由来进行呈现外,还有很多其他事项需要考虑。以下是您需要处理的事项的不完整列表:
4 |
5 | - 为服务器和浏览器打包代码
6 | - 不打包服务器端专用代码到浏览器端包中
7 | - 可在服务器端和浏览器端运行的代码拆分功能
8 | - 服务器端数据加载,让你真正有东西可呈现
9 | - 适用于客户端和服务器的数据加载策略
10 | - 处理服务器和客户端的代码分割
11 | - 正确的 HTTP 状态代码和重定向
12 | - 环境变量和机密信息
13 | - 部署
14 |
15 | 设置好这一切可能会很麻烦,但只有在服务器渲染时才能获得的性能和用户体验特性是值得的。
16 |
17 | 如果您想对 React Router 应用程序进行服务器渲染,我们强烈推荐您使用[Remix](https://remix.run/)。这是我们的另一个项目,它构建于 React Router 之上,可以处理上述所有问题,甚至更多。试试看吧!
18 |
19 | 如果您想自己解决这个问题,您需要在服务器上使用 `` 或 `` ,这取决于您选择的[路由](../routers/picking-a-router)。如果使用 `` ,请跳转到 ["不使用数据路由 "](../guides/ssr#without-a-data-router)部分。
20 |
21 | ## 使用数据路由
22 |
23 | 首先,您需要为数据路由定义路由,这些路由将在服务端和客户端中使用:
24 |
25 | `router.jsx`
26 |
27 | ```jsx
28 | const React = require("react");
29 | const { json, useLoaderData } = require("react-router-dom");
30 |
31 | const routes = [
32 | {
33 | path: "/",
34 | loader() {
35 | return json({ message: "Welcome to React Router!" });
36 | },
37 | Component() {
38 | let data = useLoaderData();
39 | return {data.message}
;
40 | },
41 | },
42 | ];
43 |
44 | module.exports = routes;
45 | ```
46 |
47 | > NOTE
48 | >
49 | > 在这些示例中,我们使用了 CJS 模块,以简化服务器,但一般情况下,您会使用 ESM 模块或更高级别的打包程序,如 `esbuild` 、 `vite` 或 `webpack` 。
50 |
51 | 定义好路由后,我们就可以在 express 服务器中创建一个处理程序,然后使用 `createStaticHandler()` 为路由加载数据。请记住,数据路由器的主要目标是将数据获取与渲染解耦,因此,在使用数据路由器进行服务器渲染时,我们有不同的获取和渲染步骤。
52 |
53 | `server.jsx`
54 |
55 | ```jsx
56 | const express = require("express");
57 | const {
58 | createStaticHandler,
59 | } = require("react-router-dom/server");
60 |
61 | const createFetchRequest = require("./request");
62 | const routes = require("./routes");
63 |
64 | const app = express();
65 |
66 | let handler = createStaticHandler(routes);
67 |
68 | app.get("*", async (req, res) => {
69 | let fetchRequest = createFetchRequest(req);
70 | let context = await handler.query(fetchRequest);
71 |
72 | // We'll tackle rendering next...
73 | });
74 |
75 | const listener = app.listen(3000, () => {
76 | let { port } = listener.address();
77 | console.log(`Listening on port ${port}`);
78 | });
79 | ```
80 |
81 | 请注意,我们必须先将传入的 Express 请求转换为 Fetch 请求,这正是静态处理程序方法的操作对象。 `createFetchRequest` 方法是针对 Express 请求的,在本例中是从 `@remix-run/express` 适配程序中提取的:
82 |
83 | `request.js`
84 |
85 | ```js
86 | module.exports = function createFetchRequest(req) {
87 | let origin = `${req.protocol}://${req.get("host")}`;
88 | // Note: This had to take originalUrl into account for presumably vite's proxying
89 | let url = new URL(req.originalUrl || req.url, origin);
90 |
91 | let controller = new AbortController();
92 | req.on("close", () => controller.abort());
93 |
94 | let headers = new Headers();
95 |
96 | for (let [key, values] of Object.entries(req.headers)) {
97 | if (values) {
98 | if (Array.isArray(values)) {
99 | for (let value of values) {
100 | headers.append(key, value);
101 | }
102 | } else {
103 | headers.set(key, values);
104 | }
105 | }
106 | }
107 |
108 | let init = {
109 | method: req.method,
110 | headers,
111 | signal: controller.signal,
112 | };
113 |
114 | if (req.method !== "GET" && req.method !== "HEAD") {
115 | init.body = req.body;
116 | }
117 |
118 | return new Request(url.href, init);
119 | };
120 | ```
121 |
122 | 通过执行所有匹配的路由`loader`加载数据后,我们使用 `createStaticRouter()` 和 `` 渲染 HTML 并将响应发送回浏览器:
123 |
124 | `server.jsx`
125 |
126 | ```jsx
127 | app.get("*", async (req, res) => {
128 | let fetchRequest = createFetchRequest(req);
129 | let context = await handler.query(fetchRequest);
130 |
131 | let router = createStaticRouter(
132 | handler.dataRoutes,
133 | context
134 | );
135 | let html = ReactDOMServer.renderToString(
136 |
140 | );
141 |
142 | res.send("" + html);
143 | });
144 | ```
145 |
146 | 将 HTML 发送回浏览器后,我们需要使用 `createBrowserRouter()` 和 `` 在客户端 "水合 "应用程序:
147 |
148 | `entry-client.jsx`
149 |
150 | ```jsx
151 | import * as React from "react";
152 | import * as ReactDOM from "react-dom/client";
153 | import {
154 | createBrowserRouter,
155 | RouterProvider,
156 | } from "react-router-dom";
157 |
158 | import { routes } from "./routes";
159 |
160 | let router = createBrowserRouter(routes);
161 |
162 | ReactDOM.hydrateRoot(
163 | document.getElementById("app"),
164 |
165 | );
166 | ```
167 |
168 | 这样,一个服务器端渲染和水合的应用程序就完成了!有关工作示例,您也可以参考 Github 代码库中的[示例](https://github.com/remix-run/react-router/tree/main/examples/ssr-data-router)。
169 |
170 | ### 其他概念
171 |
172 | 如上所述,服务器端渲染在大规模应用和生产级应用中非常棘手,如果你想实现这一目标,我们强烈推荐你使用 [Remix](https://remix.run/) 。但是,如果你要走手动的路由,这里有一些额外的概念你可能需要考虑:
173 |
174 | #### Hydration
175 |
176 | 服务器端渲染的一个核心概念是 [hydration](https://react.dev/reference/react-dom/client/hydrateRoot),它涉及将客户端 React 应用程序 "附加 "到服务器渲染的 HTML 上。要正确地做到这一点,我们需要在与服务器渲染时相同的状态下创建客户端 React Router 应用程序。当服务器通过 `loader` 函数渲染加载数据时,我们需要将这些数据发送上去,这样就可以使用相同的加载器数据创建客户端路由器,用于初始渲染/水合。
177 |
178 | 本指南中介绍的 `` 和 `createBrowserRouter` 的基本用法可以在内部处理这一问题,但如果需要控制水合过程,可以通过 [``](../routers/static-router-provider#hydrate) 禁用自动水合过程。
179 |
180 | 在某些高级用例中,您可能希望对客户端 React Router 应用程序进行部分水合。您可以通过传给 `createBrowserRouter` 的 [`future.v7_partialHydration`](../routers/create-browser-router#partial-hydration-data) 标志来实现这一点。
181 |
182 | #### 重定向
183 |
184 | 如果有任何`loader`重定向, `handler.query` 将直接返回 `Response` ,因此应检查这一点并发送重定向响应,而不是尝试呈现 HTML 文档:
185 |
186 | `server.jsx`
187 |
188 | ```jsx
189 | app.get("*", async (req, res) => {
190 | let fetchRequest = createFetchRequest(req);
191 | let context = await handler.query(fetchRequest);
192 |
193 | if (
194 | context instanceof Response &&
195 | [301, 302, 303, 307, 308].includes(context.status)
196 | ) {
197 | return res.redirect(
198 | context.status,
199 | context.headers.get("Location")
200 | );
201 | }
202 |
203 | // Render HTML...
204 | });
205 | ```
206 |
207 | #### 懒加载路由
208 |
209 | 如果您在路由中使用了[`route.lazy`](../route/lazy),那么在客户端上,您可能已经拥有了水合所需的所有数据,但还没有路由定义!理想情况下,您的设置会在服务器上确定匹配的路由,并在关键路径上交付路由包,这样您就不会在最初匹配的路由上使用 `lazy` 。但如果情况并非如此,则需要在水合之前加载这些路由并更新到位,以避免路由器退回到加载状态:
210 |
211 | `entry-client.jsx`
212 |
213 | ```jsx
214 | // Determine if any of the initial routes are lazy
215 | let lazyMatches = matchRoutes(
216 | routes,
217 | window.location
218 | )?.filter((m) => m.route.lazy);
219 |
220 | // Load the lazy matches and update the routes before creating your router
221 | // so we can hydrate the SSR-rendered content synchronously
222 | if (lazyMatches && lazyMatches?.length > 0) {
223 | await Promise.all(
224 | lazyMatches.map(async (m) => {
225 | let routeModule = await m.route.lazy();
226 | Object.assign(m.route, {
227 | ...routeModule,
228 | lazy: undefined,
229 | });
230 | })
231 | );
232 | }
233 |
234 | let router = createBrowserRouter(routes);
235 |
236 | ReactDOM.hydrateRoot(
237 | document.getElementById("app"),
238 |
239 | );
240 | ```
241 |
242 | 另请参阅:
243 |
244 | - [`createStaticHandler`](../routers/create-static-handler)
245 | - [`createStaticRouter`](../routers/create-static-router)
246 | - [``](../routers/static-router-provider)
247 |
248 | ## 不使用数据路由
249 |
250 | 首先,你需要某种在服务器和浏览器上呈现的 "应用程序 "或 "根 "组件:
251 |
252 | `App.jsx`
253 |
254 | ```jsx
255 | export default function App() {
256 | return (
257 |
258 |
259 | Server Rendered App
260 |
261 |
262 |
263 | Home} />
264 | About} />
265 |
266 |
267 |
268 |
269 | );
270 | }
271 | ```
272 |
273 | 下面是一个简单的 Express 服务器,可在服务器上渲染应用程序。请注意 `StaticRouter` 的使用。
274 |
275 | `server.entry.js`
276 |
277 | ```jsx
278 | import express from "express";
279 | import ReactDOMServer from "react-dom/server";
280 | import { StaticRouter } from "react-router-dom/server";
281 | import App from "./App";
282 |
283 | let app = express();
284 |
285 | app.get("*", (req, res) => {
286 | let html = ReactDOMServer.renderToString(
287 |
288 |
289 |
290 | );
291 | res.send("" + html);
292 | });
293 |
294 | app.listen(3000);
295 | ```
296 |
297 | 最后,您还需要一个类似的文件,以便将应用程序与包含相同 `App` 组件的 JavaScript 捆绑程序 "水合"。注意使用 `BrowserRouter` 而不是 `StaticRouter` 。
298 |
299 | `client.entry.js`
300 |
301 | ```jsx
302 | import * as ReactDOM from "react-dom";
303 | import { BrowserRouter } from "react-router-dom";
304 | import App from "./App";
305 |
306 | ReactDOM.hydrate(
307 |
308 |
309 | ,
310 | document.documentElement
311 | );
312 | ```
313 |
314 | 与客户端入口的唯一真正区别是:
315 |
316 | - `StaticRouter`而不是`BrowserRouter`
317 | - 将服务器上的 URL 传递给 ``
318 | - 使用 `ReactDOMServer.renderToString` 代替 `ReactDOM.render`。
319 |
320 | 有些部分需要自己动手才能完成:
321 |
322 | - 如何打包代码以便在浏览器和服务器中运行
323 | - 如何知道 `` 组件中 `