React Router 完美教程(上)

概述

什么叫路由呢,说白了就是如何处理页面的跳转。在传统的网站中,我们都是向服务器请求页面及相应的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%;
}

准备根组件 Root

所谓根组件就是App的主体界面组件。

export default function Root() {return (<><div id="sidebar"><h1>React Router Contacts</h1><div><form id="search-form" role="search"><inputid="q"aria-label="Search contacts"placeholder="Search"type="search"name="q"/><divid="search-spinner"aria-hiddenhidden={true}/><divclassName="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.jsximport { createBrowserRouter } from "react-router-dom";import Root from "./Root";const router = createBrowserRouter([{path: "/",element: <Root />,},
]);export default router;

createBrowserRouter()函数内是一个json数组。注意,这是一个数组,每个成员都是一个路由对象。

  • path: 跳转的路径
  • element: 当跳转到这个路径时要显示的页面组件。
    当然还有很多成员属性,我们一步一步来。
    现在我们有了路由,还要把它连接到App才能起作用。再次对App.jsx进行修改
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.jsximport { 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.jsximport { 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><imgkey={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><atarget="_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><Formmethod="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"><buttonname="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号传递给组件 <Contact />, 以往我们都是通过Propsredux 等方式进行传递,现在我们可以通过路由传递信息到相应的路由组件中。
注意分析 Root 组件,我们可以把联系人的 ID 信息写在跳转路径中。如 contacts/ID形式。本例中为 contacts/1contacts/2, 现在路径中有了我们联系的ID信息了("1 "和 “2”),那么如何传递到路径组件中呢。
我们可以通过路径变量的形式把对应的部分路径信息传递给组件。路径变量的格式为: :变量名, 如上面的 <Contact />的路径信息中为:contacts/:contactId, 就可以通过 变量contactId 获取到联系人的 ID 号了。
相同的原理,我还可以通过这种方法获取更多路由的信息,如 contacts/:contactId/:contactName等等。

回过话题,我们经过这样的配置后看看效果。这当然不是我们所期望的。我希望在联系人的右侧显示相关联系人的详情信息。就像下面这样。
在这里插入图片描述

如何实现呢,我们再次对路由的配置文件做个修改:

...const router = createBrowserRouter([{path: "/",element: <Root />,errorElement: <Error404 />,children: [{path: "contacts/:contactId",element: <Contact />,},]}
]);
...

我们把第二个页面的路由配置信息移到了第一个的 children 子页面列表里了。这个时候我们再次点击联系人时,会发现路径虽然变化了,但页面没其它变化, 联系人的详细信息没有显示出来。也就是说 <Contact />没有显示出来。 这是因为我们没有在 Root 中给 <Contact /> 组件预留显示位置, 自然也就没有显示。

渲染占位

现在修改 Root.jsx文件, 添加 组件。 它就是一个占位符,为子组件提供显示空间。

import { Outlet } from 'react-router-dom';export default function Root() {return (<>{ ... 其它元素信息 }<div id="detail"> <Outlet /> </div></>);
}

react-router 提供了很多很实用的组件,搭配使用效果更好。我们把 Root 中的 <a></a> 换成 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></>);
}

加载数据

有时我们想在页面加载的过程中就把数据也准备好,这样各组件不用各自向服务器请求数据了,大大节省了网络资源。
我们将使用 loaderuseLoaderData 来加载数据。
我们在Test09目录下创建一个工具函数库文件contacts.jsx,直接把复制进去就行了,这些工具函数只是为了示例而创建,没有其它的实用价值,我们只要知道它们的作用就行了。

// cantacts.jsximport 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.jsximport { 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.jsximport {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组件:
一般的网页中我们提交表单都是通过 <form></form> 元素中写处理表单的服务器接口, 本示例的做法是提交表单是向路由提交,至于提交后的如何处理则由路由的action来做决定。总之最终的数据是由action来负责请求返回的。要想向路由提交表单,必须要用到 React Router Dom中的 Form 组件功能。下面我们把 form 改用成 Form,如例如示。

// Root.jsximport {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;

现在我们单击新建联系人按钮只是增加了一个空的联系人。不过没有关系,后面我们还要继续讲解,我们下回见分晓。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/659416.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Conventional SortSearch

Merge Sort——from bottom to top(iteration) void merge_sort( ElementType list[], ElementType sorted[], int N, int length ){int size1;for(;size<N;size*2){for(int i0;i<N;i2*size){int l1i; int l2sizei;int locl1;while(l1<isize&&l1<N&…

etcd自动化安装配置教程

文章目录 前言一、简介1. 简介2. 特点3. 端口介绍 二、etcd安装教程&#xff08;单机版&#xff09;1. 复制脚本2. 增加执行权限3. 执行脚本4. 查看启动状态5. 卸载etcd 三、etcd安装教程&#xff08;集群版&#xff09;1. 复制脚本2. 增加执行权限3. 分发脚本4. 执行脚本5. 启…

中科大计网学习记录笔记(一):Internet | 网络边缘

计算机网络 前言&#xff1a; 学习视频&#xff1a;中科大郑烇、杨坚全套《计算机网络&#xff08;自顶向下方法 第7版&#xff0c;James F.Kurose&#xff0c;Keith W.Ross&#xff09;》课程 该视频是B站非常著名的计网学习视频&#xff0c;但相信很多朋友和我一样在听完前面…

【JavaSE篇】——内部类

目录 &#x1f393;内部类 &#x1f388;内部类的分类 &#x1f6a9;实例内部类 一.如何实例内部类对象 二.实例内部类中为什么不能有静态成员变量 &#xff08;用final解决&#xff09; 三.在实例内部类对象时&#xff0c;如何访问外部类当中相同的成员变量&#xff1f;…

SpringMVC-基本概念

一、引子 我们在上篇文章Spring集成Web中抛出了一个问题&#xff1a;为什么我们一直在自用Java Web阶段使用的Servlet来承接客户端浏览器的请求呢&#xff0c;我们熟知甚至是已经在日常开发中经常使用的Controller又与之有什么关系呢&#xff1f;我们将在本篇文章解答读者的这…

检测CUDA 是否能访问GPU时回应速度慢【笔记】

SUPWEMICRO 418G-Q20X12 维护记录&#xff1a; 两台设备均已安装CUDA与Pytorch&#xff0c;在检测CUDA 是否能访问GPU&#xff0c;执行torch.cuda.is_available()命令时&#xff0c;一台设备速度秒回应True&#xff0c;但另外一台设备回应速度慢&#xff08;1分钟左右&#xff…

node,node-sass,sass-loader之间的版本关系

前言 安装配置node-sass 以及 sass-loader想必是很多前端的噩梦–一不小心又不成功还得装个半天。 下面说一下这个问题。 当然&#xff0c;你肯定遇到过&#xff1a; Node Sass version 9.0.0 is incompatible with ^4.0.0-这样的问题&#xff0c;这个也是因为三者关系对不上…

【PyQt】02-基本UI

文章目录 前言一、首先了解什么是GUI&#xff1f;二、初学程序1.界面展示代码运行结果 2.控件2.1按钮展示代码运行结果 2.2 纯文本和输入框代码运行结果 3、重新设置大小 -resize4、移动窗口-move()5、设置界面在电脑中央5.1 代码运行结果 6、设置窗口图标代码运行结果 7、布局…

Django模型(二)

一、更新数据库表结构 不管是新增模型,还是修改已有模型后,只需要执行行命令即可: 1.1、创建迁移 在项目根目录的cmd中运行: $ python manage.py makemigrations model_app备注 model_app是子应用的名称,如果不指定,那么就是对所有 INSTALLED_APPS 中的应用都进行预备…

开发数据产品+AI产品通关上岸课程

该课程全面解析数据产品和人工智能产品的开发与设计。学员将学习产品规划、数据分析以及AI技术应用&#xff0c;通过案例实践掌握产品开发流程&#xff0c;致力于帮助他们成功进入数据和人工智能产品领域。 课程大小&#xff1a;9.8G 课程下载&#xff1a;https://download.cs…

Java多线程共享变量控制volatile

1. volatile实现可见性&#xff08;jdk 1.5后&#xff09; 1. 可见性 如果一个线程对共享变量值的修改&#xff0c;能够及时的被其他线程看到&#xff0c;叫做共享变量的可见性。如果一个变量同时在多个线程的工作内存中存在副本&#xff0c;那么这个变量就叫共享变量 volati…

如何从零开始开发一个PS5浏览器 | How to develop a PS5 browser

环境&#xff1a;Windows PS5一台 问题&#xff1a;PS5折腾需要使用PKG浏览器访问特定网址&#xff0c;如何自定义网址呢&#xff1f; 解决办法&#xff1a;使用开发套件PS Multi Tools开发一个空应用&#xff0c;利于deeplinkUri 参数访问网页 背景&#xff1a;PS5折腾后&…

dockerpipwork相关测试过程

pipework可以减轻docker实施过程中的工作量&#xff0c;在网上也找了几篇类似的文章&#xff0c;按照相应配置&#xff0c;结果并不相同 如下测试过程记录下&#xff1a; docker run -it --rm --name c1 busybox docker run -it --rm --name c2 busyboxpipework br1 c1 192…

Altium Designer的学习

PCB设计流程 1.新建空白工程&#xff1a; 创建一个新的工程 新建四个文件&#xff0c;并且保存&#xff1a; 每次打开文件时&#xff0c;打开以.PrjPcb结尾的文件 2.元件符号的创建&#xff1a; 在绘制图形的时候设置成10mil,为了在原理图中显得不那么大。 在绘制引脚的时候设…

拦截器,AOP,自定义注解的使用

自定义注解AOP&#xff0c;实现 进入方法打印参数日志 /*** 定义进入方法前打印日志注解* author zy*/ Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) Documented public interface PrintLog {} /*** 定义一个切面&#xff0c;在使用PrintLog注解的方法进…

Hadoop3.x基础(2)- HDFS

来源&#xff1a;B站尚硅谷 目录 HDFS概述HDFS产出背景及定义HDFS优缺点HDFS组成架构HDFS文件块大小&#xff08;面试重点&#xff09; HDFS的Shell操作&#xff08;开发重点&#xff09;基本语法命令大全常用命令实操准备工作上传下载HDFS直接操作 HDFS的API操作HDFS的API案例…

springboot 怎么设置局域网访问

如何配置Spring Boot应用以实现局域网访问 在开发一个Spring Boot应用时&#xff0c;我们通常会通过localhost来访问和测试我们的应用。但是&#xff0c;当我们想要在局域网中分享我们的应用&#xff0c;供其他设备访问时&#xff0c;仅仅使用localhost是不够的。本文将引导你…

Linux系统卸载重装JDK

CentOS 系统是开发者常用的 Linux 操作系统&#xff0c;安装它时会默认安装自带的旧版本的 OpenJDK&#xff0c;但在开发者平时开发 Java 项目时还是需要完整的 JDK&#xff0c;所以我们部署 CentOS 开发环境时&#xff0c;需要先卸载系统自带的 OpenJDK&#xff0c;再重新安装…

汽车销量可视化分析

目录 一.分析的背景、目的、意义 1、背景 2、目的 3、意义 二.数据来源 三.图表分析 1、汽车品牌销量柱状图 2、中国汽车销量柱状图 3、汽车销量前10排行柱状图 4、汽车厂商销量折线图 ​编辑5、汽车销量词云图 6、汽车车型销量 7、汽车价格分布雷达图 8、汽车分…

免费的ChatGPT网站(7个)

还在为找免费的chatGPT网站或者应用而烦恼吗&#xff1f;博主归纳总结了7个国内非常好用&#xff0c;而且免费的chatGPT网站&#xff0c;AI语言大模型&#xff0c;我们都来接触一下吧。 免费&#xff01;免费&#xff01;免费&#xff01;...&#xff0c;建议收藏保存。 1&…