React Router 备忘清单 === [](https://npmjs.org/package/react-router-dom) [](https://www.npmjs.com/package/react-router-dom) [](https://github.com/remix-run/react-router/network/dependents) [](https://github.com/remix-run/react-router) 适合初学者的综合 [React Router 6.x](https://reactrouter.com/en/main/start/overview) 备忘清单 <!--rehype:style=padding-top: 12px;--> 入门 ---- ### 安装使用 ```bash $ npm create vite@latest myApp --\ --template react # 按照提示操作 $ cd <myApp> $ npm install react-router-dom \ localforage \ match-sorter \ sort-by $ npm run dev ``` ### 添加路由器 <!--rehype:wrap-class=row-span-2--> ```jsx {3-6,8-13,19} import React from "react"; import ReactDOM from "react-dom/client"; import { createBrowserRouter, RouterProvider, } from "react-router-dom"; const router = createBrowserRouter([ { path: "/", element: <div>Hello world!</div>, }, ]); const root=document.getElementById('root'); ReactDOM.createRoot(root).render( <React.StrictMode> <RouterProvider router={router} /> </React.StrictMode> ); ``` ### 根路由 ```jsx {1,6} import Root from "./routes/root"; const router = createBrowserRouter([ { path: "/", element: <Root />, }, ]); ``` ### 处理未找到错误 ```jsx {1,7} import ErrorPage from "./error-page"; const router = createBrowserRouter([ { path: "/", element: <Root />, errorElement: <ErrorPage />, }, ]); ``` ### `contacts` 用户界面 ```jsx {1,9-12} import Contact from "./routes/contact"; const router = createBrowserRouter([ { path: "/", element: <Root />, errorElement: <ErrorPage />, }, { path: "contacts/:contactId", element: <Contact />, }, ]); ``` ### 嵌套路由 `src/main.jsx` ```jsx {6-11} const router = createBrowserRouter([ { path: "/", element: <Root />, errorElement: <ErrorPage />, children: [ { path: "contacts/:contactId", element: <Contact />, }, ], }, ]); ``` `src/routes/root.jsx` ```jsx {1,8} import { Outlet } from "react-router-dom"; export default function Root() { return ( <> {/* 所有其他元素 */} <div id="detail"> <Outlet /> </div> </> ); } ``` ### 客户端路由 ```jsx {2,9,14} import { Outlet, Link } from "react-router-dom"; export default function Root() { return ( <ul> <li> <Link to={`contacts/1`}> Your Name </Link> </li> <li> <Link to={`contacts/2`}> Your Friend </Link> </li> </ul> ); } ``` ### 创建联系人 <!--rehype:wrap-class=row-span-2--> 创建动作并将 `<form>` 更改为 `<Form>` ```jsx {5,8,11-14,20-22} import { Outlet, Link, useLoaderData, Form, } from "react-router-dom"; import { getContacts, createContact } from "../contacts"; export async function action() { const contact = await createContact(); return { contact }; } export default function Root() { const { contacts } = useLoaderData(); return ( <div id="sidebar"> <h1>React Router Contacts</h1> <Form method="post"> <button type="submit">New</button> </Form> </div> ); } ``` 导入并设置路由上的 `action` ```jsx {3,12} import Root, { loader as rootLoader, action as rootAction, } from "./routes/root"; const router = createBrowserRouter([ { path: "/", element: <Root />, errorElement: <ErrorPage />, loader: rootLoader, action: rootAction, children: [ { path: "contacts/:contactId", element: <Contact />, }, ], }, ]); ``` ### 加载程序中的 URL 参数 <!--rehype:wrap-class=row-span-3--> ```jsx {3} [ { path: "contacts/:contactId", element: <Contact />, }, ]; ``` `:contactId` URL 段。 冒号(<pur>:</pur>)具有特殊含义,将其变成“`动态段`” ```jsx {1-4,6-8,11} import { useLoaderData } from "react-router-dom"; import { getContact } from "../contacts"; export async function loader({ params }) { return getContact(params.contactId); } export default function Contact() { const contact = useLoaderData(); // existing code } ``` 在路由上配置 `loader` ```jsx {2,16} import Contact, { loader as contactLoader, } from "./routes/contact"; const router = createBrowserRouter([ { path: "/", element: <Root />, errorElement: <ErrorPage />, loader: rootLoader, action: rootAction, children: [ { path: "contacts/:contactId", element: <Contact />, loader: contactLoader, }, ], }, ]); ``` ### 更新数据 <!--rehype:wrap-class=row-span-3--> ```jsx import { Form, useLoaderData } from "react-router-dom"; export default function EditContact() { const contact = useLoaderData(); return ( <Form method="post" id="contact-form"> <label> <span>Twitter</span> <input type="text" name="twitter" placeholder="@jack" defaultValue={contact.twitter} /> </label> <label> <span>Notes</span> <textarea name="notes" defaultValue={contact.notes} rows={6} /> </label> <p> <button type="submit">保存</button> <button type="button">取消</button> </p> </Form> ); } ``` 添加新的编辑路由 ```jsx {1,16-20} import EditContact from "./routes/edit"; const router = createBrowserRouter([ { path: "/", element: <Root />, errorElement: <ErrorPage />, loader: rootLoader, action: rootAction, children: [ { path: "contacts/:contactId", element: <Contact />, loader: contactLoader, }, { path: "contacts/:contactId/edit", element: <EditContact />, loader: contactLoader, }, ], }, ]); ``` ### 活动链接样式 ```jsx {2,7-8} import { NavLink, } from "react-router-dom"; <NavLink to={`contacts/${contact.id}`} className={({ isActive, isPending }) => isActive ? "active" : isPending ? "pending" : "" } > {/* other code */} </NavLink> ``` ### 全局待定用户界面 ```jsx {7,13-14} import { useNavigation, } from "react-router-dom"; export default function Root() { const { contacts } = useLoaderData(); const navigation = useNavigation(); return ( <div id="detail" className={ navigation.state === 'loading' ? 'loading' : '' } > <Outlet /> </div> ); } ``` ### 使用 FormData 更新联系人 <!--rehype:wrap-class=col-span-2--> 向编辑模块添加一个动作 ```jsx {4,6,8-13} 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}`); } ``` 将动作连接到路由 ```jsx {2,22} import EditContact, { action as editAction, } from "./routes/edit"; const router = createBrowserRouter([ { path: "/", element: <Root />, errorElement: <ErrorPage />, loader: rootLoader, action: rootAction, children: [ { path: "contacts/:contactId", element: <Contact />, loader: contactLoader, }, { path: "contacts/:contactId/edit", element: <EditContact />, loader: contactLoader, action: editAction, }, ], }, ]); ``` ### 删除记录 ```jsx {3} <Form method="post" action="destroy" onSubmit={(event) => { if (!confirm("请确认您要删除此记录")) { event.preventDefault(); } }} > <button type="submit">删除</button> </Form> ``` 添加销毁动作 ```jsx import {redirect} from "react-router-dom"; import {deleteContact} from "../contacts"; export async function action({ params }) { await deleteContact(params.contactId); return redirect("/"); } ``` 将 `destroy` 路由添加到路由配置中 ```jsx {2,9-12} import { action as destroyAction } from "./routes/destroy"; const router = createBrowserRouter([ { path: "/", children: [ { path: "contacts/:contactId/destroy", action: destroyAction, }, ], }, ]); ``` ### 上下文错误 ```jsx {2} export async function action({ params }) { throw new Error("oh dang!"); await deleteContact(params.contactId); return redirect("/"); } ``` 让我们为 `destroy` 路由创建上下文错误消息: ```jsx {5} [ { path: "contacts/:contactId/destroy", action: destroyAction, errorElement: <div>哎呀!有一个错误</div>, }, ]; ``` ### 首页路由 ```jsx {1,11} import Index from "./routes/index"; const router = createBrowserRouter([ { path: "/", element: <Root />, errorElement: <ErrorPage />, loader: rootLoader, action: rootAction, children: [ { index: true, element: <Index /> }, ], }, ]); ``` ### 取消按钮 ```jsx {5,10,18-20} import { Form, useLoaderData, redirect, useNavigate, } from "react-router-dom"; export default function Edit() { const contact = useLoaderData(); const navigate = useNavigate(); return ( <Form method="post" id="contact-form"> <div> <button type="submit">保存</button> <button type="button" onClick={() => { navigate(-1); }} > 取消 </button> </div> </Form> ); } ``` ### 使用客户端路由获取提交 <!--rehype:wrap-class=row-span-2--> 将 `<form>` 更改为 `<Form>` ```jsx {1,9} <Form id="search-form" role="search"> <input id="q" aria-label="Search contacts" placeholder="Search" type="search" name="q" /> </Form> ``` 如果有 `URLSearchParams` 过滤列表 ```jsx {1,4} export async function loader({ request }) { const url = new URL(request.url); const q = url.searchParams.get("q"); const contacts = await getContacts(q); return { contacts }; } ``` ### 将 URL 同步到表单状态 <!--rehype:wrap-class=row-span-2--> ```jsx {5,9,20} 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 }; } export default function Root() { const { contacts, q } = useLoaderData(); const navigation = useNavigation(); return ( <Form id="search-form" role="search"> <input id="q" aria-label="Search contacts" placeholder="Search" type="search" name="q" defaultValue={q} /> {/* existing code */} </Form> ); } ``` ### 将输入值与 URL 搜索参数同步 ```jsx {1,7-9} import { useEffect } from "react"; export default function Root() { const { contacts, q } = useLoaderData(); const navigation = useNavigation(); useEffect(() => { document.getElementById("q").value = q; }, [q]); } ``` ### 提交变更 Forms ```jsx {2,8,19-21} import { useSubmit, } from "react-router-dom"; export default function Root() { const { contacts, q } = useLoaderData(); const navigation = useNavigation(); const submit = useSubmit(); return ( <Form id="search-form" role="search"> <input id="q" aria-label="Search contacts" placeholder="Search" type="search" name="q" defaultValue={q} onChange={(event) => { submit(event.currentTarget.form); }} /> {/* existing code */} </Form> ); } ``` Routers --- ### 挑选路由器 在 v6.4 中,引入了支持新数据 API 的新路由器: :-- | -- :-- | -- `createBrowserRouter` | [#](https://reactrouter.com/en/main/routers/create-browser-router) `createMemoryRouter` | [#](https://reactrouter.com/en/main/routers/create-memory-router) `createHashRouter` | [#](https://reactrouter.com/en/main/routers/create-hash-router) 以下路由器不支持数据 `data` API: :-- | -- :-- | -- `<BrowserRouter>` | [#](https://reactrouter.com/en/main/router-components/browser-router) `<MemoryRouter>` | [#](https://reactrouter.com/en/main/router-components/memory-router) `<HashRouter>` | [#](https://reactrouter.com/en/main/router-components/hash-router) `<NativeRouter>` | [#](https://reactrouter.com/en/main/router-components/native-router) `<StaticRouter>` | [#](https://reactrouter.com/en/main/router-components/static-router) ### 路由示例 <!--rehype:wrap-class=col-span-2--> 快速更新到 v6.4 的最简单方法是从 createRoutesFromElements 获得帮助,因此您无需将 `<Route>` 元素转换为路由对象 ```jsx import { createBrowserRouter, createRoutesFromElements, Route, RouterProvider, } from "react-router-dom"; const router = createBrowserRouter( createRoutesFromElements( <Route path="/" element={<Root />}> <Route path="dashboard" element={<Dashboard />} /> {/* ... etc. */} </Route> ) ); ReactDOM.createRoot(document.getElementById("root")).render( <React.StrictMode> <RouterProvider router={router} /> </React.StrictMode> ); ``` ### createBrowserRouter <!--rehype:wrap-class=row-span-2--> ```jsx import * as React from "react"; import * as ReactDOM from "react-dom"; import { createBrowserRouter, RouterProvider, } from "react-router-dom"; import Root, {rootLoader} from "./root"; import Team, {teamLoader} from "./team"; const router = createBrowserRouter([ { path: "/", element: <Root />, loader: rootLoader, children: [ { path: "team", element: <Team />, loader: teamLoader, }, ], }, ]); const root=document.getElementById('root'); ReactDOM.createRoot(root).render( <RouterProvider router={router} /> ); ``` Type Declaration ```jsx function createBrowserRouter( routes: RouteObject[], opts?: { basename?: string; window?: Window; } ): RemixRouter; ``` routes ```jsx createBrowserRouter([ { path: "/", element: <Root />, loader: rootLoader, children: [ { path: "events/:id", element: <Event />, loader: eventLoader, }, ], }, ]); ``` `basename` 用于您无法部署到域的根目录,而是部署到子目录的情况 ```jsx createBrowserRouter(routes, { basename: "/app", }); createBrowserRouter(routes, { basename: "/app", }); <Link to="/" />; // results in <a href="/app" /> createBrowserRouter(routes, { basename: "/app/", }); <Link to="/" />; // results in <a href="/app/" /> ``` ### createHashRouter ```jsx {4,11} import * as React from "react"; import * as ReactDOM from "react-dom"; import { createHashRouter, RouterProvider, } from "react-router-dom"; import Root, { rootLoader } from "./root"; import Team, { teamLoader } from "./team"; const router = createHashRouter([ { path: "/", element: <Root />, loader: rootLoader, children: [ { path: "team", element: <Team />, loader: teamLoader, }, ], }, ]); const root=document.getElementById('root'); ReactDOM.createRoot(root).render( <RouterProvider router={router} /> ); ``` ### createMemoryRouter <!--rehype:wrap-class=row-span-2--> ```jsx {2-3,24-27} import { RouterProvider, createMemoryRouter, } from "react-router-dom"; import * as React from "react"; import { render, waitFor, screen, } from "@testing-library/react"; import "@testing-library/jest-dom"; import CalendarEvent from "./event"; test("event route", async () => { const FAKE_EVENT = { name: "测试事件" }; const routes = [ { path: "/events/:id", element: <CalendarEvent />, loader: () => FAKE_EVENT, }, ]; const router=createMemoryRouter(routes,{ initialEntries: ["/", "/events/123"], initialIndex: 1, }); render( <RouterProvider router={router} /> ); await waitFor( () => screen.getByRole("heading") ); expect(screen.getByRole("heading")) .toHaveTextContent( FAKE_EVENT.name ); }); ``` initialEntries ```jsx createMemoryRouter(routes, { initialEntries: ["/", "/events/123"], }); ``` initialIndex ```jsx {3} createMemoryRouter(routes, { initialEntries: ["/", "/events/123"], initialIndex: 1, // start at "/events/123" }); ``` ### \<RouterProvider> ```jsx {26} import { createBrowserRouter, RouterProvider, } from "react-router-dom"; const router = createBrowserRouter([ { path: "/", element: <Root />, children: [ { path: "dashboard", element: <Dashboard />, }, { path: "about", element: <About />, }, ], }, ]); const root=document.getElementById('root'); ReactDOM.createRoot(root).render( <RouterProvider router={router} fallbackElement={<BigSpinner />} /> ); ``` `fallbackElement` 如果您不是服务器渲染您的应用程序,DataBrowserRouter 将在安装时启动所有匹配的路由加载器。 在此期间,您可以提供一个 fallbackElement 来向用户表明该应用程序正在运行 ```jsx {3} <RouterProvider router={router} fallbackElement={<SpinnerOfDoom />} /> ``` Router Components --- ### \<BrowserRouter> ```jsx {4} import * as React from "react"; import * as ReactDOM from "react-dom"; import { BrowserRouter } from "react-router-dom"; const root=document.getElementById('root'); ReactDOM.createRoot(root).render( <HashRouter> {/* 你的应用程序的其余部分在这里 */} </HashRouter>, ); ``` `<BrowserRouter>` 使用干净的 `URL` 将当前位置存储在浏览器的地址栏中,并使用浏览器的内置历史堆栈进行导航 ### \<HashRouter> ```jsx {4} import * as React from "react"; import * as ReactDOM from "react-dom"; import { HashRouter } from "react-router-dom"; const root=document.getElementById('root'); ReactDOM.createRoot(root).render( <HashRouter> {/* 你的应用程序的其余部分在这里 */} </HashRouter>, ); ``` `<HashRouter>` 用于 Web 浏览器,因为某些原因不应(或不能)将 URL 发送到服务器 ### \<NativeRouter> ```jsx {3} import * as React from "react"; import { NativeRouter } from "react-router-native"; function App() { return ( <NativeRouter> {/* 你的应用程序的其余部分在这里 */} </NativeRouter> ); } ``` `<NativeRouter>` 是在 React Native 应用程序中运行 React Router 的推荐接口 ### \<MemoryRouter> <!--rehype:wrap-class=col-span-2 row-span-2--> ```jsx import * as React from "react"; import { create } from "react-test-renderer"; import { MemoryRouter, Routes, Route, } from "react-router-dom"; describe("My app", () => { it("renders correctly", () => { let renderer = create( <MemoryRouter initialEntries={["/users/mjackson"]}> <Routes> <Route path="users" element={<Users />}> <Route path=":id" element={<UserProfile />} /> </Route> </Routes> </MemoryRouter> ); expect(renderer.toJSON()).toMatchSnapshot(); }); }); ``` `<MemoryRouter>` 在内部将其位置存储在一个数组中。 与 `<BrowserHistory>` 和 `<HashHistory>` 不同,它不依赖于外部源,例如浏览器中的历史堆栈。 这使得它非常适合需要完全控制历史堆栈的场景,例如测试 ### \<Router> `<Router>` 是所有路由器组件(如 `<BrowserRouter>` 和 `<StaticRouter>`)共享的低级接口。 就 React 而言,`<Router>` 是一个上下文提供者,它向应用程序的其余部分提供路由信息 ### \<StaticRouter> ```jsx import * as React from "react"; import * as ReactDOMServer from "react-dom/server"; import { StaticRouter } from "react-router-dom/server"; import http from "http"; function requestHandler(req, res) { let html = ReactDOMServer.renderToString( <StaticRouter location={req.url}> {/* 你的应用程序的其余部分在这里 */} </StaticRouter> ); res.write(html); res.end(); } http.createServer(requestHandler) .listen(3000); ``` <!--rehype:className=wrap-text--> 另见 --- - [React Router 官网](https://reactrouter.com/)