Tutorial v6.4.2 | React Router
import React from "react";
import ReactDOM from "react-dom/client";
import {
createBrowserRouter,
RouterProvider,
Route,
} from "react-router-dom";
import "./index.css";
const router = createBrowserRouter([
{
path: "/",
element: <div>Hello world!</div>,
},
]);
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
在src
目录下需要创建和编辑index.css
和contacts.js
文件
css
js
const router = createBrowserRouter([
{
path: "/",
element: ,
},
]);
routes/root.tsx
定义Root组件
export default function Root() {
return (
<>
>
);
}
函数时编程,useRouteError
函数获取出错信息
import { useRouteError } from "react-router-dom";
export default function ErrorPage() {
const error = useRouteError();
console.error(error);
return (
<div id="error-page">
<h1>Oops!</h1>
<p>Sorry, an unexpected error has occurred.</p>
<p>
<i>{error.statusText || error.message}</i>
</p>
</div>
);
}
main.tsx
import ErrorPage from "./error-page";
const router = createBrowserRouter([
{
path: "/",
element: ,
errorElement: ,
},
]);
当路由出错时:
Note that useRouteError
provides the error that was thrown. When the user navigates to routes that don’t exist you’ll get an error response with a “Not Found” statusText
. We’ll see some other errors later in the tutorial and discuss them more.
For now, it’s enough to know that pretty much all of your errors will now be handled by this page instead of infinite spinners, unresponsive pages, or blank screens
声明组件routes/contact.tsx
import { Form } from "react-router-dom";
export default function Contact() {
const contact = {
first: "Your",
last: "Name",
avatar: "https://placekitten.com/g/200/200",
twitter: "your_handle",
notes: "Some notes",
favorite: true,
};
return (
<div id="contact">
<div>
<img
key={contact.avatar}
src={contact.avatar || null}
/>
</div>
<div>
<h1>
{contact.first || contact.last ? (
<>
{contact.first} {contact.last}
</>
) : (
<i>No Name</i>
)}{" "}
<Favorite contact={contact} />
</h1>
{contact.twitter && (
<p>
<a
target="_blank"
href={`https://twitter.com/${contact.twitter}`}
>
{contact.twitter}
</a>
</p>
)}
{contact.notes && <p>{contact.notes}</p>}
<div>
<Form action="edit">
<button type="submit">Edit</button>
</Form>
<Form
method="post"
action="destroy"
onSubmit={(event) => {
if (
!confirm(
"Please confirm you want to delete this record."
)
) {
event.preventDefault();
}
}}
>
<button type="submit">Delete</button>
</Form>
</div>
</div>
</div>
);
}
function Favorite({ contact }) {
// yes, this is a `let` for later
let favorite = contact.favorite;
return (
<Form method="post">
<button
name="favorite"
value={favorite ? "false" : "true"}
aria-label={
favorite
? "Remove from favorites"
: "Add to favorites"
}
>
{favorite ? "★" : "☆"}
</button>
</Form>
);
}
import Contact from "./routes/contact";
const router = createBrowserRouter([
{
path: "/",
element: ,
errorElement: ,
},
{
path: "contacts/:contactId",
element: ,
},
]);
现在contacts
会单独在一个页面中显示,我们希望他在Root
组件右侧显示,即搜索后,在右侧显示联系人信息。
const router = createBrowserRouter([
{
path: "/",
element: ,
errorElement: ,
children: [
{
path: "contacts/:contactId",
element: ,
},
],
},
]);
原本的Root组件结构
我们的目的是在Root组件里面显示下级路由组件Contacts
的内容,官方是这样做的:
import { Outlet } from "react-router-dom";
在一个空div中,添加了Outlet
组件,那么我们可以猜到,子路由的组件将渲染到该位置。
效果:
Outlet v6.4.2 | React Router
An
should be used in parent route elements to render their child route elements. This allows nested UI to show up when child routes are rendered. If the parent route matched exactly, it will render a child index route or nothing if there is no index route.
在父路由元素中应该使用
来呈现子路由元素。这允许在呈现子路由时显示嵌套UI。如果父路由完全匹配,它将呈现子索引路由,如果没有索引路由则不呈现子索引路由。
将标签替换为组件
-
Your Name
-
Your Friend
URL段、布局、数据经常耦合在一起,例如:
URL Segment | Component | Data |
---|---|---|
/ |
|
list of contacts |
contacts/:id |
|
individual contact |
因此,React Router定义了一些约定(data conventions) 帮助将数据传给路由组件,包括loader
和useLoaderData
.
在root.tsx
中导出一个loader
函数:
import { getContacts } from "../contacts";
export async function loader() {
const contacts = await getContacts();
return { contacts };
}
loader v6.4.2 | React Router
Each route can define a “loader” function to provide data to the route element before it renders.
在路由组件渲染前(挂载)给组件传递数据。
root.tsx
import {
useLoaderData,
} from "react-router-dom";
/* other code */
export default function Root() {
const { contacts } = useLoaderData();
return (
<>
>
);
}
由于getContacts
没有接收到参数,所以返回的是空对象。显示无联系人
Root组件中的New
按钮触发了表单提交事件
Tutorial v6.4.2 | React Router
While unfamiliar to some web developers, HTML forms actually cause a navigation in the browser, just like clicking a link. The only difference is in the request: links can only change the URL while forms can also change the request method (GET vs POST) and the request body (POST form data).
虽然对一些web开发人员来说并不熟悉,但HTML表单实际上会在浏览器中产生导航,就像点击链接一样。唯一的区别在于请求:链接只能更改URL,而表单还可以更改请求方法(GET vs POST)和请求体(POST表单数据)。
Instead of sending that POST to the Vite server to create a new contact, let’s use client side routing instead.
取消触发传统表达事件处理,把这个url链接交给路由处理。
Tutorial v6.4.2 | React Router
root.tsx
import {
Outlet,
Link,
useLoaderData,
Form,
} from "react-router-dom";
import { getContacts, createContact } from "../contacts";
export async function action() {
await createContact();
}
/* other code */
export default function Root() {
const { contacts } = useLoaderData();
return (
<>
>
);
}
导出了一个事件处理函数action
。
main.ts
定义路由时,添加action
属性
import Root, {
loader as rootLoader,
action as rootAction,
} from "./routes/root";
const router = createBrowserRouter([
{
path: "/",
element: ,
errorElement: ,
loader: rootLoader,
action: rootAction,
children: [
{
path: "contacts/:contactId",
element: ,
},
],
},
]);
能够添加联系人了,但是联系人属性没有保存。
The createContact
method just creates an empty contact with no name or data or anything. But it does still create a record, promise!
Wait a sec … How did the sidebar update? Where did we call the
action
? Where’s the code to refetch the data? Where areuseState
,onSubmit
anduseEffect
?!
这就是“old school web”编程模式出现的地方。正如我们前面所讨论的,阻止浏览器将请求发送到服务器,而是将其发送到您的路由操作。在web语义中,POST通常意味着某些数据正在发生变化。按照约定,React Router将此作为提示,在操作完成后自动重新验证页面上的数据。这意味着所有的useLoaderData钩子都会更新,UI会自动与您的数据保持同步!很酷。
点击联系人record后,链接变成contacts/xxxx
。
看一下路由声明:
[
{
path: "contacts/:contactId",
element: ,
},
];
These
params
are passed to the loader with keys that match the dynamic segment. For example, our segment is named:contactId
so the value will be passed asparams.contactId
.These params are most often used to find a record by ID. Let’s try it out.
contact.tsx
使用params
import { Form, useLoaderData } from "react-router-dom";
import { getContact } from "../contacts";
export async function loader({ params }) {
return getContact(params.contactId);
}
export default function Contact() {
// const contact = {
// first: "Your",
// last: "Name",
// avatar: "https://placekitten.com/g/200/200",
// twitter: "your_handle",
// notes: "Some notes",
// favorite: true,
// };
const contact = useLoaderData();
// existing code
}
localforage
Just like creating data, you update data withForm
. Let’s make a new route at contacts/:contactId/edit
. Again, we’ll start with the component and then wire it up to the route config.
edit.tsx
import { Form, useLoaderData } from "react-router-dom";
export default function EditContact() {
const contact = useLoaderData();
return (
);
}
main.tsx
const router = createBrowserRouter([
{
path: "/",
element: ,
errorElement: ,
loader: rootLoader,
action: rootAction,
children: [
{
path: "contacts/:contactId",
element: ,
loader: contactLoader,
},
{
path: "contacts/:contactId/edit",
element: ,
loader: contactLoader,
},
],
},
]);
(You might note we reused the contactLoader
for this route. This is only because we’re being lazy in the tutorial. There is no reason to attempt to share loaders among routes, they usually have their own.)
与之前相同,自定义表单Action事件,避免将请求发送给服务器
edit.tsx
import {
Form,
useLoaderData,
redirect,
} from "react-router-dom";
import { updateContact } from "../contacts";
export async function action({ request, params }) {
const formData = await request.formData();
const updates = Object.fromEntries(formData);
await updateContact(params.contactId, updates);
return redirect(`/contacts/${params.contactId}`);
}
/* existing code */
之后与之前一样在main.tsx
中声明action属性。
修改是如何生效的,以及页面如何触发重新渲染?
如果没有添加额外js代码。当表单提交时,浏览器将会创建FormData
,并将其作为request的body发送给服务器。
React Router将发送给服务器的request转交给action,并阻止其发送给服务器。
export async function action({ request, params }) {
const formData = await request.formData();
const firstName = formData.get("first");
const lastName = formData.get("last");
// ...
}
除了from
的action
(react router提供),所有api如:request
, request.formData
都是web平台提供的。
Loaders and actions can both return a
Response
(makes sense, since they received aRequest
!). Theredirect
helper just makes it easier to return a response that tells the app to change locations.
Without client side routing, if a server redirected after a POST request, the new page would fetch the latest data and render. As we learned before, React Router emulates this model and automatically revalidates the data on the page after the action. That’s why the sidebar automatically updates when we save the form. The extra revalidation code doesn’t exist without client side routing, so it doesn’t need to exist with client side routing either!
如果没有客户端路由,如果服务器在POST请求后重定向,新页面将获取最新数据并呈现。正如我们以前学到的,React Router模拟这个模型,并在操作之后自动重新验证页面上的数据。这就是为什么当我们保存表单时,侧栏会自动更新。在没有客户端路由的情况下,额外的重新验证代码是不存在的,所以在没有客户端路由的情况下,它也不需要存在!
root.tsx
中的action
返回重定向
import {
Outlet,
Link,
useLoaderData,
Form,
redirect,
} from "react-router-dom";
import { getContacts, createContact } from "../contacts";
export async function action() {
const contact = await createContact();
return redirect(`/contacts/${contact.id}/edit`);
}
isActive
? "active"
: isPending
? "pending"
: ""
}
>
{/* other code */}
当我们在
所指定的路由时,isActive
会被设置为true
,When it’s about to be active (the data is still loading) then isPending
will be true. This allows us to easily indicate where the user is, as well as provide immediate feedback on links that have been clicked but we’re still waiting for data to load.
当用户导航应用程序时,React Router将离开旧的页面,因为数据正在为下一页加载。你可能已经注意到,当你在列表之间点击时,应用程序感觉有点没有反应。让我们为用户提供一些反馈,这样应用程序就不会感到没有响应。
React Router在幕后管理所有的状态,并揭示你构建动态web应用所需要的部分。在本例中,我们将使用usenavnavigation
钩子。
root.tsx
import {
// existing code
useNavigation,
} from "react-router-dom";
// existing code
export default function Root() {
const { contacts } = useLoaderData();
const navigation = useNavigation();
return (
<>
>
);
}
useNavigation
returns the current navigation state: it can be one of "idle" | "submitting" | "loading"
.
表示当前路由状态
#detail.loading {
opacity: 0.25;
transition: opacity 200ms;
transition-delay: 200ms;
}
edit按钮事件以及处理好,接下来处理删除事件,同样是发送post请求。
contact.tsx
这里直接在Form组件中写了action属性,因此发出的请求url是contact/:contactId/destroy;
所有我们需要注册这个路由,并为这个路由添加action属性。
destroy.tsx
import { redirect } from "react-router-dom";
import { deleteContact } from "../contacts";
export async function action({ params }) {
await deleteContact(params.contactId);
return redirect("/");
}
main.tsx
{
path: "contacts/:contactId/destroy",
action: deleteAction,
},
手动抛出错误
destroy.tsx
export async function action({ params }) {
throw new Error("oh dang!");
await deleteContact(params.contactId);
return redirect("/");
}
配置路由组件的错误处理组件
[
/* other routes */
{
path: "contacts/:contactId/destroy",
action: destroyAction,
errorElement: Oops! There was an error.,
},
];
当不为contacts/:contactId/destroy
路由对应的errorElement
时,error会像上级组件传递,即被
处理。
效果为:
当子路由没有任何匹配时,右侧是空白的,我们希望其显示一些默认内容,例如数据统计等。
像传统web一样,我们会显示目录下的index.html文件,在react路由中,我们可以声明index组件,当路由与当前路由完全匹配时(没有子路由匹配),显示index组件。
index.tsx
export default function Index() {
return (
This is a demo for React Router.
Check out{" "}
the docs at reactrouter.com
.
);
}
children: [
{
index: true,
element:
},
Note the
{ index:true }
instead of{ path: "" }
. That tells the router to match and render this route when the user is at the parent route’s exact path, so there are no other child routes to render in the.
edit.tsx
import {
Form,
useLoaderData,
redirect,
useNavigate,
} from "react-router-dom";
export default function Edit() {
const contact = useLoaderData();
const navigate = useNavigate();
return (
);
}
Now when the user clicks “Cancel”, they’ll be sent back one entry in the browser’s history.
Why is there no
event.preventDefault
on the button?
A , while seemingly redundant, is the HTML way of preventing a button from submitting its form.
Two more features to go. We’re on the home stretch!
All of our interactive UI so far have been either links that change the URL or forms that post data to actions. The search field is interesting because it’s a mix of both: it’s a form but it only changes the URL, it doesn’t change data.
到目前为止,我们所有的交互UI都是更改URL的链接或将数据发布到操作的表单。搜索字段很有趣,因为它是两者的混合:它是一个表单,但它只改变URL,不改变数据。
Note the browser’s URL now contains your query in the URL as URLSearchParams:
http://127.0.0.1:5173/?q=ryan
root.tsx
As we’ve seen before, browsers can serialize forms by the
name
attribute of it’s input elements. The name of this input isq
, that’s why the URL has?q=
. If we named itsearch
the URL would be?search=
.Note that this form is different from the others we’ve used, it does not have
. The default
method
is"get"
. That means when the browser creates the request for the next document, it doesn’t put the form data into the request POST body, but into theURLSearchParams
of a GET request.
使用GET请求方式的化,参数会加在路径中,而不是加在请求体中。
root.tsx
将form改为Form组件
获取参数
export async function loader({ request }) {
const url = new URL(request.url);
const q = url.searchParams.get("q");
const contacts = await getContacts(q);
return { contacts };
}
Because this is a GET, not a POST, React Router does not call the
action
. Submitting a GET form is the same as clicking a link: only the URL changes. That’s why the code we added for filtering is in theloader
, not theaction
of this route.This also means it’s a normal page navigation. You can click the back button to get back to where you were.
由于这是GET请求,不是POST请求,不会触发路由的action,提交GET表单和点击一个链接效果是相同的,只有url发送改变。这也是为什么我们只在loader中添加了代码,而不是action。
目前存在的问题:
即表单状态与url不同步。
获取url中的参数,并将其填充到表单中
export async function loader({ request }) {
const url = new URL(request.url);
const q = url.searchParams.get("q");
const contacts = await getContacts(q);
return { contacts, q };
}
const { contacts, q } = useLoaderData();
defaultValue={q}
根据测试目前是没有问题的,但是官方提到这只解决了第二个问题,第一个问题没有解决。
但是假设当前url是:http://localhost:5173/,设搜索后的url是http://localhost:5173/?q=12,
返回后是没有问题的,因为获取到的q是空,填充到表单也是空。
这是因为没有将form改为Form,点击回车后触发了form表单的事件,触发页面更新,而使用Form组件后,页面不会更新,不会重新触发loader,因此页面不会更新。(会重新进行渲染,但根据react的Diff算法,不会刷新DOM)
当我们刷新后,表单输入才会变成空。
Hook API 索引 – React (reactjs.org)
该 Hook 接收一个包含命令式、且可能有副作用代码的函数。
在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。
使用 useEffect
完成副作用操作。赋值给 useEffect
的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。
默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它 在只有某些值改变的时候 才执行。
通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。要实现这一点,useEffect
函数需返回一个清除函数。以下就是一个创建订阅的例子:
与 componentDidMount
、componentDidUpdate
不同的是,传给 useEffect
的函数会在浏览器完成布局与绘制之后,在一个延迟事件中被调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因为绝大多数操作不应阻塞浏览器对屏幕的更新。
默认情况下,effect 会在每轮组件渲染完成后执行。这样的话,一旦 effect 的依赖发生变化,它就会被重新创建。
然而,在某些场景下这么做可能会矫枉过正。比如,在上一章节的订阅示例中,我们不需要在每次组件更新时都创建新的订阅,而是仅需要在 source
prop 改变时重新创建。
要实现这一点,可以给 useEffect
传递第二个参数,它是 effect 所依赖的值数组。更新后的示例如下:
使用useEffect的条件执行
useEffect(() => {
document.getElementById("q").value = q;
}, [q]);
const [query, setQuery] = useState(q);
{
setQuery(e.target.value);
}}
/>
onChange={(event) => {
submit(event.currentTarget.form);
}}
Now as you type, the form is submitted automatically!
Note the argument to
submit
. We’re passing inevent.currentTarget.form
. ThecurrentTarget
is the DOM node the event is attached to, and thecurrentTarget.form
is the input’s parent form node. Thesubmit
function will serialize and submit any form you pass to it.
在生产项目中,搜索是需要花一定时间的,为了获得更好的用户体验,让我们为搜索添加一些即时UI反馈。为此,我们将再次使用useNavigation。
const searching =
navigation.location &&
new URLSearchParams(navigation.location.search).has(
"q"
);
The
navigation.location
will show up when the app is navigating to a new URL and loading the data for it. It then goes away when there is no pending navigation anymore.
navigation.location
会在正在当前应用正在向另一个链接导航,并且loader正在执行时为真。
root.tsx
export default function Root() {
console.log('@@@')
当我们在搜索栏键入1后:
我们使用了useNavigation
,他返回了一个state,当它改变后,触发了页面更新。
const isFirstSearch = q == null;
submit(event.currentTarget.form, {
replace: !isFirstSearch,
});
到目前为止我们都是通过表单更高url获取POST请求,在历史堆栈中添加记录,实现mutations(the times we change data)。
那么我们如何不借助导航让数据发送改变呢。
useFetcher
hook函数让我们能够直接与loader
函数通信。
喜欢按钮符合我们这样做的原则:我们并不是在创建或删除新记录,也不想更改页面,我们只是想更改正在查看的页面上的数据。
编辑contact.tsx
import {
useLoaderData,
Form,
useFetcher,
} from "react-router-dom";
function Favorite({ contact }) {
const fetcher = useFetcher();
let favorite = contact.favorite;
return (
);
}
和往常一样,我们的表单有带有名称prop的字段。这个表单将发送带有favorite key的formData,它要么是"true",要么是" false"。因为它有method=“post”,它会调用action。因为没有
prop,它将发送到呈现表单的路由。
export async function action({ request, params }) {
let formData = await request.formData();
return updateContact(params.contactId, {
favorite: formData.get("favorite") === "true",
});
}
import Contact, {
loader as contactLoader,
action as contactAction,
} from "./routes/contact";
与Form组件唯一不同的是:它不是一个导航——URL不会改变,历史堆栈不受影响。
点击喜欢后,需要时间处理这个请求,这段时间界面没有任何反馈,一段时间后才显示喜欢图标。
为了增加反馈,我们需要使用fetcher的状态,就行我们之前使用navigation的状态一样。
fetcher知道提交给action的表单数据,所以可以在fetcher. formdata上获得。我们将使用它立即更新star的状态,即使请求还没有完成。如果更新最终失败,UI将恢复到真实的数据。
// existing code
function Favorite({ contact }) {
const fetcher = useFetcher();
let favorite = contact.favorite;
if (fetcher.formData) {
favorite = fetcher.formData.get("favorite") === "true";
}
return (
);
}
如果你现在点击按钮,你应该会看到星星立即变为新的状态。我们并不总是呈现实际的数据,而是检查fetcher是否有任何formData被提交,如果有,我们将使用它。当动作完成时,fetcher.formData将不再存在,我们将回到使用实际数据。因此,即使您在乐观的UI代码中编写了错误,它最终也会回到正确的状态。
当我们试图指定url查看一个不存在的用户,会引发error,并跳转到error界面。
这是因为我们loader获取到的contact对象是null。
对于这种已知的错误,我们希望应用进行明确的显示,并主动抛出错误。:
export async function loader({ params }) {
const contact = await getContact(params.contactId);
if (!contact) {
throw new Response("", {
status: 404,
statusText: "Not Found",
});
}
return contact;
}
但是,这个错误是顶级路由处理的,我们的界面无法显示其他任何内容,当我们指定这是contact组件的错误,我们只希望在contact组件位置显示该错误界面。
One last thing. The last error page we saw would be better if it rendered inside the root outlet, instead of the whole page. In fact, every error in all of our child routes would be better in the outlet, then the user has more options than hitting refresh.
We’d like it to look like this:
我们可以向每个组件都添加errorElement:
属性,但这显然是重复、代码冗余的。
有一个更清洁的方法。可以在没有路径的情况下使用路由,这使得它们可以参与UI布局,而不需要在URL中添加新的路径段:
createBrowserRouter([
{
path: "/",
element: ,
loader: rootLoader,
action: rootAction,
errorElement: ,
children: [
{
errorElement: ,
children: [
{ index: true, element: },
{
path: "contacts/:contactId",
element: ,
loader: contactLoader,
action: contactAction,
},
/* the rest of the routes */
],
},
],
},
]);
这样的话,error在冒泡时,就被无路径路由捕获了。
在之前,我们的路由都是在最开始就完成了全部定义。
对于我们的最后一个技巧,许多人更喜欢用JSX配置他们的路由。你可以用createRoutesFromElements做到这一点。在配置路由时,JSX和对象之间没有功能上的区别,这只是一种风格偏好。
import {
createRoutesFromElements,
createBrowserRouter,
} from "react-router-dom";
const router = createBrowserRouter(
createRoutesFromElements(
}
loader={rootLoader}
action={rootAction}
errorElement={ }
>
}>
} />
}
loader={contactLoader}
action={contactAction}
/>
}
loader={contactLoader}
action={editAction}
/>
)
);