跳到主要内容

3 篇博文 含有标签「React Router」

查看所有标签

· 阅读需 15 分钟

前言

本文我们讨论如何在 React Router 6 中使用搜索参数。搜索参数是一项强大的功能,它使你能够捕获 URL 中的状态。通过在 URL 中包含状态,你可以与其他人实现共享。例如,如果应用程序显示产品目录,开发人员将使用户能够搜索它。在 React 中,这将转换为项目列表(此处为:产品)和用于过滤它们的 HTML 输入字段。

现在,React 开发人员很有可能会使用 React 的 useState Hook 来管理这种搜索状态。这对这个用户来说很好,但不适合与其他用户共享。

因此,一个不错的方式是在 URL 中管理搜索状态,因为这样搜索状态就可以与其他用户共享。如果一个用户按标题(例如“Rust”)搜索项目列表,则搜索参数将作为键值对附加到 URL,例如 /bookshelf?title=Rust,因为可以与另一个用户共享。所以获得链接的其他用户将在其页面上看到相同的过滤项目列表。

React Router 从状态到 URL

首先,我们将实现上一个所设想的那样,其中有一个项目列表,并通过 HTML 输入字段进行搜索。我们不会使用 React 的 useState Hook 来捕获搜索状态,而是使用 React Router 来获取可共享的 URL。 App 组件如下所示,类似于前面提到的 React Router 教程中的 App 组件:

const App = () => {
return (
<>
<h1>React Router</h1>

<nav>
<Link to="/home">Home</Link>
<Link to="/bookshelf">Bookshelf</Link>
</nav>

<Routes>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="bookshelf" element={<Bookshelf />} />
<Route path="*" element={<NoMatch />} />
</Routes>
</>
);
};

虽然 Home 和 NoMatch 组件只是具有任何实现的占位符组件,但我们将关注 Bookshelf 组件,它将Books显示为列表组件。这些Books示例数据可以从远程 API(或模拟 API)获取:

const Bookshelf = () => {
const books = [
{
title: 'The Road to Rust',
isCompleted: false,
},
{
title: 'The Road to React',
isCompleted: true,
},
];

return (
<>
<h2>Bookshelf</h2>

<ul>
{books.map((book) => (
<li key={book.title}>{book.title}</li>
))}
</ul>
</>
);
};

为了使用户能够通过不区分大小写的标题匹配来过滤此列表,我们使用 React 的 useState Hook 和 HTML 输入字段。最后,事件处理程序将从输入字段中读取值并将其写入状态:

const byTitle = (title) => (book) =>
book.title.toLowerCase().includes((title || '').toLowerCase());

const Bookshelf = () => {
const books = [...];

const [title, setTitle] = React.useState('');

const handleTitle = (event) => {
setTitle(event.target.value);
};

return (
<>
<h2>Bookshelf</h2>

<input type="text" value={title} onChange={handleTitle} />

<ul>
{books.filter(byTitle(title)).map((book) => (
<li key={book.title}>{book.title}</li>
))}
</ul>
</>
);
};

这就是就是“在 React 中使用状态”的版本。接下来,我们要使用 React Router 来在 URL 中捕获此状态。React Router 为我们提供了 useSearchParams hook,它几乎可以用来替代 React 的 useState hook:

import * as React from 'react';
import {
Routes,
Route,
Link,
useSearchParams,
} from 'react-router-dom';

...

const Bookshelf = () => {
const books = [...];

const [search, setSearch] = useSearchParams();

const handleTitle = (event) => {
setSearch({ title: event.target.value });
};

return (
<>
<h2>Bookshelf</h2>

<input
type="text"
value={search.get('title')}
onChange={handleTitle}
/>

<ul>
{books.filter(byTitle(search.get('title'))).map((book) => (
<li key={book.title}>{book.title}</li>
))}
</ul>
</>
);
};

由于以下两点,它不能直接替代 React 的 useState Hook。首先,它对一个对象而不是字符串进行操作,因为一个 URL 可以有多个搜索参数(例如 /bookshelf?title=Rust&rating=4),因此每个搜索参数都成为该对象中的一个属性(例如{ title: 'Rust', rating: 4 })。

如果我们将 React 的 useState Hook 与对象而不是字符串一起使用,它本质上与我们之前的实现类似:

const [search, setSearch] = React.useState({ title: '' });

然而,即使 useSearchParams 返回的有状态值是对象类型(typeof search === 'object'),它仍然不能像单纯的 JavaScript 对象数据结构那样访问,因为它是 URLSearchParams 的一个实例。因此我们需要调用它的 getter 方法(例如 search.get('title'))。

其次,React Router 的 useSearchParams Hook 不接受初始状态,因为初始状态来自 URL。因此,当用户与搜索参数(例如 /bookshelf?title=Rust)共享 URL 时,另一个用户将从 React Router 的 Hook 获得 { title: 'Rust' } 作为初始状态。当应用程序将用户导航到带有搜索参数且设置了可选搜索参数的路线时,也会发生同样的情况。

这就是使用状态的 URL 而不是使用 React 的状态管理 Hook 之一。它极大地改善了用户体验,因为 URL 变得更加特定于用户在页面上看到的内容。因此,这个特定的 URL 可以与其他用户共享,他们将看到具有相同 UI 的页面。

URLSEARCHPARAMS 转换为对象

如果你在处理 React Router 的 useSearchParams Hook 时不想使用 URLSearchParams,你可以编写一个自定义hook,它返回一个 JavaScript 对象而不是 URLSearchParams 的实例:

const useCustomSearchParams = () => {
const [search, setSearch] = useSearchParams();
const searchAsObject = Object.fromEntries(
new URLSearchParams(search)
);

return [searchAsObject, setSearch];
};

const Bookshelf = () => {
const books = [...];

const [search, setSearch] = useCustomSearchParams();

const handleTitle = (event) => {
setSearch({ title: event.target.value });
};

return (
<>
<h2>Bookshelf</h2>

<input
type="text"
value={search.title}
onChange={handleTitle}
/>

<ul>
{books.filter(byTitle(search.title)).map((book) => (
<li key={book.title}>{book.title}</li>
))}
</ul>
</>
);
};

然而,这个自定义hook应该有一点不足,因为它不适用于重复键(例如带有 ?editions=1&editions=3 的数组搜索参数)和使用复杂 URL 时的其他边界情况。

一般来说,仅使用 React Router 的 useSearchParams Hook(或这个自定义的 useCustomSearchParams hook)并不能为你提供 URL 状态管理的完整体验,因为它仅可用于字符串而不能用于其他数据类型。我们将在接下来的部分中探讨这一点以及如何解决这个问题。

搜索参数与保留数据类型

并非所有状态都只包含字符串。在前面使用 React Router 的搜索参数的例子中,我们使用了一个字符串(这里是:title),它被编码到 URL 中。当从 URL 解码这个字符串时,我们将默认得到一个字符串——这在我们的例子中有效,因为我们需要一个字符串。但是其他原始数据类型如数字或布尔值呢?更不用说复杂的数据类型,例如数组。

为了探索解决这个,我们将通过实现一个复选框来继续之前的示例。我们将使用这个复选框组件并将其连接到 React Router 的搜索参数:

const bySearch = (search) => (book) =>
book.title
.toLowerCase()
.includes((search.title || '').toLowerCase()) &&
book.isCompleted === search.isCompleted;

const Bookshelf = () => {
const books = [...];

const [search, setSearch] = useCustomSearchParams();

const handleTitle = (event) => {
setSearch({ title: event.target.value });
};

const handleIsCompleted = (event) => {
setSearch({ isCompleted: event.target.checked });
};

return (
<>
<h2>Bookshelf</h2>

<input
type="text"
value={search.title}
onChange={handleTitle}
/>

<Checkbox
label="Is Completed?"
value={search.isCompleted}
onChange={handleIsCompleted}
/>

<ul>
{books.filter(bySearch(search)).map((book) => (
<li key={book.title}>{book.title}</li>
))}
</ul>
</>
);
};

在浏览器中实验以下。你将看到对 isCompleted 布尔值的搜索不起作用,因为来自我们的搜索对象的 isCompleted 被表示为一个字符串,如“true”或“false”。我们可以通过增强我们的自定义hook来规避这一点:

const useCustomSearchParams = (param = {}) => {
const [search, setSearch] = useSearchParams();
const searchAsObject = Object.fromEntries(
new URLSearchParams(search)
);

const transformedSearch = Object.keys(param).reduce(
(acc, key) => ({
...acc,
[key]: param[key](acc[key]),
}),
searchAsObject
);

return [transformedSearch, setSearch];
};

const PARAMS = {
BooleanParam: (string = '') => string === 'true',
};

const Bookshelf = () => {
const books = [...];

const [search, setSearch] = useCustomSearchParams({
isCompleted: PARAMS.BooleanParam,
});

...

return (...);
};

本质上,新版本的自定义hook采用具有可选转换功能的对象。它遍历每个转换函数,如果找到转换函数和搜索参数之间的匹配项,则将该函数应用于搜索参数。在这种情况下,我们将字符串布尔值(“true”或“false”)转换为实际的布尔值。如果没有找到匹配项,它只返回原始搜索参数。因此我们不需要标题的转换函数,因为它是一个字符串并且可以继续为字符串。

通过拥有自定义hook的实现细节,我们还可以创建其他转换器函数(例如 NumberParam),从而填补缺失数据类型转换(例如数字)的空白:

const PARAMS = {
BooleanParam: (string = '') => string === 'true',
NumberParam: (string = '') => (string ? Number(string) : null),
// other transformation functions to map all data types
};

开源组件中use-query-params这个库完美的解决这个问题。

React Router 使用搜索参数

use-query-params 库非常适合将复杂的 URL 用作超越字符串的状态的用例。在本节中,我们将探索 use-query-params 库,从而摆脱我们自定义的 useSearchParams hook。

自己按照库的安装说明进行操作。你需要在命令行上安装该库并在 React 项目的根级别实例化它:

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route } from 'react-router-dom';
import { QueryParamProvider } from 'use-query-params';

import App from './App';

ReactDOM.render(
<BrowserRouter>
<QueryParamProvider ReactRouterRoute={Route}>
<App />
</QueryParamProvider>
</BrowserRouter>,
document.getElementById('root')
);

然而, use-query-params 还没有正确适应 React Router 6。因此,你可能会看到以下错误弹出:“<Route> 仅用作 <Routes> 元素的子元素,永远不会直接呈现。请将你的 <Route> 包装在 <Routes> 中。”。因此,在根级别调整你的代码:

import React from 'react';
import ReactDOM from 'react-dom';
import {
BrowserRouter,
useNavigate,
useLocation,
} from 'react-router-dom';
import { QueryParamProvider } from 'use-query-params';

import App from './App';

const RouteAdapter = ({ children }) => {
const navigate = useNavigate();
const location = useLocation();

const adaptedHistory = React.useMemo(
() => ({
replace(location) {
navigate(location, { replace: true, state: location.state });
},
push(location) {
navigate(location, { replace: false, state: location.state });
},
}),
[navigate]
);
return children({ history: adaptedHistory, location });
};

ReactDOM.render(
<BrowserRouter>
<QueryParamProvider ReactRouterRoute={RouteAdapter}>
<App />
</QueryParamProvider>
</BrowserRouter>,
document.getElementById('root')
);

现在你可以使用 use-query-params 在 React 中进行强大的 URL 状态管理。你所要做的就是使用新的 useQueryParams 钩子。另请注意,与我们的自定义钩子相比,你还需要“转换”字符串搜索参数:

import * as React from 'react';
import { Routes, Route, Link } from 'react-router-dom';
import {
useQueryParams,
StringParam,
BooleanParam,
} from 'use-query-params';

...

const Bookshelf = () => {
const books = [...];

const [search, setSearch] = useQueryParams({
title: StringParam,
isCompleted: BooleanParam,
});

...

return (...);
};

你还可以提供合理的默认值。例如,此时在没有搜索参数的情况下导航到 /bookshelf 时,title 和 isComplete 将是未定义的。但是,如果你希望它们至少是标题的空字符串和 isComplete 的 false,你可以提供这些默认值,例如:

import * as React from 'react';
import { Routes, Route, Link } from 'react-router-dom';
import {
useQueryParams,
StringParam,
BooleanParam,
withDefault
} from 'use-query-params';

...

const Bookshelf = () => {
const books = [...];

const [search, setSearch] = useQueryParams({
title: withDefault(StringParam, ''),
isCompleted: withDefault(BooleanParam, false),
});

...

return (...);
};

还有一件值得注意的事情要提到:目前,use-query-params 使用默认的“push in”模式,这意味着每次附加搜索参数时,它不会覆盖其他搜索参数。因此,你在更改其中之一的同时保留所有搜索参数。但是,如果这不是你想要的行为,你还可以更改模式(例如,更改为“push”),这样将不再保留以前的搜索参数(尽管这在我们的场景中没有意义):

const Bookshelf = () => {
...

const handleTitle = (event) => {
setSearch({ title: event.target.value }, 'push');
};

const handleIsCompleted = (event) => {
setSearch({ isCompleted: event.target.checked }, 'push');
};

...

return (...);
};

除了我们在这里使用的两种数据类型转换之外,还有对数字、数组、对象等的转换。例如,如果你希望在 React 中有一个可选择的表,你可能希望将表中的每个选定行表示为数组中的标识符(在 use-query-params 中,它是 ArrayParam 转换)映射到实际 URL .然后你可以与另一个用户共享此 URL,该用户将从所选行开始。

使用 URL 作为状态是改善用户体验的方式。在处理单个或多个字符串状态时,React Router 的搜索参数为你提供了一个很好的体验。但是,一旦你想保留映射到 URL 的数据类型,你可能希望使用诸如 use-query-params 之类的库在 React 中进行复杂的 URL 状态管理。

参考

use-query-params

· 阅读需 11 分钟

前言

本文教你如何在 React Router 6 中使用嵌套路由。嵌套路由是一个强大的功能。虽然大多数人认为 React Router 只会在页面之间路由使用,但它也允许用户根据当前路由交换视图的特定片段。例如,在用户页面上,会显示多个选项卡(例如个人资料、帐户)以浏览用户信息。通过单击这些选项卡,浏览器中的 URL 会发生变化,但不会替换整个页面,只会替换选项卡的内容。 下面我们将使用 React Router 重新创建这个场景。为了说明这是如何工作的,以及如何自己在 React 中逐步实现嵌套路由,我们将从以下示例开始:

import { Routes, Route, Link } from 'react-router-dom';

const App = () => {
return (
<>
<h1>React Router</h1>

<nav>
<Link to="/home">Home</Link>
<Link to="/user">User</Link>
</nav>

<Routes>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="user" element={<User />} />
<Route path="*" element={<NoMatch />} />
</Routes>
</>
);
};

在这个函数组件中,我们使用了 React Router 中的 Link 和 Route 组件,用于 home/ 和 user/ 路由。此外,我们有一个加载了 Home 组件的所谓的索引路由和一个加载了 NoMatch 组件的所谓的 No Match 路由。两者都作为备选路线。从这里开始,我们将了解嵌套路由的概念。

React Router 中的嵌套路由

我们将继续处理 User 组件,这是我们希望通过选项卡进行嵌套路由的地方。因此,我们将实例化一组新的 Link 组件(将是我们的无样式选项卡),用于将用户导航到他们的个人资料和他们的帐户。

const User = () => {
return (
<>
<h1>User</h1>

<nav>
<Link to="/user/profile">Profile</Link>
<Link to="/user/account">Account</Link>
</nav>
</>
);
};

我们在这里使用绝对路径将用户从他们的个人资料导航到他们的帐户,反之亦然,但是,我们也可以使用相对路径作为最佳实践。因为 User 组件位于 /user 路由中,所以 Link 组件可以预测它们的父路由(这里是:/user),并且只需将相对路径(这里:profile 和 account)附加到它(例如 /user/profile):

const User = () => {
return (
<>
<h1>User</h1>

<nav>
<Link to="profile">Profile</Link>
<Link to="account">Account</Link>
</nav>
</>
);
};

此时,当我们尝试在 React 应用程序中单击这些链接之一时,我们将被困在我们的 No Match Route 中。这告诉我们,我们还没有将这些路由(此处:/user/profile 和 /user/account)映射到任何实际的路由组件。因此,我们将这两个新路由作为所谓的嵌套路由添加到我们的 /user 路由中:

const App = () => {
return (
<>
<h1>React Router</h1>

<nav>
<Link to="/home">Home</Link>
<Link to="/user">User</Link>
</nav>

<Routes>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="user" element={<User />}>
<Route path="profile" element={<Profile />} />
<Route path="account" element={<Account />} />
</Route>
<Route path="*" element={<NoMatch />} />
</Routes>
</>
);
};

Route 组件现在以一对一的关系映射到 Link 组件。但是,可以有多个 Link 组件链接到同一个路由,因此它实际上是一对多的关系。

在浏览器中对此进行测试时,我们将看到仅显示 User 组件,而不会显示其嵌套的 Profile 组件,也不会显示其嵌套的 Account 组件。我们缺少 React Router 的关键 Outlet 组件:

import { Routes, Route, Link, Outlet } from 'react-router-dom';

...

const User = () => {
return (
<>
<h1>User</h1>

<nav>
<Link to="profile">Profile</Link>
<Link to="account">Account</Link>
</nav>

<Outlet />
</>
);
};

Outlet 组件从父 Routes 的 Route 组件集合中使用其各自的组件(此处为 Profile 或 Account 组件)呈现匹配的子路由。 如果没有 /profile 和 /account 路由匹配(例如 /user/settings),你将只看到 User 组件出现。为避免这种情况,你可以添加索引和无匹配路由的组合。之后,默认路由将是 /profile 路由:

const App = () => {
return (
<>
<h1>React Router</h1>

<nav>
<Link to="/home">Home</Link>
<Link to="/user">User</Link>
</nav>

<Routes>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="user" element={<User />}>
<Route index element={<Profile />} />
<Route path="profile" element={<Profile />} />
<Route path="account" element={<Account />} />
<Route path="*" element={<NoMatch />} />
</Route>
<Route path="*" element={<NoMatch />} />
</Routes>
</>
);
};

虽然 User 组件总是将选项卡呈现为导航,但其内容(Outlet)被匹配的嵌套路由(基于 /user/profile 或 /user/account 路由的 Profile 或 Account 组件)替换。如果在访问 /user 路由时这些路由都不匹配,应用程序将显示 Profile 组件(如果路由与 /user 完全匹配)或 NoMatch 组件(如果路由不匹配,例如 /user/setting)出现。

React router 中的 动态嵌套路由

在嵌套路由的下一个示例中,我们将从 App 组件中开始。这次我们不想像之前那样渲染静态嵌套路由(例如 /user/profile),而是基于标识符的动态嵌套路由(例如 /users/1 用于显示具有标识符 1 并因此匹配此路由的用户)。因此,我们将示例从单用户路由 (/user) 调整为多用户路由 (/user)。

const App = () => {
const users = [
{ id: '1', fullName: 'Robin Wieruch' },
{ id: '2', fullName: 'Sarah Finnley' },
];

return (
<>
<h1>React Router</h1>

<nav>
<Link to="/home">Home</Link>
<Link to="/users">Users</Link>
</nav>

<Routes>
<Route element={<Layout />}>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="users" element={<Users users={users} />} />
<Route path="*" element={<NoMatch />} />
</Route>
</Routes>
</>
);
};

Users 组件成为 React 中的列表组件,因为它遍历每个用户并为其返回 JSX。在这种情况下,它不仅仅是一个列表,因为我们将 React Router 的 Link 组件添加到组合中。 Link 组件中的相对路径提示相应的嵌套(此处:/${user.id} 嵌套在 /users 中)但动态(此处:/${user.id})路由:

const Users = ({ users }) => {
return (
<>
<h2>Users</h2>

<ul>
{users.map((user) => (
<li key={user.id}>
<Link to={user.id}>
{user.fullName}
</Link>
</li>
))}
</ul>
</>
);
};

通过拥有这个新的动态嵌套路由,我们需要在 App 组件中为它创建一个匹配的嵌套路由组件。首先,由于它是 /users 路由的所谓嵌套路由(或子路由),我们可以将它嵌套在相应的父路由组件中。此外,由于它是所谓的动态路由,它使用定义为 :userId 的动态路由,而用户的标识符则动态匹配(例如,id 为 '1' 的用户将与 /users/1 匹配):

const App = () => {
const users = [
{ id: '1', fullName: 'Robin Wieruch' },
{ id: '2', fullName: 'Sarah Finnley' },
];

return (
<h1>React Router</h1>

<nav>...</nav>

<Routes>
<Route element={<Layout />}>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="users" element={<Users users={users} />}>
<Route path=":userId" element={<User />} />
</Route>
<Route path="*" element={<NoMatch />} />
</Route>
</Routes>
);
};

这样,User 组件就变成了 Users 组件的嵌套路由。因此,再次在 Outlet 组件的帮助下渲染其嵌套路由是用户组件的责任——再次渲染匹配的子路由:

import { Routes, Route, Link, Outlet } from 'react-router-dom';

...

const Users = ({ users }) => {
return (
<>
<h2>Users</h2>

<ul>...</ul>

<Outlet />
</>
);
};

接下来,我们将声明缺少的 User 组件,只要用户的标识符在 URL 中匹配,该组件就会通过 Users 组件中的 Outlet 嵌套。在这个新组件中,我们可以使用 React Router 的 useParams Hook 从 URL 中获取相应的 userId(等于 :userId):

import {
...
useParams,
} from 'react-router-dom';

...

const User = () => {
const { userId } = useParams();

return (
<>
<h2>User: {userId}</h2>

<Link to="/users">Back to Users</Link>
</>
);
};

我们已经看到了如何通过将一个 Route 组件(或多个 Route 组件)嵌套在另一个 Route 组件中来创建嵌套路由。前者是嵌套的子路由,后者是渲染封闭组件的父路由,该组件必须使用 Outlet 组件来渲染实际匹配的子路由。

此外,我们还看到了如何通过在路由的路径属性中使用冒号(例如:userId)来创建动态路由。本质上, :userId 充当任何标识符的星号。在我们的例子中,我们使用 Link 组件将用户导航到 /users/:userId 路由,其中​​ :userId 代表实际用户的标识符。最后,我们总是可以通过使用 React Router 的 useParams Hook 从 URL 中获取动态路径(称为参数或 params)。

如果你碰巧将 React Router 用于你的 React 应用程序,嵌套路由可以通过让你的用户访问你的应用程序非常特定的部分,同时将这些部分作为 URL 共享,从而极大地提升你的用户体验。

· 阅读需 22 分钟

前言

不久前,React Router 库更新到了第 6 版,随之而来的是一些有趣的变化,本文将讲述React Router 6的一些新特性及使用的案例。 接下来是一些准备工作:

  1. 首先需要创建一个新的 React 项目(例如 create-react-app)。然后,按照官方文档安装 React Router。
    yarn add react-router-dom@latest    
    我们这里安装的是6.0.2版本。
  2. 第一个实现细节将告诉我们的 React 应用程序我们想要使用 React Router。因此,在 React 项目的顶级文件(例如 index.js)中导入 Router 组件,其中 React 使用 ReactDOM API 挂载到 HTML:
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';

import App from './App';

ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
);

从这里开始,我们将在 App.js 文件中继续我们的实现。

匹配路由

首先,我们将使用 React Router 的 Link 组件在我们的 App 组件中实现导航。我不建议使用内联样式,因此请根据你的 React 项目选择合适的样式策略和样式方法:

import { Link } from 'react-router-dom';

const App = () => {
return (
<>
<h1>React Router</h1>
<Navigation />
</>
);
};

const Navigation = () => {
return (
<nav
style={{
borderBottom: 'solid 1px',
paddingBottom: '1rem',
}}
>
<Link to="/home">Home</Link>
<Link to="/users">Users</Link>
</nav>
);
};

当你在浏览器中启动 React 应用程序时,你应该能够单击两个 Link 组件,这些组件应该将你导航到各自的路由。单击这些链接时,可通过检查浏览器的当前 URL 来确认。接下来,我们需要使用 React Router 的 Route 组件将路由映射到实际渲染:

import { Routes, Route, Link } from 'react-router-dom';

const App = () => {
return (
<>
<h1>React Router</h1>

<Navigation />

<Routes>
<Route path="home" element={<Home />} />
<Route path="users" element={<Users />} />
</Routes>
</>
);
};

const Navigation = () => {
return (
<nav
style={{
borderBottom: 'solid 1px',
paddingBottom: '1rem',
}}
>
<Link to="/home">Home</Link>
<Link to="/users">Users</Link>
</nav>
);
};

你可以通过检查它们各自的 to 和 path 属性来查看 Link 和 Route 组件之间的直接匹配。当路由匹配时,每个 Route 组件都会渲染一个 React 元素。由于我们在这里渲染一个 React 元素,我们也可以传递 React props。缺少的是相应功能组件的声明:

const Home = () => {
return (
<main style={{ padding: '1rem 0' }}>
<h2>Home</h2>
</main>
);
};

const Users = () => {
return (
<main style={{ padding: '1rem 0' }}>
<h2>Users</h2>
</main>
);
};

返回浏览器时,你应该能够在看到 Home 和 Users 组件的同时从一个页面导航到另一个页面(此处:从 /home 到 /users 路由)。基本上这就是 React Router 的本质:设置 Link 组件并将它们与 Route 组件匹配。链接与路由是多对一的关系,因此你的应用程序中可以有多个链接链接到同一个路由。

布局路由、索引路由、无匹配路由

接下来,你将看到新的 Home 和 Users 组件如何共享相同的布局。作为 React 开发人员,直觉上我们会从 Home 和 Users 组件中提取一个带有样式的新组件,以避免重复。在这个新组件中,我们将使用 React 的 children 属性将组件组合在一起。第一步,将样式提取到它自己的组件中:

const Home = () => {
return (
<>
<h2>Home</h2>
</>
);
};

const Users = () => {
return (
<>
<h2>Users</h2>
</>
);
};

const Layout = ({ children }) => {
return <main style={{ padding: '1rem 0' }}>{children}</main>;
};

其次,在 App 组件中渲染它。通过使用 React 的子级,Layout 组件应该渲染匹配的封闭子路由:

import { Routes, Route, Link } from 'react-router-dom';

const App = () => {
return (
<>
<h1>React Router</h1>

<Navigation />

<Routes>
<Layout>
<Route path="home" element={<Home />} />
<Route path="users" element={<Users />} />
</Layout>
</Routes>
</>
);
};

const Navigation = () => {
return (
<nav
style={{
borderBottom: 'solid 1px',
paddingBottom: '1rem',
}}
>
<Link to="/home">Home</Link>
<Link to="/users">Users</Link>
</nav>
);
};

const Home = () => {
return (
<>
<h2>Home</h2>
</>
);
};

const Users = () => {
return (
<>
<h2>Users</h2>
</>
);
};

const Layout = ({ children }) => {
return <main style={{ padding: '1rem 0' }}>{children}</main>;
};

但是你会看到这在 React Router 中是不允许的,你会得到一个异常说:<Routes> 的所有组件子项必须是 <Route><React.Fragment>。解决此问题的一种常见方法是在每个组件中单独使用 Layout 组件(类似于我们之前使用的)或在每个 Route 组件中(如下例所示):

const App = () => {
return (
<>
...

<Routes>
<Route path="home" element={<Layout><Home /></Layout>} />
<Route path="users" element={<Layout><Users /></Layout>} />
</Routes>
</>
);
};

然而,这给 React 应用程序增加了不必要的冗余。因此,我们将使用所谓的 Layout Route,而不是复制 Layout 组件,它不是实际的路由,而只是一种方法,可以让一组 Route 中的每个 Route 组件的元素具有相同的周围样式:

const App = () => {
return (
<>
...

<Routes>
<Route element={<Layout />}>
<Route path="home" element={<Home />} />
<Route path="users" element={<Users />} />
</Route>
</Routes>
</>
);
};

如你所见,可以将 Route 组件嵌套在另一个 Route 组件中——而前者成为所谓的嵌套路由。现在不再在 Layout 组件中使用 React 的子组件,而是使用 React Router 的 Outlet 组件作为等效组件:

import { Routes, Route, Outlet, Link } from 'react-router-dom';

...

const Layout = () => {
return (
<main style={{ padding: '1rem 0' }}>
<Outlet />
</main>
);
};

本质上,Layout 组件中的 Outlet 组件插入了父路由(这里:Layout 组件)的匹配子路由(这里:Home 或 Users 组件)。毕竟,使用 Layout Route 可以帮助你为集合中的每个 Route 组件提供相同的布局(例如,CSS 样式,HTML 结构)。

从这里开始,你可以更进一步,将 App 组件的所有实现细节(标题、导航)移动到这个新的 Layout 组件中。此外,我们可以与 NavLink 组件交换链接,以实现所谓的活动链接——向用户显示当前活动的路线。因此,当将新的 NavLink 组件与函数一起使用时,我们可以访问其style和 className props中的 isActive 标志:

import { Routes, Route, Link, NavLink, Outlet } from 'react-router-dom';

const App = () => {
return (
<>
<h1>React Router</h1>

<Navigation />

<Routes>
<Route element={<Layout />}>
<Route path="home" element={<Home />} />
<Route path="users" element={<Users />} />
</Route>
</Routes>
</>
);
};

const Navigation = () => {
return (
<nav
style={{
borderBottom: 'solid 1px',
paddingBottom: '1rem',
}}
>
<Link to="/home">Home</Link>
<Link to="/users">Users</Link>
</nav>
);
};

const Home = () => {
return (
<>
<h2>Home</h2>
</>
);
};

const Users = () => {
return (
<>
<h2>Users</h2>
</>
);
};

const Layout = () => {
const style = ({ isActive }) => ({
fontWeight: isActive ? 'bold' : 'normal',
});

return (
<>
<h1>React Router</h1>

<nav
style={{
borderBottom: 'solid 1px',
paddingBottom: '1rem',
}}
>
<NavLink to="/home" style={style}>Home</NavLink>
<NavLink to="/users" style={style}>Users</NavLink>
</nav>

<main style={{ padding: '1rem 0' }}>
<Outlet />
</main>
</>
);
};

接下来你可能已经注意到这个 React 应用程序缺少一个基本路由。虽然我们有 /home 和 /users 路由,但没有 / 路由。你也会在浏览器的开发人员工具中看到此警告:没有路由匹配位置“/”。因此,每当用户访问 / 路由时,我们都会创建一个所谓的索引路由作为回退。此回退路由的元素可以是新组件或任何已匹配的路由(例如,Home 应为路由 / 和 /home 呈现,如下例所示):

const App = () => {
return (
<Routes>
<Route element={<Layout />}>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="users" element={<Users />} />
</Route>
</Routes>
);
};

当父路由匹配但没有子路由匹配时,你可以将索引路由视为默认路由。接下来,如果用户导航到不匹配的路由(例如 /about),我们将添加一个所谓的 No Match Route(也称为 Not Found Route),它相当于网站的 404 页面:

const App = () => {
return (
<Routes>
<Route element={<Layout />}>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="users" element={<Users />} />
<Route path="*" element={<NoMatch />} />
</Route>
</Routes>
);
};

const NoMatch = () => {
return (<p>There's nothing here: 404!</p>);
};

到目前为止,在使用 Routes 组件作为 Route 组件集合的容器时,通过使用 Layout Routes、Index Routes 和 No Match Routes 展示了 React Router 的其他最佳实践。如你所见,也可以将 Route 组件嵌套到 Route 组件中。下面我们接着了解有关嵌套路由的更多信息。

动态且嵌套的路由

接下来我们将用实现细节来装饰用户组件。首先,我们将在我们的 App 组件中初始化一个项目列表(这里是:用户)。该列表只是示例数据,但它也可以在 React 中从远程 API 获取。其次,我们将用户作为props传递给用户组件:

const App = () => {
const users = [
{ id: '1', fullName: 'Robin Wieruch' },
{ id: '2', fullName: 'Sarah Finnley' },
];

return (
<Routes>
<Route element={<Layout />}>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="users" element={<Users users={users} />} />
<Route path="*" element={<NoMatch />} />
</Route>
</Routes>
);
};

Users 组件成为 React 中的列表组件,因为它遍历每个用户并为其返回 JSX。在这种情况下,它不仅仅是一个列表,因为我们将 React Router 的 Link 组件添加到组合中。 Link 组件中的相对路径提示相应的动态(此处:/${user.id})尚未嵌套(此处:/${user.id} 嵌套在 /users 中)路由:

const Users = ({ users }) => {
return (
<>
<h2>Users</h2>

<ul>
{users.map((user) => (
<li key={user.id}>
<Link to={`/users/${user.id}`}>
{user.fullName}
</Link>
</li>
))}
</ul>
</>
);
};

通过拥有这个新的动态嵌套路由,我们需要在 App 组件中为它创建一个匹配的嵌套路由组件。首先,由于它是 /users 路由的所谓嵌套路由(或子路由),我们可以将它嵌套在相应的父路由组件中。此外,由于它是所谓的动态路由,它使用定义为 :userId 的动态路由,而用户的标识符则动态匹配(例如,id 为 '1' 的用户将与 /users/1 匹配):

const App = () => {
const users = [
{ id: '1', fullName: 'Robin Wieruch' },
{ id: '2', fullName: 'Sarah Finnley' },
];

return (
<Routes>
<Route element={<Layout />}>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="users" element={<Users users={users} />}>
<Route path=":userId" element={<User />} />
</Route>
<Route path="*" element={<NoMatch />} />
</Route>
</Routes>
);
};

之前,当我们介绍将 /home 和 /users 路由作为其子路由的父布局路由时,我们已经了解了嵌套路由。当我们进行此更改时,我们必须使用父路由中的 Outlet 组件来渲染匹配的子路由。同样的情况在这里再次发生,因为用户组件也必须渲染它的嵌套路由:

const Users = ({ users }) => {
return (
<>
<h2>Users</h2>

<ul>...</ul>

<Outlet />
</>
);
};

接下来,我们将声明缺少的 User 组件,只要用户的标识符在 URL 中匹配,该组件就会通过 Users 组件中的 Outlet 嵌套。因此,我们可以使用 React Router 的 useParams Hook 从 URL 中获取相应的 userId(等于 :userId):

import {
...
useParams,
} from 'react-router-dom';

...

const User = () => {
const { userId } = useParams();

return (
<>
<h2>User: {userId}</h2>

<Link to="/users">Back to Users</Link>
</>
);
};

我们再次看到了如何通过将一个 Route 组件(或多个 Route 组件)嵌套在另一个 Route 组件中来创建嵌套路由。前者是嵌套的子路由,后者是渲染封闭组件的父路由,该组件必须使用 Outlet 组件来渲染实际匹配的子路由。

我们还看到了如何通过在路由的路径属性(例如:userId)中使用冒号来创建动态路由。本质上, :userId 充当任何标识符的星号。在我们的例子中,我们使用 Link 组件将用户导航到 /users/:userId 路由,其中​​ :userId 代表实际用户的标识符。最后,我们总是可以通过使用 React Router 的 useParams Hook 从 URL 中获取动态路径(称为参数或 params)。

React Router中的相关链接

最新版本的 React Router 带有所谓的相对链接。我们将通过查看用户组件及其用于链接组件的绝对 /users/${user.id} 路径来研究这个概念。在之前版本的 React Router 中,需要指定整个路径。但是,在此版本中,你可以仅使用嵌套路径作为相对路径:

const Users = ({ users }) => {
return (
<>
<h2>Users</h2>

<ul>
{users.map((user) => (
<li key={user.id}>
<Link to={user.id}>
{user.fullName}
</Link>
</li>
))}
</ul>
</>
);
};

由于 Users 组件用于 /users 路由,因此 Users 组件中的 Link 知道其当前位置,不需要创建绝对路径的整个顶级部分。相反,它知道 /users 并且只是附加 :userId 作为它的相对路径。

声明式和程序式导航

到目前为止,我们只在使用 Link 或 NavLink 组件时使用了声明式导航。但是,在某些情况下,你希望能够通过 JavaScript 以编程方式导航用户。我们将通过实现一个可以在 User 组件中删除用户的功能来展示这个场景。在删除后,用户应该从 User 组件导航到 Users 组件(从 /users/:userId 到 /users)。

我们将通过使用 React 的 useState Hook 创建一个有状态的 users 值来开始这个实现,然后实现一个事件处理程序,该处理程序使用标识符从用户中删除用户:

import * as React from 'react';
...

const App = () => {
const [users, setUsers] = React.useState([
{ id: '1', fullName: 'Robin Wieruch' },
{ id: '2', fullName: 'Sarah Finnley' },
]);

const handleRemoveUser = (userId) => {
setUsers((state) => state.filter((user) => user.id !== userId));
};

return (
<Routes>
<Route element={<Layout />}>
<Route index element={<Home />} />
<Route path="home" element={<Home />} />
<Route path="users" element={<Users users={users} />}>
<Route
path=":userId"
element={<User onRemoveUser={handleRemoveUser} />}
/>
</Route>
<Route path="*" element={<NoMatch />} />
</Route>
</Routes>
);
};

在我们将事件处理程序作为回调处理程序传递给 User 组件后,我们可以在那里使用它作为内联处理程序来通过标识符删除特定用户:

const User = ({ onRemoveUser }) => {
const { userId } = useParams();

return (
<>
<h2>User: {userId}</h2>

<button type="button" onClick={() => onRemoveUser(userId)}>
Remove
</button>

<Link to="/users">Back to Users</Link>
</>
);
};

一旦用户被删除,我们可以使用 React Router 的 useNavigate Hook,它允许我们以编程方式将用户导航到另一个路由(这里:/users):

import * as React from 'react';
import {
...
useNavigate,
} from 'react-router-dom';

const App = () => {
const navigate = useNavigate();

const [users, setUsers] = React.useState([
{ id: '1', fullName: 'Robin Wieruch' },
{ id: '2', fullName: 'Sarah Finnley' },
]);

const handleRemoveUser = (userId) => {
setUsers((state) => state.filter((user) => user.id !== userId));

navigate('/users');
};

return (...);
};

在这种情况下,删除操作是同步发生的,因为用户只是客户端的一个有状态值。但是,如果用户是数据库中的实体,则必须发出异步请求才能删除它。一旦这个操作成功(例如:promise是 resolved时),用户就会被导航到 /users 路由。你可以通过在 React 中设置一个虚假的 API 来自己尝试这个场景,而不使用实际的服务器。

搜索参数

浏览器中的 URL 不仅包含路径,还包含一个可选的查询字符串(在 React Router 中称为搜索参数),它以键/值对的形式出现在 ? URL 中的分隔符。例如,/users?name=robin 将是一个带有一对搜索参数的 URL,其中键是名称,值是 robin。以下示例将其显示为实现:

import * as React from 'react';
import {
...
useSearchParams,
} from 'react-router-dom';

...

const Users = ({ users }) => {
const [searchParams, setSearchParams] = useSearchParams();

const searchTerm = searchParams.get('name') || '';

const handleSearch = (event) => {
const name = event.target.value;

if (name) {
setSearchParams({ name: event.target.value });
} else {
setSearchParams({});
}
};

return (
<>
<h2>Users</h2>

<input
type="text"
value={searchTerm}
onChange={handleSearch}
/>

<ul>
{users
.filter((user) =>
user.fullName
.toLowerCase()
.includes(searchTerm.toLocaleLowerCase())
)
.map((user) => (
<li key={user.id}>
<Link to={user.id}>{user.fullName}</Link>
</li>
))}
</ul>

<Outlet />
</>
);
};

首先,我们使用 React Router 的 useSearchParams Hook 从 URL 中读取当前搜索参数(请参阅 searchParams 上的 get() 方法),同时还将搜索参数写入 URL(请参阅 setSearchParams() 函数)。虽然我们使用前者按键获取搜索参数(此处:“name”)来控制输入字段,但我们使用后者在 URL 中按键设置搜索参数。在输入字段中键入。在其核心,React Router 的 useSearchParams Hook 与 React 的 useState Hook 相同,区别在于该状态是 URL 状态,而不是 React 中的本地状态。最后我们使用搜索参数来过滤用户的实际列表以完成此功能。

毕竟,在你的 URL 中包含搜索参数可以让你与他人共享更具体的 URL。如果你在一个搜索黑色鞋子的电子商务网站上,你可能希望共享整个 URL(例如 myecommerce.com/shoes?color=black)而不仅仅是路径(例如 myecommerce.com/shoes)。

总结

React Router 是 React 最常用的第三方库之一。它的核心功能是将 Link 组件映射到 Route 组件,这使开发人员无需向 Web 服务器发出请求即可实现客户端路由。然而,除了这个核心功能之外,它还是一个成熟的路由库,它支持声明式嵌套路由、动态路由、导航、活动链接,还可以通过 URL 进行编程导航和搜索。

参考

react-router CRA