React Router — 把框架能力收进同一个路由包
已复核React Router 8.3.0 是一个 React 路由库 + 可选全栈框架入口:同一份 react-router 包既提供浏览器 data router,也导出 SSR、request handler、cookie/session。日常类比:以前柜台(react-router-dom)和后厨(Remix server-runtime)是两家店;固定 8.3.0 把菜单印在同一本册子上,你按导出条件取用。
你写:
import { createBrowserRouter, RouterProvider } from "react-router";
const router = createBrowserRouter([ { path: "/posts/:postId", loader: ({ params }) => fetchPost(params.postId), Component: PostPage, },]);
<RouterProvider router={router} />createBrowserRouter 用 browser history 调用内部 createRouter(...).initialize()。路由器对象必须建在 React 树外,不能放进 component state。
不理解固定 8.3.0,下面这些事都会按 v6 文档推错:
- 为什么新项目
import { Link, Form } from "react-router",而 npm 上没有react-router-dom@8.3.0 - 为什么 React peer 是
>=19.2.7、Node engines 是>=22.22.0 - 为什么改
?tab=会让其它路由的 loader 默认重跑 - 为什么
href("/posts/:id", { id })只有在 typegen 写入Register.pages后才变成硬类型
固定源码的主链可以拆成五步:
-
路由表 → ranked branches:
matchRoutes先flattenAndRankRoutes,再按排名匹配。动态段带静态后缀的排名在 8.3.0 changelog 里修过,不能拿更早 v7 印象外推。 -
createBrowserRouter只是 history 适配:真正状态机是createRouter。默认dataStrategy为defaultDataStrategyWithMiddleware:没有 middleware 时,对所有shouldLoadmatch 并行resolve()。 -
loader 与 action 共用 data strategy:导航、fetcher、revalidate 都走同一套 match → strategy →
loaderData/actionData/errors。throw redirect()/data()是一等结果,不是业务层约定。 -
默认
shouldRevalidate比“路径变了才重拉”更宽:call-site 布尔优先;4xx/5xx action 默认跳过;forced revalidation、同一 URL、search 变化或新 route instance 默认为 true。search 变化会让其余 loader 默认重跑。 -
framework 入口在同一包:
createRequestHandler(build)把ServerBuild编成createStaticHandler,再处理 document / single-fetch。href()读Register.pages;没有 typegen 时类型退化为宽松AnyPages。
案例 1:data router 的 loader 在树外创建
Section titled “案例 1:data router 的 loader 在树外创建”import { createBrowserRouter, RouterProvider, useLoaderData } from "react-router";
const router = createBrowserRouter([ { path: "/posts/:postId", loader: async ({ params }) => { const post = await fetchPost(params.postId); if (!post) throw new Response("Not Found", { status: 404 }); return post; }, Component() { const post = useLoaderData(); return <article>{post.title}</article>; }, },]);声明式 <BrowserRouter><Routes> 仍导出,但没有 loader/action 状态机。要数据 API,必须走 createBrowserRouter + <RouterProvider>。
案例 2:search 变化会重跑其它 loader
Section titled “案例 2:search 变化会重跑其它 loader”{ path: "/posts", loader: () => fetchPosts(), children: [{ path: ":postId", loader: ({ params }) => fetchPost(params.postId) }],}从 /posts/1 跳到 /posts/1?tab=comments 时,默认 shouldRevalidate 因 search 变化变为 true。父列表 loader 也会重跑,除非该路由自己返回 false。这不是“只有 params 变才重拉”。
案例 3:href 的类型来自 Register,不是字符串魔法
Section titled “案例 3:href 的类型来自 Register,不是字符串魔法”import { Link, href } from "react-router";
<Link to={href("/posts/:postId", { postId: "123" })} />href 用 RFC 3986 path-segment 规则编码:$ & + , ; = : @ 保持字面量,/ ? # %、空白与非 ASCII 仍会转义。splat * 按段编码并保留 /。没有 typegen 时,TypeScript 不会替你守住路径表。
-
继续装
react-router-dom@8:固定审查日 npm latest 仍是7.18.2,不存在8.3.0。v8 的 DOM API 在react-router本包。 -
把 v6
useParams<{ id: string }>()当类型合同:params 运行时仍是字符串表;泛型不校验 URL。 -
以为改 query 只影响当前页:默认策略下 search 变化会重跑其它
shouldLoadloader。 -
把路由器放进
useState/ 组件内:文档与实现都要求在树外创建一次。 -
把 RSC / unstable_ 写成稳定合同*:8.3.0 changelog 里多条 RSC 能力标为 unstable,本文不提升为已验证行为。
适用 vs 不适用场景
Section titled “适用 vs 不适用场景”适用:
- 已接受 React 19.2+ / Node 22.22+ 的新 SPA 或 Remix 血统全栈应用
- 需要 loader/action/Form 渐进增强,而不是自己拼 fetch + 全局 cache
- 要在同一包里同时读浏览器路由和
createRequestHandler
不适用:
- 仍锁 React 18 的维护项目——固定 peer 是
>=19.2.7 - 必须继续使用独立
react-router-dom@8包名的工具链 - 想要编译期路径补全,但不跑 framework typegen,也不接受宽松
href类型 - 需要把静态阅读写成运行过的 SSR/RSC 证据
固定版本边界
Section titled “固定版本边界”- 本文绑定
remix-run/react-router@2edaca7a...,tag 与 npm 包均为react-router@8.3.0。 - npm 未暴露可比
gitHead;create-react-router@8.3.0同指该提交。 - 运行时依赖只有
cookie-es;条件导出区分 development/production 与react-server。 - 本文未安装依赖、未跑上游测试、未做 SSR/RSC/bundle 测量,状态保持
UNVERIFIED。
- 包名合并不等于 API 变简单——v8 把 Remix 能力收进
react-router,但 data router、声明式 router 和 framework handler 仍是三条入口。 - 默认 revalidate 是产品决策——search 变化重跑 loader,比“路径没变就复用”更贵,也更接近 document 请求。
- 类型要靠 Register,不靠字符串字面量——
href在未 typegen 时故意宽松。 - peer/engines 是硬边界——React 19.2.7 与 Node 22.22.0 不能从 v6 文档继承。
- 新项目要
Link/Form,固定 8.3.0 应该从哪个包 import? - 从
/posts/1导航到/posts/1?tab=meta,父路由 loader 默认会不会重跑? - 没有跑 typegen 时,
href("/nope/:id", { id: "1" })会不会被 TypeScript 拒绝?
检查点:
react-router本包。npm 上没有react-router-dom@8.3.0。- 会。search 变化使默认
shouldRevalidate为 true。 - 不会仅因路径不存在而拒绝;未注册
Register.pages时类型是宽松的。
- 固定源码:remix-run/react-router —— 本文绑定提交
2edaca7a4f12a50cad002d55d84f73b0cdd462b6 - 文档:reactrouter.com(library / framework 入口需按版本核对)
- remix —— 框架能力并入 React Router 之前的独立产品页
- tanstack-router —— 另一条“URL 当类型”的客户端路由合同
- next-js —— RSC 优先的另一套路由/数据边界
- remix —— 同一作者团队;v8 把 server-runtime 收进本包
- tanstack-router —— 类型先行的客户端路由对照
- tanstack-query —— 若不用 loader,常见的客户端缓存层
- react —— 固定 peer
>=19.2.7 - vite —— framework mode 常见打包宿主,不是本包的硬依赖
- remix —— Remix — 拥抱 Web 标准的 React 全栈框架