适合初学者的综合 React Router 6.x 备忘清单
入门
安装使用
1 2 3 4 5 6 7 8 9
| $ npm create vite@latest myApp --\ --template react
$ cd <myApp> $ npm install react-router-dom \ localforage \ match-sorter \ sort-by $ npm run dev
|
添加路由器
{3-6,8-13,19}1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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> );
|
根路由
{1,6}1 2 3 4 5 6 7 8
| import Root from "./routes/root";
const router = createBrowserRouter([ { path: "/", element: <Root />, }, ]);
|
处理未找到错误
{1,7}1 2 3 4 5 6 7 8 9
| import ErrorPage from "./error-page";
const router = createBrowserRouter([ { path: "/", element: <Root />, errorElement: <ErrorPage />, }, ]);
|
{1,9-12}1 2 3 4 5 6 7 8 9 10 11 12 13
| import Contact from "./routes/contact";
const router = createBrowserRouter([ { path: "/", element: <Root />, errorElement: <ErrorPage />, }, { path: "contacts/:contactId", element: <Contact />, }, ]);
|
嵌套路由
src/main.jsx
{6-11}1 2 3 4 5 6 7 8 9 10 11 12 13
| const router = createBrowserRouter([ { path: "/", element: <Root />, errorElement: <ErrorPage />, children: [ { path: "contacts/:contactId", element: <Contact />, }, ], }, ]);
|
src/routes/root.jsx
{1,8}1 2 3 4 5 6 7 8 9 10 11 12
| import { Outlet } from "react-router-dom";
export default function Root() { return ( <> {/* 所有其他元素 */} <div id="detail"> <Outlet /> </div> </> ); }
|
客户端路由
{2,9,14}1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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> ); }
|
创建联系人
创建动作并将 <form>
更改为 <Form>
{5,8,11-14,20-22}1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 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
{3,12}1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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 参数
{3}1 2 3 4 5 6
| [ { path: "contacts/:contactId", element: <Contact />, }, ];
|
:contactId
URL 段。 冒号(:)具有特殊含义,将其变成“动态段
”
{1-4,6-8,11}1 2 3 4 5 6 7 8 9 10 11 12 13
| 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(); }
|
在路由上配置 loader
{2,16}1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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, }, ], }, ]);
|
更新数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| 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> ); }
|
添加新的编辑路由
{1,16-20}1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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, }, ], }, ]);
|
活动链接样式
{2,7-8}1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { NavLink, } from "react-router-dom";
<NavLink to={`contacts/${contact.id}`} className={({ isActive, isPending }) => isActive ? "active" : isPending ? "pending" : "" } > {/* other code */} </NavLink>
|
全局待定用户界面
{7,13-14}1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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> ); }
|
向编辑模块添加一个动作
{4,6,8-13}1 2 3 4 5 6 7 8 9 10 11 12 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}`); }
|
将动作连接到路由
{2,22}1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| 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, }, ], }, ]);
|
删除记录
{3}1 2 3 4 5 6 7 8 9 10 11
| <Form method="post" action="destroy" onSubmit={(event) => { if (!confirm("请确认您要删除此记录")) { event.preventDefault(); } }} > <button type="submit">删除</button> </Form>
|
添加销毁动作
1 2 3 4 5 6 7
| import {redirect} from "react-router-dom"; import {deleteContact} from "../contacts";
export async function action({ params }) { await deleteContact(params.contactId); return redirect("/"); }
|
将 destroy
路由添加到路由配置中
{2,9-12}1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import { action as destroyAction } from "./routes/destroy";
const router = createBrowserRouter([ { path: "/", children: [ { path: "contacts/:contactId/destroy", action: destroyAction, }, ], }, ]);
|
上下文错误
{2}1 2 3 4 5
| export async function action({ params }) { throw new Error("oh dang!"); await deleteContact(params.contactId); return redirect("/"); }
|
让我们为 destroy
路由创建上下文错误消息:
{5}1 2 3 4 5 6 7
| [ { path: "contacts/:contactId/destroy", action: destroyAction, errorElement: <div>哎呀!有一个错误</div>, }, ];
|
首页路由
{1,11}1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import Index from "./routes/index";
const router = createBrowserRouter([ { path: "/", element: <Root />, errorElement: <ErrorPage />, loader: rootLoader, action: rootAction, children: [ { index: true, element: <Index /> }, ], }, ]);
|
取消按钮
{5,10,18-20}1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| 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> ); }
|
使用客户端路由获取提交
将 <form>
更改为 <Form>
{1,9}1 2 3 4 5 6 7 8 9
| <Form id="search-form" role="search"> <input id="q" aria-label="Search contacts" placeholder="Search" type="search" name="q" /> </Form>
|
如果有 URLSearchParams
过滤列表
{1,4}1 2 3 4 5 6
| 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 同步到表单状态
{5,9,20}1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 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 搜索参数同步
{1,7-9}1 2 3 4 5 6 7 8 9 10
| import { useEffect } from "react";
export default function Root() { const { contacts, q } = useLoaderData(); const navigation = useNavigation();
useEffect(() => { document.getElementById("q").value = q; }, [q]); }
|
{2,8,19-21}1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| 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 |
# |
createMemoryRouter |
# |
createHashRouter |
# |
以下路由器不支持数据 data
API:
:– |
– |
<BrowserRouter> |
# |
<MemoryRouter> |
# |
<HashRouter> |
# |
<NativeRouter> |
# |
<StaticRouter> |
# |
路由示例
快速更新到 v6.4 的最简单方法是从 createRoutesFromElements 获得帮助,因此您无需将 <Route>
元素转换为路由对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 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
1 2 3 4 5 6 7
| function createBrowserRouter( routes: RouteObject[], opts?: { basename?: string; window?: Window; } ): RemixRouter;
|
routes
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| createBrowserRouter([ { path: "/", element: <Root />, loader: rootLoader, children: [ { path: "events/:id", element: <Event />, loader: eventLoader, }, ], }, ]);
|
basename
用于您无法部署到域的根目录,而是部署到子目录的情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| createBrowserRouter(routes, { basename: "/app", });
createBrowserRouter(routes, { basename: "/app", }); <Link to="/" />;
createBrowserRouter(routes, { basename: "/app/", }); <Link to="/" />;
|
createHashRouter
{4,11}1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 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
{2-3,24-27}1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| 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
1 2 3
| createMemoryRouter(routes, { initialEntries: ["/", "/events/123"], });
|
initialIndex
{3}1 2 3 4 5
| createMemoryRouter(routes, { initialEntries: ["/", "/events/123"], initialIndex: 1, });
|
<RouterProvider>
{26}1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 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 来向用户表明该应用程序正在运行
{3}1 2 3 4
| <RouterProvider router={router} fallbackElement={<SpinnerOfDoom />} />
|
Router Components
<BrowserRouter>
{4}1 2 3 4 5 6 7 8 9 10 11 12
| 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>
{4}1 2 3 4 5 6 7 8 9 10 11 12
| 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>
{3}1 2 3 4 5 6 7 8 9 10 11 12
| import * as React from "react"; import { NativeRouter } from "react-router-native";
function App() { return ( <NativeRouter> {/* 你的应用程序的其余部分在这里 */} </NativeRouter> ); }
|
<NativeRouter>
是在 React Native 应用程序中运行 React Router 的推荐接口
<MemoryRouter>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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);
|
另见