ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Relay 数据驱动的 fragment 重新获取:useRefetchableFragment 完整实战指南

Relay 数据驱动的 fragment 重新获取:useRefetchableFragment 完整实战指南 Relay 数据驱动的 fragment 重新获取useRefetchableFragment 完整实战指南【免费下载链接】relayRelay is a JavaScript framework for building>项目地址: https://gitcode.com/gh_mirrors/relay29/relay导读useRefetchableFragment是 Relay 提供的 React Hook用于在已有 fragment 数据的基础上以新的变量重新获取并渲染该 fragment 的数据例如切换评论的语言、刷新用户资料。本文以 Relay v17 官方 API 文档为骨架结合本仓库的运行时源码与编译器实现完整讲解其用法、参数、返回值、底层机制以及与旧版RefetchContainer的差异帮助你写出类型安全、可维护的可重取 fragment 组件。适用场景与核心价值在数据驱动的 React 应用中经常会遇到这样的需求某个组件已经通过 fragment 渲染了数据但当用户做出某个操作时如点击翻译评论、刷新按钮需要用一组不同的变量重新获取这份数据。useRefetchableFragment正是为此设计的它让 fragment 组件自己拥有重新获取数据的能力而不必把 refetch 查询提升到父级它由 Relay 编译器根据 fragment 上的refetchable指令自动生成对应的 refetch 查询开发者无需手写它与 Relay 的 store 缓存、Suspense、fetch policy 体系无缝集成。快速上手完整示例以下示例来自官方 API 文档演示了一个评论翻译组件点击按钮后用新的lang变量重新获取评论正文。import type {CommentBody_comment$key} from CommentBody_comment.graphql; const React require(React); const {graphql, useRefetchableFragment} require(react-relay); type Props { comment: CommentBody_comment$key, }; function CommentBody(props: Props) { const [data, refetch] useRefetchableFragment( graphql fragment CommentBody_comment on Comment refetchable(queryName: CommentBodyRefetchQuery) { body(lang: $lang) { text } } , props.comment, ); return ( p{data.body?.text}/p Button onClick{() { refetch({lang: SPANISH}, {fetchPolicy: store-or-network}) }} Translate Comment /Button / ); } module.exports CommentBody;这个例子包含了useRefetchableFragment的全部关键要素fragment 定义使用graphql模板字面量定义并带有refetchable(queryName: CommentBodyRefetchQuery)指令fragment reference 传入第二个参数props.comment是父级通过useFragment等途径传来的不透明 fragment 引用解构返回值[data, refetch]元组data用于渲染refetch用于触发重新获取触发重新获取调用refetch({lang: SPANISH}, {fetchPolicy: store-or-network})只需传入 fragment 内部引用的变量。核心前提refetchable指令与自动生成的查询使用useRefetchableFragment有一个硬性前提fragment 必须带有refetchable指令否则调用会抛出错误。fragment CommentBody_comment on Comment refetchable(queryName: CommentBodyRefetchQuery) { body(lang: $lang) { text } }什么样的 fragment 才是 refetchable 的refetchable指令只能加在满足以下条件的 fragment 上定义在Viewer或Query类型上定义在实现了Node接口的类型上即该类型拥有id字段并且可以通过id被查询。Node接口是 GraphQL 服务端规范中的节点约定实现Node的类型拥有全局唯一的idRelay 通过node(id: $id)这样的根字段来按 id 重新查询该对象。仓库的编译器测试中可以看到典型用例fragment fragmentOnNodeInterface_RefetchableFragment on Node refetchable(queryName: RefetchableFragmentQuery) { id ... on User { name ...fragmentOnNodeInterface_ProfilePicture alias } }该 fixture 位于 fragment-on-node-interface.graphql展示了 fragment 定义在Node接口上的合法形态。不需要手写 refetch 查询你不需要手动编写 refetch 查询。Relay 编译器在看到refetchable指令后会自动生成一个名为queryName指定的查询。这一点可以从编译器测试的产物中得到直接印证在 refetchable-fragment-with-connection.expected 中输入 fragment 为refetchableFragmentWithConnection_PaginationFragment定义在Node上带refetchable(queryName: RefetchableFragmentQuery)输出的人工产物是一个名为RefetchableFragmentQuery的 Fragment 节点其 selections 是{ alias: null, args: [{kind: Variable, name: id, variableName: id}], concreteType: null, kind: LinkedField, name: node, plural: false, selections: [ { args: [ {kind: Variable, name: count, variableName: count}, {kind: Variable, name: cursor, variableName: cursor} ], kind: FragmentSpread, name: refetchableFragmentWithConnection_PaginationFragment } ], type: Query }可以看到 Relay 自动生成了一个node(id: $id)查询把原 fragment 作为 spread 挂在其下并自动补充了id变量。这正是refetchable的编译器级实现对定义在Node上的 fragment生成node(id: $id)形式的查询。编译完成后Relay 还会为这个自动生成的查询生成 Flow 类型可以直接从生成文件中导入import type {CommentBodyRefetchQuery} from CommentBodyRefetchQuery.graphql;编译器底层transform 三步走从源码层面看这一自动生成逻辑位于 Rust 编译器 crates 中。compiler/crates/relay-transforms/src/refetchable_fragment.rs开头的注释精确描述了整个 transform 的三阶段流程验证Validation校验标记了refetchable的 fragment 是否具备生成 refetch 查询的资格——主要检查 fragment 的类型能否以某种规范方式被重新获取变量推断Variable inference为每个生成的查询确定变量定义。GraphQL 本身没有fragment 局部变量的概念Relay 虽然引入了这个概念但开发者仍可能引用全局变量因此需要遍历每个refetchablefragment 可达的所有 fragment求出期望定义的全局变量的并集构建查询Query building从 Fragment 到 Root IR 节点的直接拷贝式转换。对于不同类型的 fragment 基座该目录下分别提供了独立的查询生成器node_query_generator.rs面向Node、viewer_query_generator.rs面向Viewer、query_query_generator.rs面向Query。编译器同样会拦截非法用法。例如 refetchable_conflict_with_operation.invalid.graphql 演示了queryName与已有查询同名时的命名冲突错误# expected-to-throw fragment refetchableConflictWithOperationF on Node refetchable(queryName: refetchableConflictWithOperationQuery) { __typename } query refetchableConflictWithOperationQuery { node(id: y) { __typename } }这类.invalid.graphqlfixture 说明refetchable的约束命名冲突、参数冲突、缺失id等都会在编译期被可靠地校验而不是留到运行时才暴露。参数Arguments详解useRefetchableFragment接收两个参数。fragment用graphql模板字面量指定的 GraphQL fragment。要求与上文一致必须带有refetchable指令且该指令只能加在可 refetch的 fragment 上定义于Viewer、Query或实现了Node的类型。需要强调的是无需手动指定 refetch 查询本身refetchable指令会按queryName自动生成查询并同时为这个查询生成 Flow 类型可以从生成文件queryName.graphql.js导入。fragmentReferencefragment reference是一个不透明的 Relay 对象Relay 用它从 store 中读取该 fragment 的数据更具体地说它包含了数据应该从哪个对象实例读取的信息。其类型可以从生成的 Flow 类型文件fragment_name.graphql.js中导入命名为fragment_name$key用于声明Props的类型社区使用 eslint-plugin-relay。一个典型的 Props 声明如下type Props { comment: CommentBody_comment$key, // 由生成的 CommentBody_comment.graphql.js 提供 };fragment reference 既可以是非空的$key也可以是可空的?$key这会直接影响refetch函数对变量的要求——详见下一节。返回值Return Value详解useRefetchableFragment返回一个二元组[data, refetch]。[0] data从 Relay store 中读取出的数据对象其形状与 fragment 定义一致。data的 Flow 类型同样与 fragment 形状匹配并由 GraphQL Schema 推导而来。在仓库源码 useRefetchableFragment.js 中可以看到返回值类型ReturnType会依据传入的 fragment ref 类型是否为可空做条件类型推导如果 ref 类型可空则data类型为?TData可空否则为TData非空。这保证了类型系统层面的一致安全export type ReturnTypeTVariables, TData, TKey [ [readonly key: TKey] extends [ readonly key: {readonly $fragmentSpreads: unknown, ...}, ] ? TData : ?TData, RefetchFnTVariables, TKey, ];[1] refetch用于以一组可能全新的变量重新获取 fragment 的函数。参数variables包含用于执行refetchable查询的新变量值。变量语义有以下几条关键规则这些变量需要与 fragment 内部引用的 GraphQL 变量匹配如果传给useRefetchableFragment的 fragment key 是可空的optional那么必须传入所有非可选变量其中可能包含对象的id——因为此时 Relay 没有现成的变量可以复用如果 fragment key 是非空的non-optional则只需传入本次 refetch 中想要改变的变量fragment 引用到的、但本次调用中省略的变量会回退到原始父查询中的值。因此想用与首次获取完全相同的变量重取 fragment直接调用refetch({})即可同样在 fragment key 非空的情况下$id变量的传入是可选的除非你确实想用不同的id重取。当重取一个非空refetchablefragment 时Relay 已经知道当前渲染对象的 id。从运行时源码 useRefetchableFragmentInternal.js 可以确认变量合并的实现。在useRefetchFunction内部最终的 refetch 变量是三层合并的结果const refetchVariables: VariablesOfTQuery { ...(parentVariables as $FlowFixMe), // 1. 父查询变量 ...fragmentVariables, // 2. 当前 fragment 变量 ...providedRefetchVariables, // 3. 本次调用显式传入的变量 };即调用者传入的变量拥有最高优先级未传入的变量依次回退到 fragment 当前变量、父查询变量——这与文档描述的省略的变量回退到原始父查询值完全一致。同时如果查询需要标识符id或类似字段而调用者未显式提供Relay 会尝试从 fragment 数据中读取并补上if ( identifierInfo ! null !providedRefetchVariables.hasOwnProperty( identifierInfo.identifierQueryVariableName, ) ) { ... (refetchVariables as $FlowFixMe)[ identifierInfo.identifierQueryVariableName ] identifierValue; }这里identifierValue取自 fragment 数据中的identifierField如id正是非空 fragment key 时$id可省略这一语义的运行时来源。参数options可选options是一个可选对象包含选项类型说明fetchPolicyFetchPolicy决定是否使用缓存数据以及在有可用缓存时何时发起网络请求。可取store-or-network默认优先读缓存、必要时请求网络、store-and-network立即读缓存并同时请求网络以刷新、network-only忽略缓存强制网络请求等具体语义可查阅 Fetch Policies 章节onComplete(Error \| null) void在 refetch 请求完成时被调用包括任何增量数据 payload 完成时注意源码中的Options类型还包含一个UNSTABLE_renderPolicy字段见 useRefetchableFragmentInternal.js 的类型定义export type Options { fetchPolicy?: FetchPolicy, onComplete?: (Error | null) void, UNSTABLE_renderPolicy?: RenderPolicy, };UNSTABLE_renderPolicy用于控制 refetch 后渲染时对 store 中完整数据的依赖策略full或partial属于实验性能力名称中的UNSTABLE_前缀表示 API 未来可能调整。返回值disposablerefetch函数返回一个包含dispose函数的对象。调用disposable.dispose()会取消本次 refetch 请求。运行时实现中dispose直接复用了useQueryLoader暴露的disposeQueryloadQuery(refetchQuery.request.variables, { __environment: refetchEnvironment, __nameForWarning: refetch, fetchPolicy, }); ... return {dispose: disposeQuery};即每次refetch都会通过loadQuery发起或读取缓存新的查询同时 dispose 掉上一次的 refetch 查询。行为suspend 与重新渲染用一组新变量调用refetch会以新变量重新获取 fragment。你只需要提供 fragment 内部引用到的变量。在示例中即通过给lang传新值来获取当前渲染评论的翻译版正文调用refetch会重新渲染组件并且根据指定的fetchPolicy以及缓存数据是否可用组件可能会suspend如果它需要发送并等待网络请求。如果 refetch 导致组件挂起你需要确保该组件外层包裹了Suspense边界。Behavior自动订阅与 SuspenseuseRefetchableFragment的两个重要行为特性自动订阅更新组件会自动订阅 fragment 数据的更新。如果这个特定对象的数据在应用任何位置被更新例如获取了新数据或执行了 mutation组件会自动用最新数据重新渲染。这一行为来自内部对useFragmentInternal的调用——useRefetchableFragmentInternal.js 在每次渲染时都通过它读取并订阅 fragment缺失数据时 suspend如果该 fragment 的某些数据缺失且这些数据正在被某个父查询获取组件会挂起直到数据就绪。关于 Suspense 的更多细节可参考 Loading States with Suspense 指南。从源码看refetch 请求在组件挂起前后的完整数据流为refetch()调用 →loadQuery启动必要时发起网络请求并更新 queryRef→ 下一次渲染时QueryResource.prepare消费该 observable若请求仍在途则挂起 → 请求完成后读取查询响应通过getValueAtPath从响应中提取新的 fragment ref指向以 refetch 查询为 owner 的新引用→ 再用useFragmentInternal以新 ref 读取数据。这样既保证了 suspend 的正确性也保证了更新后的数据来源一致。与RefetchContainer的差异useRefetchableFragment相对旧版RefetchContainer高阶组件 API有三点关键差异无需手写 refetch 查询旧的 API 中需要开发者自己编写并传入 refetch 查询新 API 通过refetchablefragment 由 Relay 自动生成查询不再区分refetchVariables与renderVariables这两个概念在旧 API 中定义较为模糊新 API 中 refetch 会始终以你提供的变量正确地重取并渲染 fragment省略的变量回退到父查询中的原始值refetch 必定更新组件旧RefetchContainer中调用 refetch 不一定触发组件更新取决于 refetch 查询的内容以及 fragment 定义的对象类型是否正确新 API 消除了这种不确定性。从仓库历史代码可以看到旧实现的痕迹legacy/useRefetchableFragmentNode.js 保留了旧版节点逻辑供旧 API 兼容使用而新版实现则完全建立在useQueryLoaderQueryResource.prepareuseFragmentInternal的组合之上。源码级工作流总结将文档描述与仓库实现结合起来useRefetchableFragment的完整工作链路如下编译期transform_refetchable_fragmentrefetchable_fragment.rs验证refetchablefragment 的合法性 → 推断全局变量并集 → 生成node(id:)/viewer/query形式的 refetch 查询及其 Flow 类型运行时初始化useRefetchableFragmentuseRefetchableFragment.js通过getFragment解析 fragment 节点再委托给useRefetchableFragmentInternal运行时 refetchuseRefetchFunction合并三层变量、补全id标识符、构建OperationDescriptor、调用loadQuery启动请求并返回可dispose的句柄渲染通过QueryResource.prepare可能挂起getValueAtPath提取新 fragment ref useFragmentInternal读取并订阅完成数据更新与自动重渲染。仓库中对应的运行时测试可进一步佐证上述行为例如 useRefetchableFragment-test.js 与 useRefetchableFragmentNode-test.js它们覆盖了 refetch 变量合并、fetchPolicy 行为、dispose 取消、Suspense 边界等场景。常见误用与注意事项结合源码中的 warning 与断言使用时有几个容易踩坑的点不要在组件卸载后调用refetch源码会检测isMountedRef.current若组件已卸载仍调用 refetch会打印 warning 并返回一个 no-op 的dispose请务必清理定时器、interval、异步回调等可能触发请求的路径不要在 null fragment ref 上调用refetch当 fragment key 可空且当前为 null 时调用 refetchRelay 无法获知初始 fragment 数据会给出 warning。此时要么先保证传入合法 ref要么在调用时传全所有必需变量id一致性由服务端保证开发模式下 Relay 会校验 refetch 返回的id与请求时记录的id一致以及返回的__typename与 store 中已有记录一致见源码中的checkSameIDAfterRefetch/checkSameTypeAfterRefetch调试函数。如果不一致说明服务端没有正确实现唯一 id 要求Relay 会打印相应 warning务必在外层包裹Suspense当fetchPolicy无法命中缓存、需要等待网络时refetch 会导致组件 suspend缺少Suspense边界会抛错。结语useRefetchableFragment把fragment 级的数据重新获取从手写查询、手工管理变量与更新的繁琐流程中解放出来refetchable指令在编译期自动生成查询与类型refetch函数在运行时统一处理变量合并、缓存策略、取消与 Suspense。结合编译器 transform 与运行时 Hooks 的源码实现可以看到它的变量回退、id补全、dispose 取消等语义都有清晰且可验证的落地代码支撑是 Relay 现代 Hooks 体系中可靠的数据刷新方案。【免费下载链接】relayRelay is a JavaScript framework for building>项目地址: https://gitcode.com/gh_mirrors/relay29/relay创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表