路由参数和位置信息
列表页和详情页是带路由应用的经典组合:/vans 显示所有的车辆,点击其中一个应该打开 /vans/2 显示该车辆的详情。如果为每辆车都写一个单独的路由,那么每次数据变化时都要修改路由器。路由参数用一个路由定义解决了这个问题,路径中的占位符可以捕获任何出现在那个位置的值,页面读取这个值来获取正确的数据。本章涵盖这个模式,还有一个相关的技巧:在导航时传递额外信息,这样详情页就能记住列表应用了什么过滤条件。
动态段和 useParams
动态段是一个以冒号开头的路径部分。它不是匹配字面文本,而是匹配这个位置的任何内容,并将值保存在你选择的名称下:
<Route path="/vans/:id" element={<VanDetail />} />现在 /vans/1、/vans/42 和 /vans/anything 都会渲染 VanDetail。冒号标记 id 为路径内的一个变量。把它想象成函数参数:路由定义写一次,URL 提供参数值。列表页通常在映射数据时,用 Routing 中的 Link 组件链接到每一项,通过内插 id 来实现:
{vans.map(van => (
<Link key={van.id} to={`/vans/${van.id}`}>
<h3>{van.name}</h3>
</Link>
))}另一方面,useParams hook 返回一个对象,其属性对应于匹配路径中的每个动态段,以冒号后的名称作为键:
import { useParams } from 'react-router-dom'
export default function VanDetail() {
const params = useParams()
// 在 /vans/2 上 → { id: "2" }
return <h1>Van #{params.id}</h1>
}路径可以包含多个段,比如 /vans/:id/:type,每个都会变成各自的键。匹配的参数总是字符串,即使看起来像数字也一样,因为它直接来自 URL。
为参数获取数据
参数通常是获取请求的关键。详情页从 URL 中获取 id 并请求那一条记录,遵循 Fetching data 完整介绍的基于 effect 的模式:
import { useState, useEffect } from 'react'
import { useParams } from 'react-router-dom'
export default function VanDetail() {
const { id } = useParams()
const [van, setVan] = useState(null)
const [error, setError] = useState(null)
useEffect(() => {
let active = true
setVan(null)
setError(null)
fetch(`/api/vans/${id}`)
.then(res => {
if (!res.ok) throw new Error(`Request failed: ${res.status}`)
return res.json()
})
.then(data => { if (active) setVan(data.vans) })
.catch(err => { if (active) setError(err) })
return () => { active = false }
}, [id])
if (error) return <h2>Sorry, we couldn't load that van.</h2>
return van ? <h1>{van.name}</h1> : <h2>Loading...</h2>
}依赖数组包含 id 而不是空的。如果应用曾经从一个详情页链接到另一个,比如"类似车辆"部分,React Router 会交换 URL 和参数,而不卸载组件。空数组会跳过重新获取,让旧车辆留在屏幕上。依赖 id 会重新运行 effect,在运行的顶部清除 van 就会把"Loading..."放回屏幕,没有它的话,车 2 的名称会在整个车 5 的请求期间保持渲染。
res.ok 检查、catch 和 active 清理标志与 Fetching data 论证的保护措施相同,参数驱动的页面比大多数页面都更需要它们:id 来自任何人都可以编辑的地址栏,快速点击详情页是获得你已离开的车的响应在你正在查看的车之后的方式。
通过 Link 传递状态
参数将数据从 URL 传入页面。有时页面也想知道访客来自哪里。在这门课的 VanLife 项目中,列表页可以被过滤,比如只显示豪华车。点进一个,按"返回所有车",一个普通的返回链接会登陆到未过滤的列表:过滤条件丢失了,一旦涉及多个过滤条件就会很快变得烦人。
一个解决方案是把查询字符串复制到详情页的 URL 中,Search params 涵盖何时应该使用 URL 中的状态这样的情况:它能在与他人分享链接时存活。当这些信息只是当前访客的用户体验细节时,React Router 提供了一个更轻量的渠道。Link 接受一个 state prop,你传递的任何东西都随着导航而传递,不出现在 URL 中:
<Link
to={`/vans/${van.id}`}
state={{ search: `?${searchParams.toString()}`, type: typeFilter }}
>
<h3>{van.name}</h3>
</Link>searchParams 和 typeFilter 是列表页自己的 useSearchParams 值,来自 Search params 接下来完整介绍的 hook,所以链接携带完整的查询字符串加上当前的过滤名称。任何可序列化的值都可以,尽管一个带命名属性的对象读起来最清楚。
用 useLocation 读取它
目标页面用 useLocation hook 读取这个状态,它返回一个对象描述当前位置:pathname、search(当前 URL 自己的查询字符串)和 state,其中保存传入 Link 的任何东西:
import { Link, useLocation } from 'react-router-dom'
export default function VanDetail() {
const location = useLocation()
const search = location.state?.search || ''
const type = location.state?.type || 'all'
return (
<Link to={`..${search}`} relative="path">
← Back to {type} vans
</Link>
)
}这两个降级方案是重要的部分。直接登陆到 /vans/2 的人,来自书签、分享链接或手动输入的 URL,到达时 location.state 被设为 null,因为没有 Link 送他们来。读取 location.state.search 则会抛出错误,所以可选链加一个降级方案让页面保持运行:返回链接进入普通列表,文本读作"返回所有车"。有状态存在时,链接恢复完整的查询字符串,文本变成"返回豪华车"或其他任何过滤条件。
relative="path" prop 使 .. 爬升一个 URL 段而不是路由树中的一个级别,当详情路由的父级与其 URL 建议的不同时这很重要。Nested routes 解释了这个区别。
参数驱动页面上的失败路径
这门课把错误处理分为成功路径和失败路径:成功路径假设每个请求都成功,失败路径为失败的情况做准备。加载和错误机制本身属于 Fetching data:加载来自结果和错误仍都为空,在 catch 中设置 error 状态,以及提前返回一些反馈而不是在缺失数据上崩溃。
这章添加的是参数驱动页面增加了失败路径。URL 是用户可编辑的输入,所以 /vans/999 仅一个击键就出现,服务器会为不存在的 id 回复 404;上面的 res.ok 检查就是把那个响应转换成错误状态和消息而不是一个车形对象的东西。还有如上所述,导航状态可能不存在。一个通过参数到达网络的页面应该假设参数可能是错的,请求可能失败,状态可能是 null,并在每种情况下渲染合理的内容。
版本说明
在 React Router v5.1 之前,路由信息仅作为 props 到达:组件读 props.match.params 和 props.location。v5.1 添加了 useParams 和 useLocation hook,v6 移除了基于 prop 的 API,让 hook 成为唯一的方式。路由下的任何组件都可以调用它们,无论它渲染有多深。
/vans/:id,创建一个任何值都能填充的空白,所以一个路由为列表中的每个项目都提供一个详情页。在页面内,useParams 给你那个值,这样你就能获取正确的数据。 链接也能通过它们的 state prop 携带一个小的额外信息包,useLocation 在另一方读取它。记住如果有人从书签到达,这个包可能会丢失,所以总要准备好降级方案。
接下来:Search params,过滤和排序顺序存在于 URL 中的地方。

