首页
/ Preact Query `UseSuspenseQueryResult` 类型解析:读懂 `useSuspenseQuery` 返回值为何永远有 data

Preact Query `UseSuspenseQueryResult` 类型解析:读懂 `useSuspenseQuery` 返回值为何永远有 data

2026-09-08 11:24:10作者:蔡丛锟

UseSuspenseQueryResult<TData, TError>@tanstack/preact-queryuseSuspenseQuery 钩子的返回类型定义,它建立在对 @tanstack/query-core 内建结果类型的一层"减法"之上:剔除 isPlaceholderData 死字段,从而在类型层面保证 data 永不为 undefined。本文以 类型别名参考文档 为骨架,结合本仓库中 preact-queryquery-core 的源码,讲清该类型的构造原理、与相邻 Result 类型的差异、泛型参数含义,以及它在 Suspense 渲染流中的实际用法与约束。

类型定义一览:一行别名背后的含义

该类型别名在源码 packages/preact-query/src/types.ts:336 中定义如下:

export type UseSuspenseQueryResult<
  TData = unknown,
  TError = DefaultError,
> = DistributiveOmit<
  DefinedQueryObserverResult<TData, TError>,
  'isPlaceholderData'
>

官方描述把它概括为:The result of useSuspenseQuery. Same as DefinedUseQueryResult, minus isPlaceholderData — always false on that type, so this drops the dead field rather than an active state.(即"useSuspenseQuery 的返回值。与 DefinedUseQueryResult 相同,仅去掉 isPlaceholderData——该字段在该类型上恒为 false,所以这里剔除的是一个不可能为真的死字段,而不是删除了某个真实状态。")

一句话导读式总结:这是一个"定义态(Defined)"查询结果类型,表示组件拿到返回值时数据已经就绪;类型系统不允许出现 dataundefined 的中间态。

底层骨架:DefinedQueryObserverResultDistributiveOmit

要理解这行别名,需要拆解两个在 query-core 中定义的基础构件。

DefinedQueryObserverResult:只保留"有数据"的两个状态

DefinedQueryObserverResult 定义在 packages/query-core/src/types.ts:890

export type DefinedQueryObserverResult<
  TData = unknown,
  TError = DefaultError,
> =
  | QueryObserverRefetchErrorResult<TData, TError>
  | QueryObserverSuccessResult<TData, TError>

而完整的 QueryObserverResultquery-core/src/types.ts:897)是五个分支的联合:

export type QueryObserverResult<TData = unknown, TError = DefaultError> =
  | DefinedQueryObserverResult<TData, TError>
  | QueryObserverLoadingErrorResult<TData, TError>
  | QueryObserverLoadingResult<TData, TError>
  | QueryObserverPendingResult<TData, TError>
  | QueryObserverPlaceholderResult<TData, TError>

对比可见:DefinedQueryObserverResult 排除了 pending(首次加载)、loading error(首次失败)、loadingplaceholder 四个分支,只留下成功与**后台刷新失败(refetch error)**两种情况。这两个分支的共同点是 data 的类型为确定的 TData 而非 TData | undefined

DistributiveOmit:对联合类型逐个剔除字段

普通的 Omit 无法正确处理联合类型(只会取 keyof 交集),因此 query-core 在 packages/query-core/src/types.ts:14 提供了分发版本的实现:

export type DistributiveOmit<
  TObject,
  TKey extends keyof TObject,
> = TObject extends any ? Omit<TObject, TKey> : never

它借助 TObject extends any 让条件类型在联合的每个成员上分别展开(distribute),再逐个 Omit。因此 UseSuspenseQueryResult 实际等于下面两个接口各删掉 isPlaceholderData 后的联合:

分支 data error status isSuccess isError
QueryObserverRefetchErrorResult TData(保留缓存旧数据) TError 'error' false true
QueryObserverSuccessResult TData null 'success' true false

为什么删除 isPlaceholderData 是"删死字段"而不是删状态

查看两个被保留分支的原始定义会发现,它们都把 isPlaceholderData 固定写死为字面量 false

真正可能出现 isPlaceholderData: true 的是 QueryObserverPlaceholderResultquery-core/src/types.ts:874),而它只会在渲染 placeholderData 时出现。Suspense 钩子天然无法渲染"占位数据"态,源码层面有两重保证:

  1. useSuspenseQuery 在调用底层 useBaseQuery强制placeholderData 置为 undefined(见 packages/preact-query/src/useSuspenseQuery.ts:110-117):
    return useBaseQuery(
      {
        ...options,
        enabled: true,
        suspense: true,
        throwOnError: defaultThrowOnError,
        placeholderData: undefined,
      },
      QueryObserver,
      queryClient,
    ) as UseSuspenseQueryResult<TData, TError>
    
  2. 该文档对应的 Preact 适配层设计哲学是:使用哪个钩子就推导出哪种模式(suspense 与否),而非像旧版那样通过 suspense: true 选项手动开启。相关注释见 packages/preact-query/src/types.ts:155-156

既然这个字段在任何可达分支上都恒为 false,保留它对消费者而言既无信息量又可能诱导无意义的分支判断(例如 if (result.isPlaceholderData) 永假),因此类型设计上直接将其从结果对象中剔除——这正是该类型别名与 DefinedUseQueryResult 之间唯一的差异。

类型参数:TDataTError

参数 默认值 含义 来源
TData unknown data 经过 select 变换之后的最终类型。当 useSuspenseQuery 未传 select 时,它就是 queryFn 返回的数据类型;传了 select 则窄化为其返回值类型 类型注释见 types.ts:329-342
TError DefaultError 你的 queryFn 可能抛出的错误类型 同上

其中 DefaultError 默认就是全局 Error。若要全局自定义(例如把默认错误统一为自定义 ApiError),可通过 query-core 提供的类型扩展点 Register 接口(声明于 packages/query-core/src/types.ts:37)做模块增强:为 Register 提供 defaultError 字段即可全局改写 TError 的默认值,这与 @tanstack/preact-query 的结果类型保持一致。

同时注意,useSuspenseQuery选项类型 UseSuspenseQueryOptions 不接受 enabledthrowOnErrorplaceholderDataskipToken(见 types.ts:195-212)——Suspense 钩子不能渲染"禁用态"或"占位态",所以这些会让 data 悬空的开关全部从类型层面封死。若在开发环境传入 skipToken 作为 queryFn,还会收到控制台错误提示(useSuspenseQuery.ts:104-108)。

与相邻 Result 类型的对比:一张表理清家族谱系

Preact Query 在同一个文件中定义了多组结果类型,它们之间的区别完全由 data 的可空性与 isPlaceholderData 字段决定:

类型 由哪个钩子返回 data 可空? isPlaceholderData 源码位置
UseBaseQueryResult / UseQueryResult useQuery(无 initialData 是(TData | undefined types.ts:313types.ts:324
DefinedUseQueryResult useQuery(设了 initialData types.ts:352
UseSuspenseQueryResult useSuspenseQuery 否(已剔除) types.ts:336
DefinedUseInfiniteQueryResult useInfiniteQuery(设了 initialData 否(无限数据形态) types.ts:376
UseSuspenseInfiniteQueryResult useSuspenseInfiniteQuery 否(已剔除) types.ts:388

其中 DefinedUseQueryResult 仅是 DefinedQueryObserverResult 的再导出(docs/framework/preact/reference/type-aliases/DefinedUseQueryResult.md);UseSuspenseInfiniteQueryResult 则用 OmitKeyof 完成同样的去 isPlaceholderData 操作(docs/framework/preact/reference/type-aliases/UseSuspenseInfiniteQueryResult.md)。Suspense 家族与 initialData 家族在"数据必存在"这一点上等价,差别只在是否剔除了那个恒假字段。

返回值上的类型承诺:消费者可以信赖什么

最终 useSuspenseQuery 的声明签名(useSuspenseQuery.ts:95-103)返回 UseSuspenseQueryResult<TData, TError>。结合上文可归纳出它对调用方的四项类型承诺:

  1. data 一定存在:联合的每个分支中 data 都是确定的 TData,无需 isPendingdata === undefined 之类守卫即可直接渲染。
  2. status 只会是 'success''error':失败且无缓存数据时,错误会被 throwOnError 抛给上层的错误边界处理(见下文),组件内不会出现 status === 'pending' 的手写兜底。
  3. 没有 isPlaceholderData 属性:尝试访问 result.isPlaceholderData 在 TS 中直接报错——设计者希望你不要写永远不会执行的分支。
  4. 错误只在"首次失败且无缓存"时抛出:后台刷新失败会继续携带旧 datarefetch error 分支返回,这是结果联合里保留 QueryObserverRefetchErrorResult 的原因。

Suspense 钩子如何兑现这些承诺:throwOnError 与错误边界

"首次失败抛出、有缓存则返回错误态"的语义来自 defaultThrowOnError,定义于 packages/preact-query/src/suspense.ts:12

export const defaultThrowOnError = <
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
>(
  _error: TError,
  query: Query<TQueryFnData, TError, TData, TQueryKey>,
) => query.state.data === undefined

即:只有当缓存中没有任何数据(query.state.data === undefined)时才把错误抛出,让外层 <Suspense>/错误边界接管;一旦已有缓存数据,就允许以 error + 旧 data 的状态正常渲染。这正是 UseSuspenseQueryResult 联合类型中两个错误分支(isError: trueisRefetchError: true)能够安全携带 TData 类型 data 的运行时依据。

实战用法与注意事项

参考 useSuspenseQuery 自带的使用示例(useSuspenseQuery.ts:24-93),组件内部可直接解构并使用 data,无需判空:

import { Suspense } from 'preact/compat'
import { useErrorBoundary } from 'preact/hooks'
import { QueryErrorResetBoundary, useSuspenseQuery } from '@tanstack/preact-query'
import type { ComponentChildren } from 'preact'

function Posts() {
  // `data` 在这里保证有值——无需 `isPending` 判断。
  const { data, isFetching } = useSuspenseQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  })

  return (
    <div>
      <h1>Posts {isFetching ? '(refreshing...)' : null}</h1>
      <ul>
        {data.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  )
}

function App() {
  return (
    <QueryErrorResetBoundary>
      {({ reset }) => (
        <ErrorBoundary
          onReset={reset}
          fallbackRender={({ resetErrorBoundary }) => (
            <div>
              There was an error!
              <button onClick={() => resetErrorBoundary()}>Try again</button>
            </div>
          )}
        >
          <Suspense fallback={<h1>Loading posts...</h1>}>
            <Posts />
          </Suspense>
        </ErrorBoundary>
      )}
    </QueryErrorResetBoundary>
  )
}

实践要点提醒:

  • 必须提供错误边界:首次请求失败会以 throw 的形式冒泡到外层,useSuspenseQuery 自身的类型和文档都假设外层有 QueryErrorResetBoundary + 错误边界兜底,否则应用会直接崩溃。
  • 单个组件内多个 useSuspenseQuery 会串行挂起,形成请求瀑布(waterfall)——每个查询都会阻塞渲染直到完成,下一个才会开始请求。官方在 useSuspenseQuery.ts:14-17 明确建议多个 suspenseful 查询改用 useSuspenseQueries 并行发起。此结果类型对应的错误处理与 Suspend 判定逻辑可进一步阅读 packages/preact-query/src/suspense.ts
  • 取消(cancellation)在 Suspense 模式下不生效:这也是官方在 useSuspenseQuery 文档注释中明确标注的 caveat。
  • refetch 返回的是 Promise<QueryObserverResult<TData, TError>>,与渲染用的结果联合类型不同,后者不包含 pending 等分支(基础字段定义见 query-core/src/types.ts:667-793),使用时可放心 await。

小结

UseSuspenseQueryResult<TData, TError> 是 Preact Query Suspense 模式下"类型驱动正确性"的典型设计:通过 DistributiveOmitDefinedQueryObserverResult 的成功 / 后台刷新失败两个成员上精确剔除恒假的 isPlaceholderData,既保留了 data: TData 的非空保证,又消灭了永不成立的分支判断,从类型层面对齐了 useSuspenseQuery 强制 enabled: true、无 placeholderData、失败抛错的运行时语义。理解这一别名,也就理解了为何在 Preact + TanStack Query 中接入 Suspense 后,渲染代码可以如此干净——因为它把"数据一定就绪"这一事实直接编码进了 TypeScript 类型系统。

若需继续对照查看相邻定义,可访问 UseQueryResult(data 可空版本)DefinedUseQueryResult(initialData 定义态),以及类型别名源文件 packages/preact-query/src/types.ts

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.74 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.63 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
518
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389