@tanstack/lit-query 快速上手指南:用响应式控制器在 Lit 元素中接入 TanStack Query
2026/9/8 23:27:18 网站建设 项目流程

@tanstack/lit-query 快速上手指南:用响应式控制器在 Lit 元素中接入 TanStack Query

【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query

本指南以 docs/framework/lit/quick-start.md 为骨架,讲解如何在 Lit 自定义元素中,借助@tanstack/lit-query(本仓库packages/lit-query包,即 TanStack Query 官方 Lit 适配层)完成服务端状态的数据查询(Query)、变更提交(Mutation)与缓存失效(Query Invalidation)三件事。读完本文你将能亲手搭建一个可运行的 Lit + Query 最小应用,并理解控制器生命周期、QueryClient上下文传递等底层机制。

三个核心概念:Queries、Mutations 与 Query Invalidation

Lit Query 把 TanStack Query 的使用体验带入 Lit 生态,核心抽象与 React Query 一一对应,只是接入点是 Lit 的ReactiveController而不是 Hooks。快速入门围绕三个概念展开:

  • Queries(查询):声明式地依赖某个异步数据源,并用唯一queryKey绑定它,用于读取服务端状态;
  • Mutations(变更):用于创建、更新、删除等"写"操作与服务端副作用;
  • Query Invalidation(查询失效):写入成功后主动标记相关缓存为过期,从而刷新视图。

完整的可运行示例见仓库中的 examples/lit/basic、examples/lit/pagination 与 examples/lit/ssr。

一个 70 行的完整可运行示例

下面是 quick-start 提供的完整骨架,它同时演示了上述三个概念:读取 todo 列表、新增 todo,并在新增成功后使列表缓存失效从而自动刷新。由于代码量很小,建议把它作为你新项目或现有 Lit 组件的第一个接入模板。

第一步:创建 QueryClient 并派生 Provider 元素

import { LitElement, html } from 'lit' import { QueryClient, QueryClientProvider, createMutationController, createQueryController, } from '@tanstack/lit-query' import { addTodo, getTodos } from './api' const queryClient = new QueryClient() class AppQueryProvider extends QueryClientProvider { constructor() { super() this.client = queryClient } } customElements.define('app-query-provider', AppQueryProvider)

QueryClient是缓存与请求调度的核心实例;QueryClientProvider是 Lit 自定义元素形式的 Provider,负责通过 Lit Context 把该实例传递给后代的控制器。需要特别注意的是:QueryClientProviderclient属性(property)而非 attribute,因此在使用时要用属性绑定.client=${queryClient}。它的源码位于 packages/lit-query/src/QueryClientProvider.ts。

第二步:在 LitElement 中挂载查询与变更控制器

class TodosView extends LitElement { private readonly todos = createQueryController(this, { queryKey: ['todos'], queryFn: getTodos, }) private readonly createTodo = createMutationController(this, { mutationFn: addTodo, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ['todos'] }) }, }) render() { const query = this.todos() const mutation = this.createTodo() 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> <button ?disabled=${mutation.isPending} @click=${() => this.createTodo.mutate({ title: 'Write Lit docs' })} > Add Todo </button> ` } } customElements.define('todos-view', TodosView)

这段代码的关键信息量很大,拆开来看:

  • createQueryController(this, { queryKey, queryFn })返回一个可调用的 accessor(后续会讲);在render()中调用this.todos()即可获得当前QueryObserverResult,并用isPending/isError/data完成分支渲染与 TS 类型收窄;
  • createMutationController(this, { mutationFn, onSuccess })mutationFn负责真正执行写请求;按钮点击时通过this.createTodo.mutate(...)传入变量触发;
  • 一个非常典型的失效模式:onSuccess里调用queryClient.invalidateQueries({ queryKey: ['todos'] }),让"已成功写入 → 列表必然过期"这一事实反映到缓存上;
  • 自定义元素注册(customElements.define)永远是应用层职责,包内不会替你注册。

第三步:把 Provider 包在最外层

<app-query-provider> <todos-view></todos-view> </app-query-provider>

将 Provider 挂在组件外层,后代的查询/变更控制器便能通过 Lit Context 解析到同一个QueryClient

为什么用this?——控制器与宿主生命周期

原文档特别强调:控制器都以this作为第一个参数创建,因为LitElement本身就实现了ReactiveControllerHost接口

查看 packages/lit-query/src/controllers/BaseController.ts 的实现,其内部就是一个标准的ReactiveController:构造函数中调用host.addController(this)(第 39 行),并借助宿主生命周期钩子完成三件事:

  • hostConnected():宿主元素连接到文档时订阅(subscribe),开始接收缓存变更;
  • hostDisconnected():宿主元素断开连接时退订(unsubscribe),避免内存泄漏与无谓的网络活动;
  • 结果变化时通过host.requestUpdate()触发宿主元素重新渲染

源码中还用queueMicrotask推迟了onConnected的调用(第 62 行附近),以确保子类字段初始化完成后生命周期回调再访问子类状态,这保证了在willUpdate这类时机下addController也能安全工作。对使用者而言,你可以放心地把控制器定义为类字段:

private readonly todos = createQueryController(this, { ... })

深入查询:createQueryController 的使用细节

详细指南见 docs/framework/lit/guides/queries.md。一个查询的本质是"绑定唯一queryKey的异步数据声明"。控制器要求三样东西:

  • 一个ReactiveControllerHost,通常是LitElement内部的this
  • 一个唯一的queryKey,用于缓存、共享与失效定位;
  • 一个queryFn,返回 Promise,出错时抛出异常。

通过可调用 accessor 读取结果

控制器创建函数返回的 accessor 既可调用,也带有.current属性读取最新结果:

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

从 packages/lit-query/src/createQueryController.ts 可以看到,返回对象是通过createValueAccessor(() => controller.readCurrent())构造,并额外合入了refetchsuspensedestroy三个方法(第 370-377 行)。

查询状态与抓取状态是两回事

查询结果上同时存在statusfetchStatus两组状态,必须分开理解:

  • status(数据可用性)pending(尚无数据)/error(失败,可读error)/success(有数据);
  • fetchStatus(queryFn 在做什么)fetching(正在抓取)/paused(想抓取但被暂停)/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> ` }

响应式选项:当 queryKey / queryFn 依赖宿主状态时

如果查询选项依赖宿主属性(例如userId),请传入选项 getter 函数,而不是静态对象。Lit Query 会在宿主更新期间重新读取函数型 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, })) }

enabled: false可以阻止无意义请求,配合动态 key 可做到"参数到位才开始加载"。若 options 是静态的,直接传对象即可;注意:如果你自行修改了静态 options 对象,需要调用refetch之类的控制器方法让 observer 感知到新选项,更推荐的做法是对依赖状态变化的场景一律使用函数 getter。

手动重新抓取

accessor 自带refetch

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

多个查询同时并发执行的场景,参见 docs/framework/lit/guides/parallel-queries.md(相关控制器为createQueriesController)。

深入变更:createMutationController 的使用细节

详细指南见 docs/framework/lit/guides/mutations.md。查询负责读、变更负责写。变更控制器可以放在与查询控制器同一个元素中(例如上述 todos 示例)。

变更状态机

一次变更只能处于以下状态之一:

  • isIdle/status === 'idle':从未执行,或已被 reset;
  • isPending/status === 'pending':正在执行;
  • isError/status === 'error':执行失败,error可用;
  • isSuccess/status === 'success':执行完成,data可用。

基于状态渲染 UI 的典型写法:

render() { const query = this.todos() const mutation = this.addTodo() return html` ${mutation.isError ? html`<p>${mutation.error.message}</p>` : null} ${mutation.isSuccess ? html`<p>Todo added</p>` : null} <button ?disabled=${mutation.isPending} @click=${() => this.addTodo.mutate({ title: 'Write mutation docs' })} > ${mutation.isPending ? 'Adding...' : 'Add Todo'} </button> ` }

变量:mutate 与 mutateAsync

mutate的实参即传给mutationFn的 variables:

this.addTodo.mutate({ title: this.nextTitle, })

需要 Promise 时使用mutateAsync(例如拿到返回数据后继续处理):

try { const created = await this.addTodo.mutateAsync({ title: this.nextTitle }) this.nextTitle = created.title } catch (error) { this.errorMessage = String(error) }

一个值得了解的边界行为:如果控制器无法解析到QueryClient(元素不在已连接的QueryClientProvider之下,且未显式传入 client),mutate同步抛出异常,而mutateAsync则表现为返回 rejected promise。

重置变更状态

accessor 提供reset,常用于"清除错误提示":

html` ${mutation.isError ? html`<button @click=${() => this.addTodo.reset()}>Clear error</button>` : null} `

副作用与乐观更新

变更选项支持onMutateonErroronSuccessonSettled四个生命周期回调。指南用 examples/lit/pagination 展示了一个完整闭环:先传显式queryClient给控制器,在onMutate里取消进行中的查询并快照旧数据、就地更新 UI(乐观更新),失败时在onError中根据 context 回滚,最后在onSettled里失效刷新:

private readonly favoriteMutation = createMutationController( this, { mutationKey: ['toggle-project-favorite'], mutationFn: async (input) => { const response = await toggleProjectFavoriteOnServer(input) return response.project }, onMutate: async (variables) => { await queryClient.cancelQueries({ queryKey: ['projects'] }) const snapshots = queryClient.getQueriesData<ProjectsPageResponse>({ queryKey: ['projects'], }) for (const [key, existing] of snapshots) { if (!existing) continue queryClient.setQueryData<ProjectsPageResponse>(key, { ...existing, projects: existing.projects.map((project) => project.id === variables.id ? { ...project, isFavorite: variables.isFavorite } : project, ), }) } return { snapshots } }, onError: (_error, _variables, context) => { for (const [key, snapshot] of context?.snapshots ?? []) { queryClient.setQueryData(key, snapshot) } }, onSettled: async () => { await queryClient.invalidateQueries({ queryKey: ['projects'] }) }, }, queryClient, )

深入失效:invalidateQueries 与手动缓存写入

详细指南见 docs/framework/lit/guides/query-invalidation.md。等待查询自然过期(stale)往往不够——写入成功后,你通常"已经知道"相关缓存过期了,此时应主动失效。

失效的基本语义

queryClient.invalidateQueries() // 全部 queryClient.invalidateQueries({ queryKey: ['todos'] }) // 按 key 匹配

当查询被失效时会发生两件事:

  • 该查询被标记为 stale,覆盖任何staleTime
  • 若匹配到的查询正被某控制器"激活"(active),它会在后台重新抓取。

最常用的场景就是把失效挂在变更成功回调里:

private readonly addTodo = createMutationController(this, { mutationFn: addTodo, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ['todos'] }) }, })

匹配规则:前缀匹配与 exact

默认按key 前缀匹配一组查询。下面两个 key 都会被['projects']命中:

const projectsListKey = ['projects'] const projectsPageKey = ['projects', 1, 250, false]

只想让某一页失效时用更具体的 key:

queryClient.invalidateQueries({ queryKey: ['projects', this.page], })

只精确匹配整棵 key 时加exact: true

queryClient.invalidateQueries({ queryKey: ['projects'], exact: true, })

手动写缓存:setQueryData + 失效

失效通常比规范化缓存更新简单,需要即时 UI 反馈时可组合"定向写缓存 + 失效":

queryClient.setQueryData<TodosResponse>(['todos'], (existing) => { if (!existing) return existing return { ...existing, items: [...existing.items, createdTodo], } }) await queryClient.invalidateQueries({ queryKey: ['todos'] })

乐观更新中用于快照回滚的技术细节见变更指南与 examples/lit/pagination 完整流程。

Provider 的底层行为与 QueryClient 解析规则

结合 packages/lit-query/src/QueryClientProvider.ts 的源码,Provider 组件连接时会发生三件事(connectedCallback/mountClient):

  1. 校验this.client非空,为空则抛createMissingQueryClientError()
  2. 通过@lit/contextContextProvider向所有后代控制器提供该 client(控制器侧由 controllers/BaseController.ts 派发ContextEvent消费);
  3. 调用client.mount()并把它注册进一个process-local 的默认客户端登记表registerDefaultQueryClient),断开时对称地client.unmount()与注销。

因此存在两条 client 解析路径:上下文路径(宿主位于 Provider 之下)与显式传参(构造控制器时第三个参数传入,如上面分页例子的queryClient)。

useQueryClient/resolveQueryClient依赖的是那个 process-local 登记表,规则刻意保守:

  • 没有 Provider 连接时:useQueryClient()抛错;
  • 恰有一个不同 client 连接时:useQueryClient()返回它;
  • 同一 JS 上下文中有多个不同 client 连接时:useQueryClient()resolveQueryClient()抛错,因为兜底会变得有歧义。

因此多根节点、微前端、共享模块的测试套件与嵌套应用应避免依赖全局兜底:要么让宿主控制器渲染在正确的 Provider 下,要么给控制器传显式QueryClient,要么在测试之间干净地断开 Provider。

给 React Query 用户的对照表:Reactive Controllers vs Hooks

详细对照见 docs/framework/lit/guides/reactive-controllers-vs-hooks.md。职责相似:把组件订阅到QueryClient、读取最新结果、缓存变化时更新组件。区别只在于接入点——Lit 用ReactiveControllerHost,React 用渲染/Hook 体系。

React QueryLit Query(@tanstack/lit-query)
useQuery(options)createQueryController(this, options)
useQueries(options)createQueriesController(this, options)
useMutation(options)createMutationController(this, options)
useInfiniteQuery(options)createInfiniteQueryController(this, options)
useIsFetching(options)useIsFetching(this, options)
useIsMutating(options)useIsMutating(this, options)
useMutationState(options)useMutationState(this, options)
Hook 返回的结果对象可调用的结果 accessor
React Context ProviderQueryClientProvider自定义元素
组件重渲染host.requestUpdate()

需要强调的是,所有与宿主绑定的 API 都以host作为第一个参数,并实现上述"连接即订阅、变化即请求更新、断开即退订"的生命周期契约。而useQueryClient是特例:它不是响应式控制器,不接受 host、不订阅,且在没有唯一默认 client 时同步抛错——只应在"恰好只有一个QueryClientProvider连接"的命令式代码中使用,宿主绑定 API 内部请优先走 Provider 上下文或显式传QueryClient

包内所有公开 API 的导出统一收敛在 packages/lit-query/src/index.ts:除上述控制器外,还导出queryOptionsmutationOptionsinfiniteQueryOptions以及dehydrate/hydrate等 SSR 相关能力,并且整体 re-export 了@tanstack/query-core

安装、运行示例与下一步

先完成 docs/framework/lit/installation.md 中的包安装与工程配置(包名@tanstack/lit-query,当前仓库内对应 packages/lit-query/package.json 记录的版本为 0.2.20)。随后任选一个示例目录安装依赖并运行:

  • 基础示例examples/lit/basic:与本文完全对应的查询/变更/失效最小闭环,含本地产物展示页;
  • 分页示例examples/lit/pagination:包含乐观更新、回滚与失配失效的完整业务流;
  • SSR 示例examples/lit/ssr:服务端预取 +dehydrate/hydrate水合,详见 docs/framework/lit/guides/ssr.md。

如果你想对照控制器在元素断开、重新连接、切换 Provider client 等边界情况下的行为,仓库内的测试(如 packages/lit-query/src/tests/base-controller.test.ts、packages/lit-query/src/tests/query-controller.test.ts)也是很好的阅读入口。下一步建议直接进入 Queries 指南 系统学习全部状态与选项,或先阅读 Reactive Controllers vs Hooks 对照指南 建立与 React Query 的概念映射。

【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询