Relay 查询刷新(Refreshing Queries)实战指南:用 useQueryLoader 与 useLazyLoadQuery 拉取最新数据

Relay 查询刷新(Refreshing Queries)实战指南:用 useQueryLoader 与 useLazyLoadQuery 拉取最新数据

【免费下载链接】relay Relay is a JavaScript framework for building data-driven React applications. 【免费下载链接】relay 项目地址: https://gitcode.com/gh_mirrors/relay29/relay

本篇指南聚焦 Relay 中"刷新查询"(refreshing a query)这一核心场景:以完全相同的变量重新请求并渲染同一份查询数据,从而从服务器获取最新版本。文章围绕 useQueryLoader / loadQuery 与 useLazyLoadQuery 两条主流路线,结合 network-only / store-only 等 fetchPolicy 的取舍,给出可复制的完整代码示例,并深入底层源码(loadQuery.js、useQueryLoader.js、useLazyLoadQuery.js、fetchQuery.js)解释其工作原理。读完你可以在"点击按钮刷新计数"等典型交互中,自主选择最合适的刷新实现,并理解 Suspense 边界在其中扮演的角色。

什么是"刷新查询"

在 Relay 中,"刷新查询"(refreshing a query)特指:用与最初渲染时完全相同的 variables 重新获取同一查询的数据,以获得服务器上最新的版本。例如"朋友数量"这种随时可能变化的计数,用户点击"Fetch latest count"按钮时,我们希望拿到的仍然是同一个 user(id: $id) 的 friends { count },只是数据是最新的。

这与本目录下的另外两篇文档所讲的场景有本质区别:

刷新查询则是"同样的数据、最新的版本",是上述所有场景中请求成本最低、实现最简单的一种。它的实现方式取决于你最初用哪种方式获取该查询:useQueryLoader / loadQuery 路线,还是 useLazyLoadQuery 路线。

刷新前先考虑:能否使用实时功能

在动手实现"手动刷新"之前,Relay 官方首先建议评估一个问题:这个数据是否适合用实时能力来自动保持最新,从而免去周期性手动刷新的麻烦。

一个典型例子是 GraphQL Subscriptions:当服务器数据变化时,订阅会主动推送更新,客户端无需发起新的完整查询。当然,使用订阅需要额外配置:

  • 服务器端需要支持订阅操作;
  • 客户端的 network layer 需要配置好 WebSocket 等传输层。

如果实时方案在当前场景下成本过高或不合适(例如数据变化频率低、服务器不支持订阅),再回到手动刷新方案。这也是 Relay 文档反复强调的:先想清楚是否真的需要手动刷新,实时功能往往是更省力的答案。

方案一:useQueryLoader / loadQuery 路线

如果查询最初是通过 useQueryLoader 加载的(即文档 Fetching Queries for Render 中描述的 "render-as-you-fetch" 模式),刷新查询只需再次调用 loadQuery 即可:

/**
 * App.react.js
 */

const AppQuery = require('__generated__/AppQuery.graphql');

function App(props: Props) {
  const [queryRef, loadQuery] = useQueryLoader(
    AppQuery,
    props.appQueryRef /* initial query ref */
  );

  const refresh = useCallback(() => {
    // Load the query again using the same original variables.
    // Calling loadQuery will update the value of queryRef.
    // The fetchPolicy ensures we always fetch from the server and skip
    // the local data cache.
    const {variables} = props.appQueryRef;
    loadQuery(variables, {fetchPolicy: 'network-only'});
  }, [/* ... */]);

  return (
    <React.Suspense fallback="Loading query...">
      <MainContent
        refresh={refresh}
        queryRef={queryRef}
      />
    </React.Suspense>
  );
}
/**
 * MainContent.react.js
 */

// Renders the preloaded query, given the query reference
function MainContent(props) {
  const {refresh, queryRef} = props;
  const data = usePreloadedQuery(
    graphql`
      query AppQuery($id: ID!) {
        user(id: $id) {
          name
          friends {
            count
          }
        }
      }
    `,
    queryRef,
  );

  return (
    <>
      <h1>{data.user?.name}</h1>
      <div>Friends count: {data.user.friends?.count}</div>
      <Button
        onClick={() => refresh()}>
        Fetch latest count
      </Button>
    </>
  );
}

逐点拆解

  1. 在事件处理器中调用 loadQuery:loadQuery 被放在 refresh 回调(如按钮的 onClick)中,而不是渲染期间,因此网络请求会立即开始;随后新生成的 queryRef 被传入使用 usePreloadedQuery 的 MainContent,从而渲染更新后的数据。
  2. fetchPolicy: 'network-only':确保每次刷新都强制走网络、跳过本地数据缓存(Relay Store)。关于四种 fetchPolicy 的完整语义见下文。
  3. Suspense 行为:因为 network-only 必然发起网络请求,调用 loadQuery 会让组件重新渲染并导致 usePreloadedQuery 挂起(suspend)(详见 Loading States with Suspense),因此必须用 <React.Suspense> 包裹 MainContent 以展示 fallback 加载态。

源码视角:loadQuery 为什么"再调用一次"就是刷新

从源码看,每次调用 loadQuery 都会生成一个新的 fetchKey:

// packages/react-relay/relay-hooks/loadQuery.js
// Every time you call loadQuery, we will generate a new fetchKey.
// This will ensure that every query reference that is created and
// passed to usePreloadedQuery is independently evaluated,
// even if they are for the same query/variables.
// Specifically, we want to avoid a case where we try to refetch a
// query by calling loadQuery a second time, and have the Suspense
// cache in usePreloadedQuery reuse the cached result instead of
// re-evaluating the new query ref and triggering a refetch if
// necessary.
fetchKey++;

这段注释揭示了关键设计意图:第二次调用 loadQuery 时,Suspense 缓存不会"复用"上一次的查询结果,而是强制重新求值并重新发起请求。此外,loadQuery 内部还会执行 environment.retain(operation) 将查询数据保留在 store 中(防止被垃圾回收),并在构造返回的 PreloadedQuery 时写入 fetchKey、fetchPolicy、networkCacheConfig 等字段(见 loadQuery.js),供 usePreloadedQuery 消费。

在 useQueryLoader 一侧(useQueryLoader.js),每次调用 loadQuery 回调都会:

  • 用 loadQuery() 生成新的 PreloadedQuery;
  • 把它加入 undisposedQueryReferencesRef 集合跟踪;
  • 调用 setQueryReference 更新 state,触发重新渲染。

同时,useQueryLoader 会通过 releaseQuery() / dispose() 清理旧的 query reference,避免 store 中数据泄漏(useQueryLoader.js)。这保证了"反复刷新"不会造成旧数据无限堆积。

方案一的进阶:需要避免 Suspense 怎么办

network-only 必然触发挂起,而 Suspense fallback 会隐藏已经渲染好的内容。如果希望刷新期间保留当前界面(例如只在角落显示一个小 spinner),可以改用 fetchQuery + 手动管理 loading state 的组合:

注:在未来的 React 版本中,当支持并发渲染(concurrent rendering)后,React 将提供一种选项,在挂起时避免用 Suspense fallback 隐藏已渲染的内容(见 OssAvoidSuspenseNote.md)。在当前版本中,仍需自行实现。

/**
 * App.react.js
 */

const AppQuery = require('__generated__/AppQuery.graphql');

function App(props: Props) {
  const environment = useRelayEnvironment();
  const [queryRef, loadQuery] = useQueryLoader(
    AppQuery,
    props.appQueryRef /* initial query ref */
  );
  const [isRefreshing, setIsRefreshing] = useState(false)

  const refresh = useCallback(() => {
    if (isRefreshing) { return; }
    const {variables} = props.appQueryRef;
    setIsRefreshing(true);

    // fetchQuery will fetch the query and write
    // the data to the Relay store. This will ensure
    // that when we re-render, the data is already
    // cached and we don't suspend
    fetchQuery(environment, AppQuery, variables)
      .subscribe({
        complete: () => {
          setIsRefreshing(false);

          // *After* the query has been fetched, we call
          // loadQuery again to re-render with a new
          // queryRef.
          // At this point the data for the query should
          // be cached, so we use the 'store-only'
          // fetchPolicy to avoid suspending.
          loadQuery(variables, {fetchPolicy: 'store-only'});
        }
        error: () => {
          setIsRefreshing(false);
        }
      });
  }, [/* ... */]);

  return (
    <React.Suspense fallback="Loading query...">
      <MainContent
        isRefreshing={isRefreshing}
        refresh={refresh}
        queryRef={queryRef}
      />
    </React.Suspense>
  );
}

逐点拆解

  1. 手动 isRefreshing loading state:既然避免挂起,就要自己跟踪刷新状态,用它渲染忙碌 spinner 等加载 UI,而不隐藏 MainContent。
  2. 先 fetchQuery 再 loadQuery 的两段式流程:事件处理器中先调用 fetchQuery,它会发起网络请求并把结果写入本地 Relay Store;当请求 complete 后,再调用 loadQuery 拿到新的 queryRef,传给 usePreloadedQuery 渲染更新数据。isRefreshing 的 error 分支也记得复位,避免状态卡死。
  3. fetchPolicy: 'store-only':此时查询数据已经缓存在 store 中,loadQuery 用 store-only 只会读取已缓存数据,不会再次发起网络请求,从而避免挂起。

源码视角:fetchQuery 为何"只写缓存不挂起"

fetchQuery 的实现位于 packages/relay-runtime/query/fetchQuery.js:它返回一个 RelayObservable,由调用方决定何时 .subscribe();数据到达后通过 environment.lookup(operation.fragment) 读回快照,其副作用是把网络响应写入 store,但不直接驱动 React 渲染,因此不会触发 Suspense。同时注意 fetchQuery 默认 fetchPolicy 就是 'network-only'(const fetchPolicy = options?.fetchPolicy ?? 'network-only';),即"总是走网络、忽略缓存",正好契合刷新语义。源码注释还提醒:fetchQuery 默认不 retain 数据,这正是示例中为什么在刷新完成后还要调用 loadQuery 来重新获得受 retain 的 queryRef 的原因。

方案二:useLazyLoadQuery 路线

如果查询最初是通过 useLazyLoadQuery 在渲染期间懒加载的(见 Lazily Fetching Queries during Render),刷新方式则是更新 state 中的 options,让 useLazyLoadQuery 用新的 fetchKey 与 fetchPolicy 重新渲染并重新获取:

/**
 * App.react.js
 */
const AppQuery = require('__generated__/AppQuery.graphql');

function App(props: Props) {
  const variables = {id: '4'};
  const [refreshedQueryOptions, setRefreshedQueryOptions] = useState(null);

  const refresh = useCallback(() => {
    // Trigger a re-render of useLazyLoadQuery with the same variables,
    // but an updated fetchKey and fetchPolicy.
    // The new fetchKey will ensure that the query is fully
    // re-evaluated and refetched.
    // The fetchPolicy ensures that we always fetch from the network
    // and skip the local data cache.
    setRefreshedQueryOptions(prev => ({
      fetchKey: (prev?.fetchKey ?? 0) + 1,
      fetchPolicy: 'network-only',
    }));
  }, [/* ... */]);

  return (
    <React.Suspense fallback="Loading query...">
      <MainContent
        refresh={refresh}
        queryOptions={refreshedQueryOptions ?? {}}
        variables={variables}
      />
    </React.Suspense>
  );
}
/**
 * MainContent.react.js
 */

// Fetches and renders the query, given the fetch options
function MainContent(props) {
  const {refresh, queryOptions, variables} = props;
  const data = useLazyLoadQuery(
    graphql`
      query AppQuery($id: ID!) {
        user(id: $id) {
          name
          friends {
            count
          }
        }
      }
    `,
    variables,
    queryOptions,
  );

  return (
    <>
      <h1>{data.user?.name}</h1>
      <div>Friends count: {data.user.friends?.count}</div>
      <Button
        onClick={() => refresh()}>
        Fetch latest count
      </Button>
    </>
  );
}

逐点拆解

  1. 事件处理器中更新 state:refresh 回调通过 setRefreshedQueryOptions 设置新的 options,触发 MainContent(使用 useLazyLoadQuery)以新的 fetchKey 和 fetchPolicy 重新渲染并重新获取查询。
  2. 递增 fetchKey:每次刷新都传入新的 fetchKey((prev?.fetchKey ?? 0) + 1),确保查询被完全重新求值并重新获取。
  3. fetchPolicy: 'network-only':强制走网络、跳过本地缓存,与方案一语义一致。
  4. Suspense 行为:由于 network-only 必然产生网络请求,state 更新会让组件挂起(见 Loading States with Suspense),因此同样必须用 <React.Suspense> 包裹 MainContent。

源码视角:fetchKey 与 fetchPolicy 如何驱动重新获取

useLazyLoadQuery 的 options 类型定义(useLazyLoadQuery.js)明确说明了这两个字段的作用:

  • fetchKey:(string | number),传入新的 fetchKey 可以强制对当前查询与变量重新求值——即使变量没变、组件没有重新挂载。这类似于给 React 组件传入不同的 key 触发重挂载:若 fetchKey 与上一次渲染不同,当前查询会重新对照 store 求值,并根据当前 fetchPolicy 与缓存状态决定是否重新请求。
  • fetchPolicy:决定"何时使用缓存数据、何时发起网络请求",四种取值的完整语义如下。

调用时,useLazyLoadQuery 会基于 gqlQuery、variables、networkCacheConfig 构造 operation descriptor(useMemoOperationDescriptor),再连同 fetchKey、fetchPolicy 一起交给内部的 useLazyLoadQueryNode(useLazyLoadQuery.js)。正因为 fetchKey 变化会强制重新求值,"递增 fetchKey" 便成为 useLazyLoadQuery 路线下触发刷新的关键机制。

fetchPolicy 四种取值速查表

下表汇总了 useLazyLoadQuery / loadQuery options 中 fetchPolicy 的四种取值(与文档 Fetch Policies 一致):

fetchPolicy是否复用本地缓存是否发网络请求典型使用场景
store-or-network(默认)是仅当查询数据缺失/过期时才发常规首次加载
store-and-network是总是发送希望先渲染缓存再静默更新
network-only否总是发送手动刷新,强制拉取最新数据
store-only是从不发送刷新数据已入缓存后重新渲染,避免挂起

方案二的进阶:需要避免 Suspense 怎么办

与方案一相同,如果不想用 Suspense fallback 隐藏已渲染内容,同样改用 fetchQuery 先拉取并写入 store,再更新 state 让 useLazyLoadQuery 用 store-only 读取缓存:

/**
 * App.react.js
 */
import type {AppQuery as AppQueryType} from 'AppQuery.graphql';

const AppQuery = require('__generated__/AppQuery.graphql');

function App(props: Props) {
  const variables = {id: '4'}
  const environment = useRelayEnvironment();
  const [refreshedQueryOptions, setRefreshedQueryOptions] = useState(null);
  const [isRefreshing, setIsRefreshing] = useState(false)

  const refresh = useCallback(() => {
    if (isRefreshing) { return; }
    setIsRefreshing(true);

    // fetchQuery will fetch the query and write
    // the data to the Relay store. This will ensure
    // that when we re-render, the data is already
    // cached and we don't suspend
    fetchQuery(environment, AppQuery, variables)
      .subscribe({
        complete: () => {
          setIsRefreshing(false);

          // *After* the query has been fetched, we update
          // our state to re-render with the new fetchKey
          // and fetchPolicy.
          // At this point the data for the query should
          // be cached, so we use the 'store-only'
          // fetchPolicy to avoid suspending.
          setRefreshedQueryOptions(prev => ({
            fetchKey: (prev?.fetchKey ?? 0) + 1,
            fetchPolicy: 'store-only',
          }));
        }
        error: () => {
          setIsRefreshing(false);
        }
      });
  }, [/* ... */]);

  return (
    <React.Suspense fallback="Loading query...">
      <MainContent
        isRefreshing={isRefreshing}
        refresh={refresh}
        queryOptions={refreshedQueryOptions ?? {}}
        variables={variables}
      />
    </React.Suspense>
  );
}

逐点拆解

  1. 手动 isRefreshing loading state:与方案一相同,用自定义 state 呈现忙碌提示,不隐藏 MainContent。
  2. 先 fetchQuery 再更新 state 的两段式流程:先调用 fetchQuery 拉取并写入 store;请求完成后,更新 state 使 useLazyLoadQuery 以新的 fetchKey 与 fetchPolicy 重新渲染。
  3. fetchPolicy: 'store-only':state 更新时查询数据已缓存,store-only 只读缓存、不发请求,从而避免挂起。

两种路线、两种策略的选型总结

维度useQueryLoader / loadQueryuseLazyLoadQuery
触发方式重新调用 loadQuery(variables, options)更新 state 中的 fetchKey 与 fetchPolicy
刷新 fetchPolicynetwork-only(接受挂起)network-only(接受挂起)
避免挂起方案fetchQuery + loadQuery(..., {fetchPolicy: 'store-only'})fetchQuery + 更新 state 为 store-only
依赖需要 useRelayEnvironment 获取 environment(避免挂起方案)需要 useRelayEnvironment 获取 environment(避免挂起方案)
底层机制loadQuery 每次生成新 fetchKey 并 retain 数据(loadQuery.js)useLazyLoadQuery 依据变化的 fetchKey 重新求值(useLazyLoadQuery.js)

选择建议:

  • 如果项目已采用 render-as-you-fetch(usePreloadedQuery + useQueryLoader),优先用方案一,刷新即"再调一次 loadQuery",心智负担最小;
  • 如果项目以 useLazyLoadQuery 为主,用方案二,借助递增 fetchKey 完成刷新;
  • 无论哪条路线,只要刷新期间希望保留已渲染内容,就切换到 fetchQuery + store-only 的组合,并自行管理 isRefreshing 状态;
  • 而所有手动刷新方案的共同前提是:先评估该数据是否适合用 GraphQL Subscriptions 等实时能力自动更新,避免为低价值场景引入不必要的网络请求与复杂度。

需要进一步了解同族场景时,可继续阅读 refreshing-fragments.md(刷新 fragment)与 refetching-queries-with-different-data.md(以不同数据重新渲染查询),三者共同构成 Relay 的完整 refetching 能力矩阵。

【免费下载链接】relay Relay is a JavaScript framework for building data-driven React applications. 【免费下载链接】relay 项目地址: https://gitcode.com/gh_mirrors/relay29/relay

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值