FumadocsZDecode
React

React Query 底层原理与组件更新机制

React Query 把数据层和视图层彻底解耦:数据状态存在 React 之外的缓存里,组件通过 Observer 订阅,再用 useSyncExternalStore 接入渲染。理解这条「缓存 → Observer → Hook」的链路,就能解释它如何做到多组件共享数据、按字段精确更新和请求去重。


一、四层架构

QueryClient (全局单例,持有 cache)
  └── QueryCache (Map<queryHash, Query>,数据真正的容器)
        └── Query (单条数据的状态机:data / status / fetch)
              └── QueryObserver (订阅者,桥接组件)
                    └── useQuery (React Hook)
职责
QueryClient全局唯一,持有 cache,是所有操作的入口
QueryCacheMap<queryHash, Query>,缓存数据的真正容器
Query单个查询的状态机,管理 data / status / fetch与组件无关
QueryObserver关键中间层,一个组件对应一个 observer,订阅 Query 并通知组件

二、为什么需要 Observer 这一层

数据(Query)是全局共享的,组件却是局部的,同一个 queryKey 可能被多个组件使用。Observer 在「共享数据」和「局部组件」之间做桥接:

  1. 多组件共享同一份数据:N 个 observer 订阅同一个 Query,数据只存一份。
  2. 按需通知:每个 observer 持有自己的 selectenabled 配置,只在它关心的字段变化时才触发对应组件更新。

三、通知组件更新:核心是 useSyncExternalStore

React Query 没有用 Context 透传数据,而是 发布订阅 + useSyncExternalStore。简化后的实现:

function useBaseQuery(options: QueryOptions) {
  const client = useQueryClient()

  // 复用同一个 observer,避免每次渲染重建订阅
  const [observer] = React.useState(() => new QueryObserver(client, options))

  return React.useSyncExternalStore(
    React.useCallback(
      (onStoreChange) => observer.subscribe(onStoreChange), // 订阅
      [observer],
    ),
    () => observer.getOptimisticResult(options), // getSnapshot
    () => observer.getOptimisticResult(options), // getServerSnapshot
  )
}

完整通知链路:


四、关键优化:不是所有变化都通知

Observer 内部用三层手段过滤无效渲染:

  1. select 选择器:组件只订阅 select 返回值的变化,原始 data 其它部分变了也不触发。
  2. notifyOnChangeProps: 'tracked'(默认):通过 getter 代理追踪组件实际读取了哪些字段,只有用到的字段变化才 re-render。
function TodoCount() {
  // 只读了 data,没读 isFetching
  const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })

  // 因此仅当 data 变化才重渲染,isFetching 翻转不会触发
  return <span>{data?.length ?? 0}</span>
}
  1. 结构共享(structural sharing):新旧数据深度对比,引用未变的部分复用旧引用,让 === 比较生效,配合 useMemo / select 能进一步减少下游渲染。

五、补充

为什么用 useSyncExternalStore 而不是 useState QueryCache 的数据变更发生在 React 之外。useSyncExternalStore 是 React 18 为外部数据源提供的官方订阅 API,能正确处理并发模式下的 tearing(撕裂),保证一次渲染内读到的快照一致。v4 之前用的是手写 forceUpdate,并发模式下不安全。

缓存数据为什么不放 Context? Context 中任意值变化都会让所有消费者 re-render,无法做细粒度控制,也不适合频繁变化的数据。外部 store + observer 才能做到精确的、按字段的更新。

多个组件用同一个 key,fetch 发几次? 一次。Query 是共享的,多个 observer 订阅同一个 Query,queryFn 在去重窗口内只执行一次(请求去重 / dedupe)。


一句话记忆:React Query 把数据放在 React 之外的 QueryCache,用 Observer 做发布订阅、用 useSyncExternalStore 接入渲染,再靠 tracked 字段追踪和结构共享实现按需、精确的组件更新——既共享数据又避免无效渲染。

On this page