Skip to content
This page has been auto-translated and may contain errors.View in English

路由参数和位置信息

列表页和详情页是带路由应用的经典组合:/vans 显示所有的车辆,点击其中一个应该打开 /vans/2 显示该车辆的详情。如果为每辆车都写一个单独的路由,那么每次数据变化时都要修改路由器。路由参数用一个路由定义解决了这个问题,路径中的占位符可以捕获任何出现在那个位置的值,页面读取这个值来获取正确的数据。本章涵盖这个模式,还有一个相关的技巧:在导航时传递额外信息,这样详情页就能记住列表应用了什么过滤条件。

动态段和 useParams

动态段是一个以冒号开头的路径部分。它不是匹配字面文本,而是匹配这个位置的任何内容,并将值保存在你选择的名称下:

jsx
<Route path="/vans/:id" element={<VanDetail />} />

现在 /vans/1/vans/42/vans/anything 都会渲染 VanDetail。冒号标记 id 为路径内的一个变量。把它想象成函数参数:路由定义写一次,URL 提供参数值。列表页通常在映射数据时,用 Routing 中的 Link 组件链接到每一项,通过内插 id 来实现:

jsx
{vans.map(van => (
  <Link key={van.id} to={`/vans/${van.id}`}>
    <h3>{van.name}</h3>
  </Link>
))}

另一方面,useParams hook 返回一个对象,其属性对应于匹配路径中的每个动态段,以冒号后的名称作为键:

jsx
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 的模式:

jsx
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 检查、catchactive 清理标志与 Fetching data 论证的保护措施相同,参数驱动的页面比大多数页面都更需要它们:id 来自任何人都可以编辑的地址栏,快速点击详情页是获得你已离开的车的响应在你正在查看的车之后的方式。

参数将数据从 URL 传入页面。有时页面也想知道访客来自哪里。在这门课的 VanLife 项目中,列表页可以被过滤,比如只显示豪华车。点进一个,按"返回所有车",一个普通的返回链接会登陆到未过滤的列表:过滤条件丢失了,一旦涉及多个过滤条件就会很快变得烦人。

一个解决方案是把查询字符串复制到详情页的 URL 中,Search params 涵盖何时应该使用 URL 中的状态这样的情况:它能在与他人分享链接时存活。当这些信息只是当前访客的用户体验细节时,React Router 提供了一个更轻量的渠道。Link 接受一个 state prop,你传递的任何东西都随着导航而传递,不出现在 URL 中:

jsx
<Link
  to={`/vans/${van.id}`}
  state={{ search: `?${searchParams.toString()}`, type: typeFilter }}
>
  <h3>{van.name}</h3>
</Link>

searchParamstypeFilter 是列表页自己的 useSearchParams 值,来自 Search params 接下来完整介绍的 hook,所以链接携带完整的查询字符串加上当前的过滤名称。任何可序列化的值都可以,尽管一个带命名属性的对象读起来最清楚。

用 useLocation 读取它

目标页面用 useLocation hook 读取这个状态,它返回一个对象描述当前位置:pathnamesearch(当前 URL 自己的查询字符串)和 state,其中保存传入 Link 的任何东西:

jsx
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">
      &larr; 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.paramsprops.location。v5.1 添加了 useParamsuseLocation hook,v6 移除了基于 prop 的 API,让 hook 成为唯一的方式。路由下的任何组件都可以调用它们,无论它渲染有多深。

Link 状态是对浏览器 History API 的轻量包装。每个历史条目可以通过 history.pushState 携带一个 state 值,React Router 在导航发生时把你的 state prop 存储在那里。这个机制解释了它的行为方式:在 BrowserRouter 下,值存活于页面刷新和返回和前进按钮,因为它存在于历史条目本身,浏览器会持久化这些。它在 URL 传播时消失,因为地址中没有任何关于它的东西:把链接粘贴到另一个浏览器,location.state 就是 null

这为按导航数据的位置给出了一个干净的决定规则。任何应该存活于分享、收藏或新访问的东西都进入 URL 作为搜索参数。任何是当前访客会话的体贴的东西,比如在返回按钮后恢复过滤,适合历史状态。任何一种方式,都把 location.state 当作不信任的且在每个读取位置可能是 null;直接登陆的情况在规模上是确定的。

参数应该得到同样的怀疑加两个机制说明。首先,一个匹配的段到达作为字符串,而一个可选段如 /vans/:id? 没有匹配到达作为 undefined(一个 splat 没有匹配给出空字符串),这就是为什么 TypeScript 类型是 string | undefined。一个字符串 id 用 === 与一个数字比较会无声地失败,所以在边界转换:Number(id) === van.id

其次,一个参数变化重新渲染已挂载的组件而不是重新挂载它,这就是为什么 fetch effect 必须在其依赖中列出参数,以及为什么任何从参数初始化的派生状态在它变化时需要重置。

路由器也在动态段之前匹配静态段,所以 /vans/new 可以与 /vans/:id 共存:React Router 按路由特异性排名而不是取定义顺序,这让一个字面路由不被相邻的参数吞没。

Juno一个路由,多个页面 路由路径中的冒号,如 /vans/:id,创建一个任何值都能填充的空白,所以一个路由为列表中的每个项目都提供一个详情页。在页面内,useParams 给你那个值,这样你就能获取正确的数据。

链接也能通过它们的 state prop 携带一个小的额外信息包,useLocation 在另一方读取它。记住如果有人从书签到达,这个包可能会丢失,所以总要准备好降级方案。

Juno一个路由,多个页面 定义一次 path="/vans/:id",用 useParams 读 id,把它放在你的 fetch effect 的依赖数组中,这样在详情页间导航时会重新获取。

要让过滤活着待在返回按钮后面,在 Linkstate prop 中传递查询字符串,通过 location.state 用可选链和降级方案读取它,因为直接登陆到达时 statenull

以与任何获取相同的方式处理加载、错误和坏 id,把 URL 当作用户输入。

Juno一个路由,多个页面 Link 状态通过 pushState 乘坐于历史条目,所以它存活于刷新和返回导航但从不跨浏览器或分享的 URL;把可分享的数据放在搜索参数中,保留历史状态给会话本地的体贴。

一个匹配的参数是字符串,参数变化重新渲染而不卸载,所以 effect 必须依赖它。路由匹配按特异性排名,让静态段如 /vans/new 安全地与 /vans/:id 共存。

接下来:Search params,过滤和排序顺序存在于 URL 中的地方。