如何用结构共享与 select 细粒度订阅减少 TanStack Router 组件重复渲染
【免费下载链接】router🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router
如果你用 TanStack Router(@tanstack/react-router)把分页、筛选条件等状态放在 URL search params 里,可能会遇到这种情况:某个组件只用到了search.foo,但用户修改search.bar后它也跟着重新渲染。TanStack Router 提供了两个互补的手段来解决这个问题:**细粒度选择器(select)**让组件只订阅 router state 的一个子集,**结构共享(structural sharing)**让select返回的对象在值没变时保持引用稳定。本文按"先用select缩小订阅范围,再按需开启 structural sharing"的顺序给出完整配置路径,全部内容来自仓库内的 Render Optimizations 指南。
重复渲染来自哪里
先明确文档给出的两个触发点,这决定了后两步优化分别针对什么:
- 订阅了不需要的状态。像
useSearch、useRouterState这类 hook 返回完整的 search 对象或 router state,URL 中任意 search param 变化都会让订阅它的组件重新渲染。 select每次返回新对象。当select函数返回对象时,每次调用都会产生一个新的对象引用,即使内部字段值完全没变,组件也会重新渲染。
另外要注意:结构共享默认是关闭的,文档说明这是为了保持向后兼容,且这一点在 v2 中可能会改变(见 Render Optimizations)。所以在做任何订阅优化之前,先确认你的项目没有开启它。
第一步:用select缩小订阅范围
useSearch支持select选项:函数接收 search 对象,返回值直接作为 hook 的返回值。官方文档给出的示例是一个带foo、bar两个 search params 的details路由:
// component won't re-render when `bar` changes const foo = Route.useSearch({ select: ({ foo }) => foo })文档对上面这行的行为判定是:当导航从/details?foo=f1&bar=b1到/details?foo=f1&bar=b2时,组件不会因为bar的变化而重新渲染。
Route.useSearch()是路由上的类型化 hook;如果在别的组件里读取某条路由的 search params,使用顶层useSearch并传入from选项(useSearch hook API):
import { useSearch } from '@tanstack/react-router' const selected = useSearch({ from: '/posts/$postId', select: (search) => search.postView, })select不是useSearch独有的。useRouterState同样提供select与structuralSharing两个选项,例如只订阅 location:
const selected = useRouterState({ select: (state) => state.location, })useRouterState hook API 中还有一条建议:如果只是要拿当前 location 或 matches,优先考虑useLocation、useMatches这类 hook,它们比直接读useRouterState更轻量。此外,仓库 API 文档中useMatches、useChildMatches、useParentMatches、useParams、useLoaderData、useLoaderDeps、useMatch等 hook 也都定义了structuralSharing选项,订阅 router state 的其他位置可以按同样思路处理。
到这一步能解决"订阅面过大"的问题。但如果你希望select返回对象(而不是单个字段),就会撞上第二个触发点:
const result = Route.useSearch({ select: (search) => { return { foo: search.foo, hello: `hello ${search.foo}`, } }, })文档明确指出:这样写的话,select每次调用都返回一个新对象,组件会因此重新渲染。解决办法是下一步的 structural sharing。
第二步:开启 structural sharing 让 select 返回的对象保持引用稳定
TanStack Router 对 search params 本来就内置了结构共享:导航只改bar时,search.foo保持引用稳定,只有search.bar被替换(见 Render Optimizations)。对于select返回的值,需要显式开启结构共享,文档给了两种互斥的开启方式:
方式一:在 router 选项中全局默认开启
const router = createRouter({ routeTree, defaultStructuralSharing: true, })defaultStructuralSharing是RouterOptions的一个可选项,类型boolean,默认值为false,作用是"为细粒度选择器默认启用 structural sharing"(见 RouterOptions API)。
方式二:按 hook 调用点单独开启
const result = Route.useSearch({ select: (search) => { return { foo: search.foo, hello: `hello ${search.foo}`, } }, structuralSharing: true, })structuralSharing选项的定义是:配置select返回值是否启用 structural sharing(见 useSearch hook API 的opts.structuralSharing一节)。
选择建议:如果整个应用里大量 hook 都需要select返回对象,用方式一全局开启更省事;如果只是个别订阅点需要,用方式二局部开启,避免影响其他调用点的默认行为。
如何验证优化是否生效
文档给出的可观察判定如下,可直接用于核对:
- 订阅收窄生效:按第一步的示例,导航只修改未被订阅的参数(如
bar)时,使用该select订阅的组件不应重新渲染。这是文档对select: ({ foo }) => foo的直接行为描述。 - 结构共享生效:导航
/details?foo=f1&bar=b1→/details?foo=f1&bar=b2时,search.foo引用保持不变,只有search.bar被替换。 - 类型层面对开启 structural sharing 的调用做约束:如果你让
select返回 class 实例(如new Date())并开启structuralSharing: true,TypeScript 会直接报错:
const result = Route.useSearch({ select: (search) => { return { date: new Date(), } }, structuralSharing: true, })也就是说,非 JSON 兼容的返回值在编译期就能被拦截,这本身也是一次免费的检查。
限制与回退
- structural sharing 只支持 JSON 兼容的数据。开启后不能用
select返回 class 实例之类的值;上面的new Date()例子在类型上会报错,这是预期行为而不是 bug。 - 如果按方式一在 router 选项里全局开启了
defaultStructuralSharing,个别必须返回非 JSON 数据(或不想做结构共享)的调用点可以单独设置structuralSharing: false来规避。 - 默认关闭且 v2 可能改变:文档明确说明结构共享默认关闭是为了向后兼容,"this may change in v2"。升级版本时建议重新确认
defaultStructuralSharing的默认值。
完成上述两步后,"订阅面过大"和"select返回新对象"两个触发点都已被覆盖:前者用select解决,后者用defaultStructuralSharing或逐点的structuralSharing解决。更多 hook 的select/structuralSharing细节可查阅 Render Optimizations 指南 与docs/router/api/router/下各 hook 的 API 文档。
【免费下载链接】router🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考