什么叫路由呢,说白了就是如何处理页面的跳转。在传统的网站中,我们都是向服务器请求页面及相应的css和js代码。自从前后端分离的相思提出后,一堆基于js虚拟Dom的框架应运而生。React就是其中优秀的代表作之一。这种方式极大的优化了开发体验。从些,我们前端开发也可以向开发桌面软件一样那么的优雅,这在开发中后端产品中优势特别明显。那么,既然已经分离了,在浏览器中如何处理跳转关系呢,这就离不开路由这个话题了。在React中借助 Router 这个第三方包可以优雅的处理这种操作,让我们的界面实现丝滑的转换。同样,我还是借用官方的示例为大家讲解,应该说话是是比较全面的了。话不多说,进入主题。
进入我们的项目的根目录 my-react-app
cd my-react-app
npm install react-router-dom localforage match-sorter sort-by
## 运行我们的项目
yarn dev
为我们今天的学习创建目录:在我们项目的src目录下创建新的目录 Test08
首先我们引入一个已经定义好的CSS文件,在App.jsx中引用就好了。
/* index.css */
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
html,
body {
height: 100%;
margin: 0;
line-height: 1.5;
color: #121212;
}
textarea,
input,
button {
font-size: 1rem;
font-family: inherit;
border: none;
border-radius: 8px;
padding: 0.5rem 0.75rem;
box-shadow: 0 0px 1px hsla(0, 0%, 0%, 0.2), 0 1px 2px hsla(0, 0%, 0%, 0.2);
background-color: white;
line-height: 1.5;
margin: 0;
}
button {
color: #3992ff;
font-weight: 500;
}
textarea:hover,
input:hover,
button:hover {
box-shadow: 0 0px 1px hsla(0, 0%, 0%, 0.6), 0 1px 2px hsla(0, 0%, 0%, 0.2);
}
button:active {
box-shadow: 0 0px 1px hsla(0, 0%, 0%, 0.4);
transform: translateY(1px);
}
#contact h1 {
display: flex;
align-items: flex-start;
gap: 1rem;
}
#contact h1 form {
display: flex;
align-items: center;
margin-top: 0.25rem;
}
#contact h1 form button {
box-shadow: none;
font-size: 1.5rem;
font-weight: 400;
padding: 0;
}
#contact h1 form button[value="true"] {
color: #a4a4a4;
}
#contact h1 form button[value="true"]:hover,
#contact h1 form button[value="false"] {
color: #eeb004;
}
form[action$="destroy"] button {
color: #f44250;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
#root {
display: flex;
height: 100%;
width: 100%;
}
#sidebar {
width: 22rem;
background-color: #f7f7f7;
border-right: solid 1px #e3e3e3;
display: flex;
flex-direction: column;
}
#sidebar>* {
padding-left: 2rem;
padding-right: 2rem;
}
#sidebar h1 {
font-size: 1rem;
font-weight: 500;
display: flex;
align-items: center;
margin: 0;
padding: 1rem 2rem;
border-top: 1px solid #e3e3e3;
order: 1;
line-height: 1;
}
#sidebar h1::before {
content: url("data:image/svg+xml,%3Csvg width='25' height='18' viewBox='0 0 25 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M19.4127 6.4904C18.6984 6.26581 18.3295 6.34153 17.5802 6.25965C16.4219 6.13331 15.9604 5.68062 15.7646 4.51554C15.6551 3.86516 15.7844 2.9129 15.5048 2.32334C14.9699 1.19921 13.7183 0.695046 12.461 0.982805C11.3994 1.22611 10.516 2.28708 10.4671 3.37612C10.4112 4.61957 11.1197 5.68054 12.3363 6.04667C12.9143 6.22097 13.5284 6.3087 14.132 6.35315C15.2391 6.43386 15.3241 7.04923 15.6236 7.55574C15.8124 7.87508 15.9954 8.18975 15.9954 9.14193C15.9954 10.0941 15.8112 10.4088 15.6236 10.7281C15.3241 11.2334 14.9547 11.5645 13.8477 11.6464C13.244 11.6908 12.6288 11.7786 12.0519 11.9528C10.8353 12.3201 10.1268 13.3799 10.1828 14.6234C10.2317 15.7124 11.115 16.7734 12.1766 17.0167C13.434 17.3056 14.6855 16.8003 15.2204 15.6762C15.5013 15.0866 15.6551 14.4187 15.7646 13.7683C15.9616 12.6032 16.423 12.1505 17.5802 12.0242C18.3295 11.9423 19.1049 12.0242 19.8071 11.6253C20.5491 11.0832 21.212 10.2696 21.212 9.14192C21.212 8.01428 20.4976 6.83197 19.4127 6.4904Z' fill='%23F44250'/%3E%3Cpath d='M7.59953 11.7459C6.12615 11.7459 4.92432 10.5547 4.92432 9.09441C4.92432 7.63407 6.12615 6.44287 7.59953 6.44287C9.0729 6.44287 10.2747 7.63407 10.2747 9.09441C10.2747 10.5536 9.07172 11.7459 7.59953 11.7459Z' fill='black'/%3E%3Cpath d='M2.64217 17.0965C1.18419 17.093 -0.0034949 15.8971 7.72743e-06 14.4356C0.00352588 12.9765 1.1994 11.7888 2.66089 11.7935C4.12004 11.797 5.30772 12.9929 5.30306 14.4544C5.29953 15.9123 4.10366 17.1 2.64217 17.0965Z' fill='black'/%3E%3Cpath d='M22.3677 17.0965C20.9051 17.1046 19.7046 15.9217 19.6963 14.4649C19.6882 13.0023 20.8712 11.8017 22.3279 11.7935C23.7906 11.7854 24.9911 12.9683 24.9993 14.4251C25.0075 15.8866 23.8245 17.0883 22.3677 17.0965Z' fill='black'/%3E%3C/svg%3E%0A");
margin-right: 0.5rem;
position: relative;
top: 1px;
}
#sidebar>div {
display: flex;
align-items: center;
gap: 0.5rem;
padding-top: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid #e3e3e3;
}
#sidebar>div form {
position: relative;
}
#sidebar>div form input[type="search"] {
width: 100%;
padding-left: 2rem;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='h-6 w-6' fill='none' viewBox='0 0 24 24' stroke='%23999' stroke-width='2'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z' /%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: 0.625rem 0.75rem;
background-size: 1rem;
position: relative;
}
#sidebar>div form input[type="search"].loading {
background-image: none;
}
#search-spinner {
width: 1rem;
height: 1rem;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24'%3E%3Cpath stroke='%23000' strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M20 4v5h-.582m0 0a8.001 8.001 0 00-15.356 2m15.356-2H15M4 20v-5h.581m0 0a8.003 8.003 0 0015.357-2M4.581 15H9' /%3E%3C/svg%3E");
animation: spin 1s infinite linear;
position: absolute;
left: 0.625rem;
top: 0.75rem;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
#sidebar nav {
flex: 1;
overflow: auto;
padding-top: 1rem;
}
#sidebar nav a span {
float: right;
color: #eeb004;
}
#sidebar nav a.active span {
color: inherit;
}
i {
color: #818181;
}
#sidebar nav .active i {
color: inherit;
}
#sidebar ul {
padding: 0;
margin: 0;
list-style: none;
}
#sidebar li {
margin: 0.25rem 0;
}
#sidebar nav a {
display: flex;
align-items: center;
justify-content: space-between;
overflow: hidden;
white-space: pre;
padding: 0.5rem;
border-radius: 8px;
color: inherit;
text-decoration: none;
gap: 1rem;
}
#sidebar nav a:hover {
background: #e3e3e3;
}
#sidebar nav a.active {
background: hsl(224, 98%, 58%);
color: white;
}
#sidebar nav a.pending {
color: hsl(224, 98%, 58%);
}
#detail {
flex: 1;
padding: 2rem 4rem;
width: 100%;
}
#detail.loading {
opacity: 0.25;
transition: opacity 200ms;
transition-delay: 200ms;
}
#contact {
max-width: 40rem;
display: flex;
}
#contact h1 {
font-size: 2rem;
font-weight: 700;
margin: 0;
line-height: 1.2;
}
#contact h1+p {
margin: 0;
}
#contact h1+p+p {
white-space: break-spaces;
}
#contact h1:focus {
outline: none;
color: hsl(224, 98%, 58%);
}
#contact a[href*="twitter"] {
display: flex;
font-size: 1.5rem;
color: #3992ff;
text-decoration: none;
}
#contact a[href*="twitter"]:hover {
text-decoration: underline;
}
#contact img {
width: 12rem;
height: 12rem;
background: #c8c8c8;
margin-right: 2rem;
border-radius: 1.5rem;
object-fit: cover;
}
#contact h1~div {
display: flex;
gap: 0.5rem;
margin: 1rem 0;
}
#contact-form {
display: flex;
max-width: 40rem;
flex-direction: column;
gap: 1rem;
}
#contact-form>p:first-child {
margin: 0;
padding: 0;
}
#contact-form>p:first-child> :nth-child(2) {
margin-right: 1rem;
}
#contact-form>p:first-child,
#contact-form label {
display: flex;
}
#contact-form p:first-child span,
#contact-form label span {
width: 8rem;
}
#contact-form p:first-child input,
#contact-form label input,
#contact-form label textarea {
flex-grow: 2;
}
#contact-form-avatar {
margin-right: 2rem;
}
#contact-form-avatar img {
width: 12rem;
height: 12rem;
background: hsla(0, 0%, 0%, 0.2);
border-radius: 1rem;
}
#contact-form-avatar input {
box-sizing: border-box;
width: 100%;
}
#contact-form p:last-child {
display: flex;
gap: 0.5rem;
margin: 0 0 0 8rem;
}
#contact-form p:last-child button[type="button"] {
color: inherit;
}
#zero-state {
margin: 2rem auto;
text-align: center;
color: #818181;
}
#zero-state a {
color: inherit;
}
#zero-state a:hover {
color: #121212;
}
#zero-state:before {
display: block;
margin-bottom: 0.5rem;
content: url("data:image/svg+xml,%3Csvg width='50' height='33' viewBox='0 0 50 33' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M38.8262 11.1744C37.3975 10.7252 36.6597 10.8766 35.1611 10.7128C32.8444 10.4602 31.9215 9.55475 31.5299 7.22456C31.3108 5.92377 31.5695 4.01923 31.0102 2.8401C29.9404 0.591789 27.4373 -0.416556 24.9225 0.158973C22.7992 0.645599 21.0326 2.76757 20.9347 4.94569C20.8228 7.43263 22.2399 9.5546 24.6731 10.2869C25.8291 10.6355 27.0574 10.8109 28.2646 10.8998C30.4788 11.0613 30.6489 12.292 31.2479 13.3051C31.6255 13.9438 31.9914 14.5731 31.9914 16.4775C31.9914 18.3819 31.6231 19.0112 31.2479 19.6499C30.6489 20.6606 29.9101 21.3227 27.696 21.4865C26.4887 21.5754 25.2581 21.7508 24.1044 22.0994C21.6712 22.834 20.2542 24.9537 20.366 27.4406C20.4639 29.6187 22.2306 31.7407 24.3538 32.2273C26.8686 32.8052 29.3717 31.7945 30.4415 29.5462C31.0032 28.3671 31.3108 27.0312 31.5299 25.7304C31.9238 23.4002 32.8467 22.4948 35.1611 22.2421C36.6597 22.0784 38.2107 22.2421 39.615 21.4443C41.099 20.36 42.4248 18.7328 42.4248 16.4775C42.4248 14.2222 40.9961 11.8575 38.8262 11.1744Z' fill='%23E3E3E3'/%3E%3Cpath d='M15.1991 21.6854C12.2523 21.6854 9.84863 19.303 9.84863 16.3823C9.84863 13.4615 12.2523 11.0791 15.1991 11.0791C18.1459 11.0791 20.5497 13.4615 20.5497 16.3823C20.5497 19.3006 18.1436 21.6854 15.1991 21.6854Z' fill='%23E3E3E3'/%3E%3Cpath d='M5.28442 32.3871C2.36841 32.38 -0.00698992 29.9882 1.54551e-05 27.0652C0.00705187 24.1469 2.39884 21.7715 5.32187 21.7808C8.24022 21.7878 10.6156 24.1796 10.6063 27.1027C10.5992 30.0187 8.20746 32.3941 5.28442 32.3871Z' fill='%23E3E3E3'/%3E%3Cpath d='M44.736 32.387C41.8107 32.4033 39.4096 30.0373 39.3932 27.1237C39.3769 24.1984 41.7428 21.7973 44.6564 21.7808C47.5817 21.7645 49.9828 24.1305 49.9993 27.0441C50.0156 29.9671 47.6496 32.3705 44.736 32.387Z' fill='%23E3E3E3'/%3E%3C/svg%3E%0A");
}
#error-page {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
}
所谓根组件就是App的主体界面组件。
export default function Root() {
return (
<>
<div id="sidebar">
<h1>React Router Contacts</h1>
<div>
<form id="search-form" role="search">
<input
id="q"
aria-label="Search contacts"
placeholder="Search"
type="search"
name="q"
/>
<div
id="search-spinner"
aria-hidden
hidden={true}
/>
<div
className="sr-only"
aria-live="polite"
></div>
</form>
<form method="post">
<button type="submit">New</button>
</form>
</div>
<nav>
<ul>
<li>
<a href={`/contacts/1`}>Your Name</a>
</li>
<li>
<a href={`/contacts/2`}>Your Friend</a>
</li>
</ul>
</nav>
</div>
<div id="detail"></div>
</>
);
}
我们先在App中引入,查看一下基本的界面
import './Test08/index.css';
import Root from './Test08/Root';
function App() {
return <Root />
}
export default App
路由表文件是指示路由走向的文件。创建路由有几种方式,我们这里以最通用的哈希路由为例,创建路由文件:
// routerConfig.jsx
import { createBrowserRouter } from "react-router-dom";
import Root from "./Root";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
},
]);
export default router;
createBrowserRouter()
函数内是一个json
数组。注意,这是一个数组,每个成员都是一个路由对象。
import './Test08/index.css';
import { RouterProvider } from "react-router-dom";
import router from './Test08/routerConfig';
function App() {
return <RouterProvider router={router} />
}
export default App
你看,这里也有一个Provider
, 跟我们之前文章中讲的状态管理器很像是不是,其实它就众多组件状态管理器中的一种,是二次包装的 Context
。
这时我们点击 Your Name
或其它链接时,会出现一个 404 的错误页面。因为目录针对于其它的地址链接我们还没有与之对应的页面显示,所以出现 404 是意料之中的事。
这样突兀的显示自然与我们的设计风格有点格格不入。这当然不是我们的设想。当然我们可以重新设计一个404页面并在导航出错时显示。
// Error404.jsx
import { useRouteError } from "react-router-dom";
export default function Error404() {
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>
);
}
在路由中指定:修改路由配置文件routerConfig.jsx
import { createBrowserRouter } from "react-router-dom";
import Root from "./Root";
import Error404 from "./Error404";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <Error404 />
},
]);
增加 errorElement
属性,作用是当当前路径下的子路径没有对应的页面显示时就显示errorElement
页面。 现在我们点击 联系人后 就会立刻跳转到我们指定的404页面了。
现在我们要创建联系的组件。
// Contact.jsx
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 }) {
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>
);
}
react-router
是提供了一些与路由操作有关的组件,它们与HTML
中的DOM
元素功能类似,但做了二次封装,方便在路由中获取数据。比如 Form
组件。
现在我们再把这个页面添加到路由中:
import { createBrowserRouter } from "react-router-dom";
import Root from "./Root";
import Error404 from "./Error404";
import Contact from "./Contact";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <Error404 />
},
{
path: "contacts/:contactId",
element: <Contact />,
}
]);
现在路由中我们有了两个平级的页面。 也就是说这两个页面的切换会卸载前一个页面,所以叫平级的。
我们发现路径contacts/:contactId
有所不同,有时候我们有这样的需求,通过路径向组件传递某种信息,比如:当我们点击联系人后想把联系的ID
号传递给组件
, 以往我们都是通过Props
或 redux
等方式进行传递,现在我们可以通过路由传递信息到相应的路由组件中。
注意分析 Root
组件,我们可以把联系人的 ID
信息写在跳转路径中。如 contacts/ID
形式。本例中为 contacts/1
和 contacts/2
, 现在路径中有了我们联系的ID
信息了("1 "和 “2”),那么如何传递到路径组件中呢。
我们可以通过路径变量的形式把对应的部分路径信息传递给组件。路径变量的格式为: :变量名
, 如上面的
的路径信息中为:contacts/:contactId
, 就可以通过 变量contactId
获取到联系人的 ID 号了。
相同的原理,我还可以通过这种方法获取更多路由的信息,如 contacts/:contactId/:contactName
等等。
回过话题,我们经过这样的配置后看看效果。这当然不是我们所期望的。我希望在联系人的右侧显示相关联系人的详情信息。就像下面这样。
如何实现呢,我们再次对路由的配置文件做个修改:
...
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <Error404 />,
children: [
{
path: "contacts/:contactId",
element: <Contact />,
},
]
}
]);
...
我们把第二个页面的路由配置信息移到了第一个的 children
子页面列表里了。这个时候我们再次点击联系人时,会发现路径虽然变化了,但页面没其它变化, 联系人的详细信息没有显示出来。也就是说
没有显示出来。 这是因为我们没有在 Root
中给
组件预留显示位置, 自然也就没有显示。
现在修改 Root.jsx文件, 添加 组件。 它就是一个占位符,为子组件提供显示空间。
import { Outlet } from 'react-router-dom';
export default function Root() {
return (
<>
{ ... 其它元素信息 }
<div id="detail"> <Outlet /> </div>
</>
);
}
react-router
提供了很多很实用的组件,搭配使用效果更好。我们把 Root 中的 换成
Link
组件要更好,因为 Link
只改变路由,不从服务器请求文档。
import { Outlet, Link } from 'react-router-dom';
export default function Root() {
return (
<>
<div id="sidebar">
{ ... }
<nav>
<ul>
<li>
<Link to={`/contacts/1`}>Your Name</Link>
</li>
<li>
<Link to ={`/contacts/2`}>Your Friend</Link>
</li>
</ul>
</nav>
</div>
<div id="detail"> <Outlet /> </div>
</>
);
}
有时我们想在页面加载的过程中就把数据也准备好,这样各组件不用各自向服务器请求数据了,大大节省了网络资源。
我们将使用 loader
和 useLoaderData
来加载数据。
我们在Test09
目录下创建一个工具函数库文件contacts.jsx
,直接把复制进去就行了,这些工具函数只是为了示例而创建,没有其它的实用价值,我们只要知道它们的作用就行了。
// cantacts.jsx
import localforage from "localforage";
import { matchSorter } from "match-sorter";
import sortBy from "sort-by";
export async function getContacts(query) {
await fakeNetwork(`getContacts:${query}`);
let contacts = await localforage.getItem("contacts");
if (!contacts) contacts = [];
if (query) {
contacts = matchSorter(contacts, query, { keys: ["first", "last"] });
}
return contacts.sort(sortBy("last", "createdAt"));
}
export async function createContact() {
await fakeNetwork();
let id = Math.random().toString(36).substring(2, 9);
let contact = { id, createdAt: Date.now() };
let contacts = await getContacts();
contacts.unshift(contact);
await set(contacts);
return contact;
}
export async function getContact(id) {
await fakeNetwork(`contact:${id}`);
let contacts = await localforage.getItem("contacts");
let contact = contacts.find(contact => contact.id === id);
return contact ?? null;
}
export async function updateContact(id, updates) {
await fakeNetwork();
let contacts = await localforage.getItem("contacts");
let contact = contacts.find(contact => contact.id === id);
if (!contact) throw new Error("No contact found for", id);
Object.assign(contact, updates);
await set(contacts);
return contact;
}
export async function deleteContact(id) {
let contacts = await localforage.getItem("contacts");
let index = contacts.findIndex(contact => contact.id === id);
if (index > -1) {
contacts.splice(index, 1);
await set(contacts);
return true;
}
return false;
}
function set(contacts) {
return localforage.setItem("contacts", contacts);
}
// fake a cache so we don't slow down stuff we've already seen
let fakeCache = {};
async function fakeNetwork(key) {
if (!key) {
fakeCache = {};
}
if (fakeCache[key]) {
return;
}
fakeCache[key] = true;
return new Promise(res => {
setTimeout(res, Math.random() * 800);
});
}
我们在再创建一个工具函数库文件, 创建一个 loader
函数工具并导出加载程序:
// utils.jsx
import { getContacts } from "./contacts";
// 模拟网络请求,获取数据
export async function rootLoader () {
const contacts = await getContacts();
return { contacts };
}
修改路由配置文件 routerConfig
, 加入数据加载的配置
import { createBrowserRouter } from "react-router-dom";
import Root from "./Root";
import Error404 from "./Error404";
import Contact from "./Contact";
import { rootLoader } from "./utils";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <Error404 />,
loader: rootLoader,
children: [
{
path: "contacts/:contactId",
element: <Contact />,
},
]
}
]);
export default router;
关于网络请求,我后面会写一个 Axios 的专门技术文章。这里请忽略网络请求部分。
现在当Root组件呈现后,loader 中就有了要使用的数据了。下面我们来把这个数据呈现出来:
// Root.jsx
import {
Outlet,
Link,
useLoaderData,
} from 'react-router-dom';
import { getContact } from './contacts';
export default function Root() {
const { contacts } = useLoaderData();
return (
<>
<div id="sidebar">
{ ... }
<nav>
{contacts.length ? (
<ul>
{contacts.map((contact) => (
<li key={contact.id}>
<Link to={`contacts/${contact.id}`}>
{contact.first || contact.last ? (
<>
{contact.first} {contact.last}
</>
) : (
<i>No Name</i>
)}{" "}
{contact.favorite && <span>★</span>}
</Link>
</li>
))}
</ul>
) : (
<p>
<i>No contacts</i>
</p>
)}
</nav>
</div>
<div id="detail"> <Outlet /> </div>
</>
);
}
使用 useLoaderData()
就可以获取 loader
中的数据。是不是很简单。
现在是一个空的联系人列表。我们继续:
我们在工具函数库 utils.jsx中增加函数 createContact()
,
import { getContacts, createContact } from "./contacts";
...
// 创建联系人
export async function action() {
const contact = await createContact();
return { contact };
}
继续修改Root.jsx组件:
一般的网页中我们提交表单都是通过
元素中写处理表单的服务器接口, 本示例的做法是提交表单是向路由提交,至于提交后的如何处理则由路由的action
来做决定。总之最终的数据是由action
来负责请求返回的。要想向路由提交表单,必须要用到 React Router Dom
中的 Form
组件功能。下面我们把 form
改用成 Form
,如例如示。
// Root.jsx
import {
Outlet,
Link,
useLoaderData,
Form,
} from "react-router-dom";
import { getContacts, createContact } from "./contacts";
/* ... */
export default function Root() {
const { contacts } = useLoaderData();
return (
<>
<div id="sidebar">
<h1>React Router Contacts</h1>
<div>
{/* ... */}
<Form method="post">
<button type="submit">New</button>
</Form>
</div>
{/* ... */}
</div>
</>
);
}
那么我们还要向路由添加 action
功能。
修改 routerConfig.jsx 配置文件,增加 action :
import { createBrowserRouter } from "react-router-dom";
import Root from "./Root";
import Error404 from "./Error404";
import Contact from "./Contact";
import {
rootLoader,
action as rootAction,
} from "./utils";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <Error404 />,
loader: rootLoader,
action: rootAction,
children: [
{
path: "contacts/:contactId",
element: <Contact />,
},
]
}
]);
export default router;
现在我们单击新建联系人按钮只是增加了一个空的联系人。不过没有关系,后面我们还要继续讲解,我们下回见分晓。