我们书接上回,继续我们的React Router 路由之路:
我们到目前为止都没有用到 state、useEffect、redux等状态管理器。但也达到了我们的设计目的。
注意,action 返回的结果 可以在组件中使用
useActionData()
来获取。就像useLoaderData()
的使用一样。
loader 中的 URL参数
接着上回的示例文件, 新建几个无名联系人。虽然联系人信息没有变化,但我们仔细查看地址栏,会发现 ID
是会发生变化的。因为我们在createContact()
中为每个联系人分配了一个ID号。
我们再次查看路由信息,
...
{path: "contacts/:contactId",element: <Contact />,
},...
在上节中我们已经详细的说过,通过 :contactId
路由变量,我们可以获取到这个符在地址中的 id
号的。我们再次对 utils.jsx
进行修改:注意里面的备注信息,很重要。
import { getContacts, createContact, getContact } from "./contacts";// 模拟网络请求,获取数据
export async function rootLoader() {const contacts = await getContacts();return { contacts };
}//创建新的联系人
export async function rootAction() {const contact = await createContact();return { contact };
}// 根据 URL 中的ID 获取对应的联系人信息。变量 params 是一个对象,包含了 URL 中的参数。
// 其中 contactId 就是路由变量中的 :contactId,即 /contacts/:contactId。同名变量。
export async function contactLoader({ params }) {const contact = await getContact(params.contactId);return { contact };
}
为了不造成理解上的概念混淆,我们把原先的action
更名为 rootAction
, 新增了 contactLoader
函数,你应该理解过来了,这个loader是用有 组件的路由配置中的。 然后在 routerConfig.jsx
中进行修改。
import { createBrowserRouter } from "react-router-dom";import Root from "./Root";
import Error404 from "./Error404";
import Contact from "./Contact";
import {rootLoader,rootAction,contactLoader,
} from "./utils";const router = createBrowserRouter([{path: "/",element: <Root />,errorElement: <Error404 />,loader: rootLoader,action: rootAction,children: [{path: "contacts/:contactId",element: <Contact />,loader: contactLoader},]}
]);export default router;
现在组件<Contact/>
加载后就能获取到相应的 contact
信息了,我们只要在组件内用 useLoaderData()
就能获取到。对Contact.jsx
进行修改:
// Contact.jsximport {Form,useLoaderData,
} from "react-router-dom";
import Favorite from "./Favorite";export default function Contact() {const { contact } = useLoaderData();// const contact = {// first: "Your",// last: "Name",// avatar: "https://placekitten.com/g/200/200",// twitter: "your_handle",// notes: "Some notes",// favorite: true,// };...}
将之前表态定义的联系人信息删掉,用 上面的替换。 上面我已经注释了。
更新联系人信息
就像创建数据一样,您可以使用 更新数据。创建 EditContact.jsx 文件。
// Edit.jsximport { Form, useLoaderData } from "react-router-dom";export default function EditContact() {const { contact } = useLoaderData();return (<Form method="post" id="contact-form"><p><span>Name</span><inputplaceholder="First"aria-label="First name"type="text"name="first"defaultValue={contact.first}/><inputplaceholder="Last"aria-label="Last name"type="text"name="last"defaultValue={contact.last}/></p><label><span>Twitter</span><inputtype="text"name="twitter"placeholder="@jack"defaultValue={contact.twitter}/></label><label><span>Avatar URL</span><inputplaceholder="https://reactrouter.com/_docs/tutorial/12.webp"aria-label="Avatar URL"type="text"name="avatar"defaultValue={contact.avatar}/></label><label><span>Notes</span><textareaname="notes"defaultValue={contact.notes}rows={6}/></label><p><button type="submit">Save</button><button type="button">Cancel</button></p></Form>);
}
增加路由信息,将编辑组件添加到路由中
// routerConfig.jsximport { createBrowserRouter } from "react-router-dom";import Root from "./Root";
import Error404 from "./Error404";
import Contact from "./Contact";
import EditContact from "./EditContact";
import {rootLoader,rootAction,contactLoader,
} from "./utils";const router = createBrowserRouter([{path: "/",element: <Root />,errorElement: <Error404 />,loader: rootLoader,action: rootAction,children: [{path: "contacts/:contactId",element: <Contact />,loader: contactLoader},{path: "contacts/:contactId/edit",element: <EditContact />,loader: contactLoader,},]}
]);export default router;
配置在子路由中的目的就是在 Root
中的 <Outlet/>
的位置中显示的。
注意路由的配置,当我们点击 Edit 按钮后,注意查看 URL 的变化,如:http://localhost:5173/contacts/uiahrwo/edit
, 一眼就能看出可以匹配 ath: "contacts/:contactId/edit"
的配置信息。
现在在界面应该如下所示:
现在我人需要将编辑的信息反映到 action
中来更新信息。在 utils.jsx
中创建 editAction()
, 并配置到路由中。文件中的参数我已经作了详细的说明。
// utils.jsximport { redirect } from "react-router-dom";
import { getContacts, createContact, getContact, updateContact } from "./contacts";...// 更新联系人信息
// request 是一个对象,包含了请求的所有信息,包括请求头、请求体等。
// params 是一个对象,包含了 URL 中的参数,即路由变量。
// 通过 request.formData() 可以获取到表单数据,返回一个 FormData 对象。
// FormData 对象是一个键值对集合,每个键对应一个值,值可以是字符串,也可以是 Blob 对象。
// 通过 Object.fromEntries() 可以将 FormData 对象转换为一个普通的对象。
// 通过 updateContact() 更新联系人信息。
// redirect() 可以重定向到指定的 URL。
export async function editAction({ request, params }) {const formData = await request.formData();const updates = Object.fromEntries(formData);await updateContact(params.contactId, updates);return redirect(`/contacts/${params.contactId}`);
}
更新路由配置信息:
// routerConfig.jsximport { createBrowserRouter } from "react-router-dom";import Root from "./Root";
import Error404 from "./Error404";
import Contact from "./Contact";
import EditContact from "./EditContact";
import {rootLoader,rootAction,contactLoader,editAction,
} from "./utils";const router = createBrowserRouter([{path: "/",element: <Root />,errorElement: <Error404 />,loader: rootLoader,action: rootAction,children: [{path: "contacts/:contactId",element: <Contact />,loader: contactLoader},{path: "contacts/:contactId/edit",element: <EditContact />,loader: contactLoader,action: editAction},]}
]);export default router;
增加活动链接状态
现在我们有一堆记录,不清楚我们在侧边栏中查看的是哪一条。我们可以使用 NavLink
来解决这个问题。
将 Root
中的 Link
替换为 NavLink
,如下所示:
import {Outlet,Link,useLoaderData,Form,NavLink,
} from 'react-router-dom';export default function Root() {const { contacts } = useLoaderData();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>{contacts.length ? (<ul>{contacts.map((contact) => (<li key={contact.id}><NavLinkto={`contacts/${contact.id}`}className={({ isActive, isPending }) =>isActive? "active": isPending? "pending": ""}>{contact.first || contact.last ? (<>{contact.first} {contact.last}</>) : (<i>No Name</i>)}{" "}{contact.favorite && <span>★</span>}</NavLink></li>))}</ul>) : (<p><i>No contacts</i></p>)}</nav></div><div id="detail"> <Outlet /> </div></>);
}
请注意,我们将一个函数传递给 className
。当用户位于 NavLink
中的 URL 时,isActive
将为 true
。当它即将处于活动状态(数据仍在加载)时,isPending
将为 true
。这使我们能够轻松指示用户的位置,并就已点击但仍在等待数据加载的链接提供即时反馈。
有时候我们希望数据在加载的时候有个状态反馈,如下图所示:
路由的加载状态可以通过 useNavigation 来获取。我们再次对 Root.jsx 进修修改:
import {// 其它代码// ... ...useNavigation,
} from "react-router-dom";// ...export default function Root() {const { contacts } = useLoaderData();const navigation = useNavigation();return (<><div id="sidebar">{/* ... */}</div><divid="detail"className={navigation.state === "loading" ? "loading" : ""}><Outlet /></div></>);
}
由于我们在本地缓存了数据,所以加载速度很快,看不到这个效果,但如果是从网络上执行耗时的操作时,这种效果就很明显了。
删除联系人
现在我们再来完善我们的应用,当我们点击删除按钮后,我们要把 联系人的 ID
传递给路由,再由相应的action
去完成删除工作。
查看 Contact.jsx
文件
<Formmethod="post"action="destroy"onSubmit={(event) => {if (!confirm("Please confirm you want to delete this record.")) {event.preventDefault();}}}
><button type="submit">Delete</button>
</Form>
仔细查看<Form>
中的参数, action
提交到 destroy
路由,这个路由地址是相对地址。由于Contact
的路由是 contacts/:contactId
, 所以这个destroy
的路径为 contacts/:contactId/destroy
,
我们先创建这个路由的action
函数: deleteAction()
,
// utils.jsximport { redirect } from "react-router-dom";
import {getContacts,createContact,getContact,updateContact,deleteContact
} from "./contacts";...// 删除联系人
export async function destroyAction({ params }) {await deleteContact(params.contactId);return redirect("/");
}
并将它配置到路由中
... import { action as destroyAction } from "./routes/destroy";const router = createBrowserRouter([{path: "/",...children: [...{path: "contacts/:contactId/destroy",action: destroyAction,},],},
]);...
由于我们没有在子路由中配置 errorElement
,这是因为我们在destroyAction
中直接重定向到 " / ", 所以这个element
就没有创建的必要了。事实上也的确没有这个需求
// routerConfig.jsx[...{path: "contacts/:contactId/destroy",action: destroyAction,errorElement: <div>Oops! There was an error.</div>,},
];
还有一点非常重要,Form 的 提交方法为 post 时才能激活路由中的action操作。
索引路由
每当我们加载这个App时,你会发现右侧是一个空页面。就像下面这样:
当一条路由有子路由时,而当前页面又处在父路由层级时,<Outlet>
没有任何子路由与之匹配,所以就没有可渲染的界面。这个时候可以将索引路由视为填充该空间的默认子路由。
创建 Index 组件如下所示
// Index.jsxexport default function Index() {return (<p id="zero-state">This is a demo for React Router.<br />Check out{" "}<a href="https://reactrouter.com">the docs at reactrouter.com</a>.</p>);
}
配置这个Index到路由
// routerConfig.jsx...import Index from "./Index";...const router = createBrowserRouter([{path: "/",element: <Root />,errorElement: <Error404 />,loader: rootLoader,action: rootAction,children: [{ index: true, element: <Index /> },...]}
]);export default router;
这个时候我们进入App重新渲染后界面如下:
完美。
现在还有最后一个功能没有完成,就是搜索功能,我们希望根据输入的关键词来搜索出联系人。
URL的搜索参数与Get提交
传统的html
表单中,如果没有指定提交方法的话默认为get
方式提交到服务器,如我们Root.jsx
中搜索框部分,这自然不是我们想要的结果,我们只是想把输入参数反应到地址栏中而不影响浏览器的变化,正好,Form
可以做到。我们把Root
中的 html
元素 form
改成 React Router
中的 Form
组件就好了,就像下面这样:
// Root.jsx
...
<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>
...
现在你的搜索栏中输入些内容回车后,你会发现浏览器的地址栏信息也会发会变化,但浏览器并没有网络请求操作。这正是我们目的,
现在我们修改 rootLoader()函数, 以获取get参数,并根据这个参数做出筛选联系人的反应:
// utils.jsx...// 模拟网络请求,获取数据
export async function rootLoader({request}) {const url = new URL(request.url);const q = url.searchParams.get("q");const contacts = await getContacts(q);return { contacts };
}
...
现在就很美了。
再次强调:因为这是一个
get
的,而不是post
,React Router
路由器不调用action
。提交GET
表格与单击链接相同:只有URL更改。这就是为什么我们添加的过滤代码在loader中,而不是在 action 中。
这也意味着这是一个普通的页面导航。您可以单击“后退”按钮以返回到自己的位置。
最后
关于路由的主要应用技术我已经讲完了,根据这些应用方法,配置 React的状态管理,会使我们的应用更加灵活,设计更加方便。只要多练习,多思考,就一定能开发出非常完美的产品。