首页
/ TanStack Query Lit 指南:掌握 createQueryController 与服务端数据查询(Query Basics、状态机与响应式刷新)

TanStack Query Lit 指南:掌握 createQueryController 与服务端数据查询(Query Basics、状态机与响应式刷新)

2026-09-07 18:34:40作者:邬祺芯Juliet

本文围绕 docs/framework/lit/guides/queries.md 展开,系统讲解在 Lit 框架中如何使用 @tanstack/lit-querycreateQueryController 控制器订阅单个查询:从最小可运行的 queryKey + queryFn 用法,到 status/fetchStatus 双状态机的准确区分、依赖宿主状态的响应式 options,以及命令式 refetch。读完你可以把任意 Lit 自定义元素与 TanStack Query 缓存无缝对接,并理解其背后的 Lit Reactive Controller 生命周期实现。

阅读前提

Queries 指南假设你已经完成了基本接线。若从未使用过 Lit Query,请先依次阅读 InstallationQuick Start,了解 QueryClientProvider 的挂载方式和运行环境要求,再回到本文把查询控制器接入元素。

Query 基础:把异步数据源声明式地接入元素

一个 query(查询)是对某个异步数据源声明式依赖,它通过一个唯一的 key 与数据源绑定。在 Lit 中,query 天生用于读取服务端状态;如果一个函数会创建、更新或删除服务端数据,应当改用 mutation,而不是 query。

在 Lit 中订阅查询,使用 createQueryController 创建控制器并传入宿主 this

import { LitElement, html } from 'lit'
import { createQueryController } from '@tanstack/lit-query'

async function fetchTodos(): Promise<{ id: number; title: string }[]> {
  const res = await fetch('/api/todos')
  if (!res.ok) throw new Error('Failed to fetch todos')
  return res.json()
}

class TodosView extends LitElement {
  private readonly todos = createQueryController(this, {
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })

  render() {
    const query = this.todos()

    if (query.isPending) return html`Loading...`
    if (query.isError) return html`Error: ${query.error.message}`

    return html`
      <ul>
        ${query.data.map((todo) => html`<li>${todo.title}</li>`)}
      </ul>
    `
  }
}

customElements.define('todos-view', TodosView)

控制器创建时需要三样东西:

必需项 说明
ReactiveControllerHost 通常是元素内部的 this(一个 LitElement),控制器借此接入 Lit 生命周期并触发重渲染
唯一的 queryKey 用于缓存、失效、去重与跨控制器共享数据(详见 Query Keys
queryFn 返回 Promise 的取数函数;出错时抛异常即可,queryFn 规范见 Query Functions

从源码看 createQueryController 的函数签名

参考文档 createQueryController 给出了完整签名:

function createQueryController<TQueryFnData, TError, TData, TQueryData, TQueryKey>(
  host,
  options,
  queryClient?,
): QueryResultAccessor<TData, TError>;

其中各泛型与参数(实现位于 packages/lit-query/src/createQueryController.ts):

  • host:拥有该查询订阅的 Lit 响应式控制器宿主。
  • options:类型为 Accessor<CreateQueryOptions<...>>,既可以直接传对象,也可以传一个返回 options 的函数(函数形式会在宿主更新期间被重新求值,使 query key 与配置跟随响应式宿主状态)。
  • queryClient(可选):显式传入的 QueryClient。省略时,控制器会从最近的已连接 QueryClientProvider 解析客户端;QueryClientProvider 通过 context.ts 中定义的 queryClientContext 在 DOM 树中共享客户端。
  • 返回 QueryResultAccessor<TData, TError>:对最新查询结果的可调用访问器,同时附带 refetchsuspensedestroy 方法(见下方实现摘录的返回对象 createQueryController.ts#L370-L377)。

读取查询结果:调用访问器,或读取 .current

控制器返回的 accessor 暴露的是当前 QueryObserverResult。在 render 中直接调用它即可取到最新结果;current 属性是等价读取方式:

const query = this.todos()
const sameQuery = this.todos.current

两者为何等价?从 accessor.ts 可以看到 ValueAccessor<T> 的类型定义:它是 (() => T) & { readonly current: T } 的交叉类型。createValueAccessor 用一个内部 getter 构造可调用函数,并用 Object.definePropertycurrent 绑定为同一个 getter。每次调用 this.todos() 时,底层会走到 createQueryController.ts#L201-L207readCurrent():若已存在 observer,先以 observer.getCurrentResult() 同步一次结果再返回,保证读到的总是最新快照。

Query 状态:isPending / isError / isSuccess 与 status

一个 query 在同一时刻只会处于以下三种主状态之一:

  • isPending(或 status === 'pending'):尚无可用的数据;
  • isError(或 status === 'error'):查询失败,此时 error 可用;
  • isSuccess(或 status === 'success'):已有数据可用。

结果上还带有 isFetching,它在初次加载或后台重新拉取(background refetch)期间都可能为 true。官方指南推荐优先使用 status 做分支判断:

render() {
  const query = this.todos()

  if (query.status === 'pending') {
    return html`<span>Loading...</span>`
  }

  if (query.status === 'error') {
    return html`<span>Error: ${query.error.message}</span>`
  }

  return html`<todo-list .items=${query.data}></todo-list>`
}

TypeScript 会在你先检查完 pendingerror 之后再读取 query.data,从而把 data 的类型收窄为"确定有值"。这是把 UI 分支写成 if (status === 'pending') / if (status === 'error') 顺序结构的最大收益:既保证渲染安全,又避免大量非空断言。

补充一点可验证性:即便控制器尚未拿到真正的 QueryClient,它也会先用 createPendingQueryResult 生成一个初始占位结果——其中 status: 'pending'data: undefinederror: nullfetchStatus: 'idle',所有布尔标记均按"尚未请求"的状态预设好,因此元素首帧渲染也符合上述状态机。

Fetch Status:把"有没有数据"与"是否在请求"分开

status 描述的是"有没有可用数据",fetchStatus 描述的是 queryFn 此刻正在做什么

  • fetchStatus === 'fetching':查询正在拉取数据;
  • fetchStatus === 'paused':查询想拉取,但被暂停了(例如当前处于离线状态,涉及 onlineManager 的网络监听);
  • fetchStatus === 'idle':查询当前没有在拉取。

这两个维度刻意相互独立。由于后台重拉取与 stale-while-revalidate(数据过期时先用缓存渲染、后台再刷新)机制,二者会组合出一些看起来"矛盾"但实际合理的状态:

  • 已有缓存数据的成功查询,在后台重拉取进行时,表现为 status === 'success'fetchStatus === 'fetching'
  • 还没有数据、且拉取暂时无法开始的查询,表现为 status === 'pending'fetchStatus === 'paused'

因此选判断依据的标准是:决定能否渲染数据时看 status决定是否展示网络活动指示器时看 fetchStatus / isFetching

render() {
  const query = this.todos()

  if (query.isPending) return html`Loading...`
  if (query.isError) return html`Error: ${query.error.message}`

  return html`
    ${query.fetchStatus === 'fetching'
      ? html`<span>Refreshing...</span>`
      : null}
    <todo-list .items=${query.data}></todo-list>
  `
}

这样数据首次加载显示全屏 "Loading...",而后续后台刷新只显示轻量的 "Refreshing..." 提示,不影响已渲染列表。

Reactive Query Options:让 key 与 queryFn 跟随宿主状态

当 query key 或 query function 依赖宿主(元素实例)的响应式状态时,createQueryController 的 options 参数应传一个 getter(返回 options 的函数)

class UserTodos extends LitElement {
  static properties = {
    userId: { type: String },
  }

  userId = ''

  private readonly todos = createQueryController(this, () => ({
    queryKey: ['todos', this.userId],
    queryFn: () => fetchTodos(this.userId),
    enabled: this.userId.length > 0,
  }))
}

其中的要点:

  • queryKey 中带有 this.userId,缓存与失效都以它为准:不同用户各自拥有独立缓存条目,宿主状态一变,key 变化会触发符合旧 key 的查询被取消/清理、并按新 key 拉取。
  • enabled: this.userId.length > 0 控制查询开关:userId 为空串时不发起请求。测试用例 M6: does not fetch when enabled=false and fetches after enabling(见 query-controller.test.ts)验证了这一行为。
  • getter 之所以是响应式的,是因为 options 的函数形式会在宿主更新期间被重新读取。对照 BaseController.ts:Lit 每次 hostUpdate() 都会触发 onHostUpdate(),随后 QueryController 通过 refreshOptions() 重新执行 getter、应用新 options(createQueryController.ts#L153-L159)。底层的求值入口就是 accessor.ts#L15-L17readAccessor——函数则调用、非函数则原样返回。

query key 本身还承担缓存、重新拉取与控制器之间数据共享的职责:只要两个控制器使用相同的 key,它们共享同一条缓存与同一个后台请求,重复请求会被去重合并。

Refetching:命令式重新拉取

accessor 上直接带有 refetch 方法,可在事件处理器中调用:

html`<button @click=${() => this.todos.refetch()}>Refetch</button>`

调用后该查询会立即重新拉取并刷新响应式结果,UI 依据 fetchStatus === 'fetching' 显示刷新中状态。从实现上看,accessor 的 refetch 是对内部 QueryController.refetch 的委托(createQueryController.ts#L178-L184),它先确保 options 已应用到 observer,再转发给 observer.refetch;若此时尚无可用 QueryClient,则按 createMissingQueryClientError 的约定以 rejected Promise 结束。参考文档中的 suspense 方法也值得一提:当你想以 Promise 形式拿到一个乐观查询结果(必要时先 fetch)时,可用 await this.todos.suspense()

如需同时管理多个并行执行的查询,或需要"多查询同启同停"的编排,参考 Parallel Queries

生命周期与缓存协作:控制器背后的实现纵深

Lit Query 的查询控制器本质是一个实现 ReactiveController 接口的宿主控制器,生命周期映射定义在 BaseController.ts

  • hostConnected()L42):宿主元素接入 DOM。若未显式传 QueryClient,控制器会派发 ContextEvent(queryClientContext, ...) 向 DOM 树上层请求客户端(由 QueryClientProvider.ts 响应);随后在微任务中建立 QueryObserversubscribe
  • hostUpdate():宿主每次更新都让 options getter 重新求值并同步结果,实现上文"响应式 options"。
  • hostDisconnected():宿主移出 DOM 时取消订阅;若依赖 context 客户端,会同时解除 context 绑定,重新挂载后再走一次解析流程。
  • 结果更新时,setResult 通过 queueMicrotask 合并调用 host.requestUpdate(),保证一次事件风暴只触发一次重渲染。

QueryController 内部,所有查询行为最终都收敛到 @tanstack/query-coreQueryObserver 上(createQueryController.ts#L128-L133),这也解释了为什么本文描述的 status/fetchStatus 语义与所有 TanStack Query 框架保持一致。仓库测试 query-controller.test.ts 进一步印证了以下能力:失效触发重拉取(QSEM-03: invalidation triggers refetch and updates result state)、key 切换时旧请求通过 AbortSignal 取消(CANCEL-01: queryFn receives AbortSignal and prior request is aborted on key switch)、key 过渡期保留旧数据(S5: keepPreviousData preserves prior data during key transitions),以及跨 Provider 切换时重建单个活跃 observer(M3)。

下一步

  • 数据读取只是第一步:写操作请阅读 mutations 指南
  • 需要列表分页/无限滚动时,查阅 infinite-queriescreateInfiniteQueryController 的用法;
  • 想调整取数时机(如 focus 重新拉取、失败重试),见 query-functionsquery-keys
  • 若需在服务端预取数据再水合客户端缓存,参考 SSR 指南
  • 仓库中的 basic 示例 提供了可运行的最小工程,适合对照本文逐步调试。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

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