TanStack Query Preact 快速上手:Queries、Mutations 与 Query Invalidation 三大核心概念实战
Preact 作为轻量级 React 替代方案,同样需要一套健壮的异步状态管理工具来处理服务端数据获取、缓存与同步。本指南基于 @tanstack/preact-query(本仓库 packages/preact-query 中的核心包)讲解其快速上手路径:从安装、创建 QueryClient 并注入 Provider,到掌握查询(Queries)、变更(Mutations)与查询失效(Query Invalidation)这三大核心概念。读完本指南,你将能够在 Preact 应用中完成「拉取数据 → 渲染 loading/error/success 三态 → 提交变更 → 自动失效并刷新数据」的完整闭环,并理解底层缓存与订阅机制。
本文内容的主体来自 docs/framework/preact/quick-start.md(该文档由生成流程从 docs/framework/react/quick-start.md 通过替换包名与框架名转写而来,参见 scripts/generate-docs.ts 中的文档生成机制),并辅以仓库源码与真实示例进行纵深验证。
安装与准备
通过包管理器安装
使用你习惯的包管理器安装 @tanstack/preact-query:
npm i @tanstack/preact-query
或使用 pnpm / yarn / bun / deno:
pnpm add @tanstack/preact-query
yarn add @tanstack/preact-query
bun add @tanstack/preact-query
deno add @tanstack/preact-query
根据 packages/preact-query/package.json 中的依赖声明,@tanstack/preact-query 的核心依赖是 @tanstack/query-core(workspace 内同版本维护),而 preact 以 ^10.0.0 作为 peerDependency 声明,即 Preact 10 及以上版本均可使用;当前仓库示例使用 preact ^10.28.0(见 examples/preact/simple/package.json)。
通过 CDN 引入
如果不想使用模块打包器或包管理器,可以直接通过 ESM 兼容 CDN(如 ESM.sh)在 HTML 中以 <script type="module"> 方式引入,参考 docs/framework/preact/installation.md:
<script type="module">
import { render } from 'https://esm.sh/preact@10.23.1'
import { QueryClient } from 'https://esm.sh/@tanstack/preact-query'
</script>
环境要求
@tanstack/preact-query 面向现代浏览器优化。官方 React 版本的环境要求(Chrome >= 91、Firefox >= 90、Edge >= 91、Safari >= 15、iOS >= 15、Opera >= 77,见 docs/framework/react/installation.md)同样适用于 Preact 版本;如果目标环境较旧,可能需要补充 polyfill,并对 node_modules 中的库自行转译。仓库中还建议搭配 @tanstack/eslint-plugin-query(文档见 docs/eslint/eslint-plugin-query.md)在编码阶段提前发现错误与不一致用法。
创建 QueryClient 并通过 Provider 注入
TanStack Query 的所有缓存与订阅能力都围绕一个 QueryClient 实例展开。第一步是创建它,并通过 QueryClientProvider 注入到组件树顶层:
import {
QueryClient,
QueryClientProvider,
} from '@tanstack/preact-query'
// Create a client
const queryClient = new QueryClient()
function App() {
return (
// Provide the client to your App
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
)
}
从源码角度看,QueryClientProvider 的实现(packages/preact-query/src/QueryClientProvider.tsx)做了两件事:
- 通过 Preact Context 下发 client:
QueryClientProvider内部使用createContext创建QueryClientContext,并用<QueryClientContext.Provider value={client}>包裹 children,使整个子树都能通过useQueryClient读取到该实例; - 绑定客户端生命周期:组件挂载时调用
client.mount()、卸载时调用client.unmount()。mount()会让 client 订阅 focus/online 事件——当应用重新获得焦点或恢复联网时,自动恢复被暂停的变更并触发必要的重新获取。
useQueryClient 从最近的 Context 中读取 client;如果组件树中没有 QueryClientProvider,它会抛出 'No QueryClient set, use QueryClientProvider to set one' 的错误(见 packages/preact-query/src/QueryClientProvider.tsx),因此请务必保证 Provider 包裹在所有使用 hook 的组件之上。
值得一提的还有包的导出结构:@tanstack/preact-query 在 packages/preact-query/src/index.ts 中直接 export * from '@tanstack/query-core',因此 QueryClient、QueryCache、MutationCache 等核心类都从同一个包导入即可,无需额外安装依赖。
三大核心概念总览
@tanstack/preact-query 的核心功能由以下三个概念构成(它们也是 docs/framework/preact/quick-start.md 一文的骨架):
- Queries(查询):声明式地依赖某个异步数据源,用唯一 key 缓存、共享数据;
- Mutations(变更):用于创建/更新/删除数据或执行服务端副作用;
- Query Invalidation(查询失效):当数据确知过期时,智能地标记查询失效并触发后台重新获取。
下面依次深入这三个概念。
概念一:查询(Queries)
查询的本质与最小用法
一个查询是对某个异步数据源的声明式依赖,它与一个唯一 key 绑定。任何基于 Promise 的方法(包括 GET 和 POST)都可以用于从服务端获取数据;但请注意:如果某个方法会修改服务端数据,官方推荐改用 Mutations 而不是 Query。
要订阅一个查询,在组件或自定义 hook 中调用 useQuery,至少传入两个参数:
- 查询的唯一 key(
queryKey); - 返回 Promise 的函数(
queryFn),该 Promise 要么 resolve 出数据,要么 reject 抛出错误。
import { useQuery } from '@tanstack/preact-query'
function App() {
const info = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList })
}
这个唯一 key 在内部被用于重新获取、缓存以及在应用各处共享查询结果。key 相同(或前缀匹配)的多个 useQuery 调用会共享同一份缓存数据。
在源码层面,useQuery 的所有重载最终都收敛到同一个实现:useBaseQuery(options, QueryObserver, queryClient)(见 packages/preact-query/src/useQuery.ts),QueryObserver 来自 @tanstack/query-core。也就是说,Preact 版本与 React 版本共用同一套核心观察者机制,只是适配层(订阅方式、context 提供)不同。
查询状态(status)
useQuery 返回的 result 对象包含模板渲染所需的全部信息。任意时刻,一个查询只能处于以下三种状态之一:
| 状态 | 说明 |
|---|---|
isPending 或 status === 'pending' |
查询还没有数据 |
isError 或 status === 'error' |
查询遇到了错误 |
isSuccess 或 status === 'success' |
查询成功且数据可用 |
除此之外,根据查询所处的状态还有更多可用信息:
error:查询处于isError状态时,可通过error属性拿到错误对象;data:查询处于isSuccess状态时,可通过data属性拿到数据;isFetching:任意状态下,只要查询正在获取数据(包括后台重新获取),isFetching即为true。
对于大多数查询,标准的渲染模式是:先检查 isPending,再检查 isError,最后默认数据可用并渲染成功态:
function Todos() {
const { isPending, isError, data, error } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList,
})
if (isPending) {
return <span>Loading...</span>
}
if (isError) {
return <span>Error: {error.message}</span>
}
// We can assume by this point that `isSuccess === true`
return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
如果你不喜欢布尔值,也可以直接使用 status 字段:
function Todos() {
const { status, data, error } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList,
})
if (status === 'pending') {
return <span>Loading...</span>
}
if (status === 'error') {
return <span>Error: {error.message}</span>
}
// also status === 'success', but "else" logic works, too
return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
TypeScript 会在你依次检查了 pending 与 error 之后自动收窄 data 的类型,无需额外的非空断言。
从 useQuery 的类型定义(packages/preact-query/src/useQuery.ts)还可以看到一个细节:如果传入了 initialData,会命中专门的重载,返回值类型变为 DefinedUseQueryResult,此时 data 在类型上永远不会是 undefined——即使后台重新获取失败,已有数据也会保留展示。
fetchStatus 与两种状态的区分
除了 status,查询结果还提供 fetchStatus 属性,取值为:
fetchStatus === 'fetching':查询当前正在获取数据;fetchStatus === 'paused':查询想要获取数据但被暂停了(更多见网络模式相关指南);fetchStatus === 'idle':查询当前什么都没做。
为什么需要两套状态?
后台重新获取与 stale-while-revalidate(过期数据先展示、后台刷新)逻辑使得 status 与 fetchStatus 的所有组合都可能出现。例如:
- 一个
success状态的查询通常处于idle的 fetchStatus,但如果正在后台重新获取,则可能处于fetching; - 一个刚挂载、还没有数据的查询通常处于
pending状态 +fetchingfetchStatus,但如果没有网络连接,也可能变成paused。
因此请记住:查询可以处于 pending 状态但并没有真正在获取数据。作为经验法则:
status反映的是data的情况:我们有没有数据?fetchStatus反映的是queryFn的情况:它是否正在运行?
概念二:变更(Mutations)
与查询不同,变更(Mutation)通常用于创建/更新/删除数据或执行服务端副作用。为此,TanStack Query 导出了 useMutation hook(Preact 版本位于 packages/preact-query/src/useMutation.ts,内部基于 query-core 的 MutationObserver 实现)。
useMutation 最小用法
下面是一个向服务端新增 todo 的变更示例:
function App() {
const mutation = useMutation({
mutationFn: (newTodo) => {
return axios.post('/todos', newTodo)
},
})
return (
<div>
{mutation.isPending ? (
'Adding todo...'
) : (
<>
{mutation.isError ? (
<div>An error occurred: {mutation.error.message}</div>
) : null}
{mutation.isSuccess ? <div>Todo added!</div> : null}
<button
onClick={() => {
mutation.mutate({ id: new Date(), title: 'Do Laundry' })
}}
>
Create Todo
</button>
</>
)}
</div>
)
}
任意时刻,一个变更只能处于以下四种状态之一:
| 状态 | 说明 |
|---|---|
isIdle 或 status === 'idle' |
变更当前处于空闲或全新的重置状态 |
isPending 或 status === 'pending' |
变更当前正在执行 |
isError 或 status === 'error' |
变更遇到了错误 |
isSuccess 或 status === 'success' |
变更成功且变更数据可用 |
根据所处状态还有更多信息:
error:处于error状态时,可通过error属性拿到错误对象;data:处于success状态时,可通过data属性拿到返回数据。
在上面的例子中你已经看到,可以通过 mutate 函数传入单个变量或对象作为 mutationFn 的参数。
单看变量传递,变更似乎没什么特别;但当它与 onSuccess 选项、QueryClient 的 invalidateQueries 方法以及 QueryClient 的 setQueryData 方法组合使用时,变更就会成为非常强大的工具——这正是第三个核心概念「查询失效」的用武之地。
注意:
mutate是异步函数。在 React 16 及更早版本中,不能直接在事件回调里使用它(受 React 事件池机制影响);如果需要在onSubmit中访问事件对象,必须把mutate包一层。例如下面这种写法在旧版本 React 中不生效:// This will not work in React 16 and earlier const CreateTodo = () => { const mutation = useMutation({ mutationFn: (event) => { event.preventDefault() return fetch('/api', new FormData(event.target)) }, }) return <form onSubmit={mutation.mutate}>...</form> }推荐的做法是在外层包装函数中先处理事件,再调用
mutate:// This will work const CreateTodo = () => { const mutation = useMutation({ mutationFn: (formData) => { return fetch('/api', formData) }, }) const onSubmit = (event) => { event.preventDefault() mutation.mutate(new FormData(event.target)) } return <form onSubmit={onSubmit}>...</form> }这一限制针对 React 旧版本的事件池行为;在 Preact 中同样建议采用包装函数这种稳妥写法。
使用 mutateAsync 获取 Promise
如果希望拿到一个「成功时 resolve、失败时 throw」的 Promise(例如用于组合副作用),请使用 mutateAsync 而不是 mutate:
const mutation = useMutation({ mutationFn: addTodo })
try {
const todo = await mutation.mutateAsync(todo)
console.log(todo)
} catch (error) {
console.error(error)
} finally {
console.log('done')
}
重置变更状态
有时需要清除某个变更请求的 error 或 data,此时可以使用 reset 函数:
const CreateTodo = () => {
const [title, setTitle] = useState('')
const mutation = useMutation({ mutationFn: createTodo })
const onCreateTodo = (e) => {
e.preventDefault()
mutation.mutate({ title })
}
return (
<form onSubmit={onCreateTodo}>
{mutation.error && (
<h5 onClick={() => mutation.reset()}>{mutation.error}</h5>
)}
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<br />
<button type="submit">Create Todo</button>
</form>
)
}
变更副作用回调
useMutation 提供了一些辅助选项,让你能在变更生命周期的任意阶段快速、方便地执行副作用。这些回调对「变更后失效并重新获取查询」以及「乐观更新」非常关键:
useMutation({
mutationFn: addTodo,
onMutate: (variables, context) => {
// A mutation is about to happen!
// Optionally return a result containing data to use when for example rolling back
return { id: 1 }
},
onError: (error, variables, onMutateResult, context) => {
// An error happened!
console.log(`rolling back optimistic update with id ${onMutateResult.id}`)
},
onSuccess: (data, variables, onMutateResult, context) => {
// Boom baby!
},
onSettled: (data, error, variables, onMutateResult, context) => {
// Error or success... doesn't matter!
},
})
回调的串行等待:如果这些回调函数返回 Promise,那么它会被 await,然后再调用下一个回调:
useMutation({
mutationFn: addTodo,
onSuccess: async () => {
console.log("I'm first!")
},
onSettled: async () => {
console.log("I'm second!")
},
})
调用 mutate 时追加回调:你可能会希望在 mutate 调用处触发 useMutation 定义之外的回调,用于执行组件特有的副作用。为此,可以在变更变量之后向 mutate 传入同样的回调选项(支持 onSuccess、onError、onSettled)。请注意:如果组件在变更完成之前卸载,这些追加回调不会执行:
useMutation({
mutationFn: addTodo,
onSuccess: (data, variables, onMutateResult, context) => {
// I will fire first
},
onError: (error, variables, onMutateResult, context) => {
// I will fire first
},
onSettled: (data, error, variables, onMutateResult, context) => {
// I will fire first
},
})
mutate(todo, {
onSuccess: (data, variables, onMutateResult, context) => {
// I will fire second!
},
onError: (error, variables, onMutateResult, context) => {
// I will fire second!
},
onSettled: (data, error, variables, onMutateResult, context) => {
// I will fire second!
},
})
连续变更时的回调差异
当处理连续多次变更时,onSuccess/onError/onSettled 的表现存在细微差别:传给 mutate 的回调只会在组件仍然挂载时触发一次(因为每次调用 mutate 时 mutation observer 都会被移除并重新订阅);而 useMutation 层级的 handler 则会对每一次 mutate 调用都执行。
另外要注意:mutationFn 通常都是异步的,因此多个变更完成的顺序可能与 mutate 调用的顺序不一致:
useMutation({
mutationFn: addTodo,
onSuccess: (data, variables, onMutateResult, context) => {
// Will be called 3 times
},
})
const todos = ['Todo 1', 'Todo 2', 'Todo 3']
todos.forEach((todo) => {
mutate(todo, {
onSuccess: (data, variables, onMutateResult, context) => {
// Will execute only once, for the last mutation (Todo 3),
// regardless which mutation resolves first
},
})
})
重试(Retry)
默认情况下,TanStack Query 不会在变更出错时重试,但可以通过 retry 选项开启:
const mutation = useMutation({
mutationFn: addTodo,
retry: 3,
})
如果变更因设备离线而失败,它们会在设备重新联网后按原有顺序被重试。
持久化变更(Persist mutations)
需要时,变更可以被持久化到存储中并在之后恢复,这通过 hydration 相关函数完成。下面的示例演示了:定义带乐观更新的默认变更 → 发起变更 → 离线暂停后 dehydrate 序列化 → 应用重启后 hydrate 恢复 → resumePausedMutations 恢复被暂停的变更:
const queryClient = new QueryClient()
// Define the "addTodo" mutation
queryClient.setMutationDefaults(['addTodo'], {
mutationFn: addTodo,
onMutate: async (variables, context) => {
// Cancel current queries for the todos list
await context.client.cancelQueries({ queryKey: ['todos'] })
// Create optimistic todo
const optimisticTodo = { id: uuid(), title: variables.title }
// Add optimistic todo to todos list
context.client.setQueryData(['todos'], (old) => [...old, optimisticTodo])
// Return a result with the optimistic todo
return { optimisticTodo }
},
onSuccess: (result, variables, onMutateResult, context) => {
// Replace optimistic todo in the todos list with the result
context.client.setQueryData(['todos'], (old) =>
old.map((todo) =>
todo.id === onMutateResult.optimisticTodo.id ? result : todo,
),
)
},
onError: (error, variables, onMutateResult, context) => {
// Remove optimistic todo from the todos list
context.client.setQueryData(['todos'], (old) =>
old.filter((todo) => todo.id !== onMutateResult.optimisticTodo.id),
)
},
retry: 3,
})
// Start mutation in some component:
const mutation = useMutation({ mutationKey: ['addTodo'] })
mutation.mutate({ title: 'title' })
// If the mutation has been paused because the device is for example offline,
// Then the paused mutation can be dehydrated when the application quits:
const state = dehydrate(queryClient)
// The mutation can then be hydrated again when the application is started:
hydrate(queryClient, state)
// Resume the paused mutations:
queryClient.resumePausedMutations()
持久化离线变更(Persisting Offline mutations)
如果配合 persistQueryClient 插件 持久化离线变更,那么除非提供了默认的 mutation 函数,否则页面重载后无法恢复这些变更。这是一个技术限制:持久化到外部存储时,只有变更的状态被序列化(函数无法被序列化)。hydration 之后,触发该变更的组件可能并未挂载,此时调用 resumePausedMutations 可能抛出 No mutationFn found 错误。解决办法是预先为相应 key 设置默认 mutation 函数:
const persister = createSyncStoragePersister({
storage: window.localStorage,
})
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
},
},
})
// we need a default mutation function so that paused mutations can resume after a page reload
queryClient.setMutationDefaults(['todos'], {
mutationFn: ({ id, data }) => {
return api.updateTodo(id, data)
},
})
export default function App() {
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister }}
onSuccess={() => {
// resume mutations after initial restore from localStorage was successful
queryClient.resumePausedMutations()
}}
>
<RestOfTheApp />
</PersistQueryClientProvider>
)
}
关于查询与变更的完整离线方案,仓库中还有更详尽的 offline 示例(对应 React 版,Preact 版可参考同样结构)。
变更作用域(Mutation Scopes)
默认情况下,所有变更都是并行执行的——即使你对同一个 mutation 的 .mutate() 连续调用多次。可以通过给变更指定带 id 的 scope 来避免这一点:所有具有相同 scope.id 的变更将串行执行——当某个 scope 已有正在进行的变更时,新触发的变更会以 isPaused: true 状态进入队列,等轮到它们时自动恢复:
const mutation = useMutation({
mutationFn: addTodo,
scope: {
id: 'todo',
},
})
概念三:查询失效(Query Invalidation)
等待查询自然过期再重新获取并不总是可行——尤其是当你知道用户的操作已经让某条数据确凿地过期时。为此,QueryClient 提供了 invalidateQueries 方法,可以智能地标记查询为过期并(按需)触发重新获取:
// Invalidate every query in the cache
queryClient.invalidateQueries()
// Invalidate every query with a key that starts with `todos`
queryClient.invalidateQueries({ queryKey: ['todos'] })
设计哲学:其他使用归一化缓存的库,会试图通过命令式更新或 schema 推断把新数据同步到本地查询;而 TanStack Query 反其道而行之——提供工具让你免于维护归一化缓存的手工劳动,转而采用定向失效 + 后台重新获取 + 最终原子更新的范式。
当通过 invalidateQueries 使一个查询失效时,会发生两件事:
- 它被标记为 stale(过期),该过期状态会覆盖
useQuery及相关 hook 中配置的任何staleTime; - 如果该查询当前正被
useQuery及相关 hook 渲染,它还会在后台被重新获取。
前缀匹配
invalidateQueries、removeQueries 等支持部分查询匹配的 API,都可以按 key 的前缀匹配多个查询,也可以非常精确地匹配单个查询。例如用 todos 前缀来失效所有 key 以 todos 开头的查询:
import { useQuery, useQueryClient } from '@tanstack/preact-query'
// Get QueryClient from the context
const queryClient = useQueryClient()
queryClient.invalidateQueries({ queryKey: ['todos'] })
// Both queries below will be invalidated
const todoListQuery = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList,
})
const todoListQuery = useQuery({
queryKey: ['todos', { page: 1 }],
queryFn: fetchTodoList,
})
你还可以通过传入更具体的 query key 来失效带特定变量的查询:
queryClient.invalidateQueries({
queryKey: ['todos', { type: 'done' }],
})
// The query below will be invalidated
const todoListQuery = useQuery({
queryKey: ['todos', { type: 'done' }],
queryFn: fetchTodoList,
})
// However, the following query below will NOT be invalidated
const todoListQuery = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList,
})
exact 精确匹配
invalidateQueries 非常灵活:如果只想失效没有任何附加变量或子 key 的 todos 查询,可以传入 exact: true:
queryClient.invalidateQueries({
queryKey: ['todos'],
exact: true,
})
// The query below will be invalidated
const todoListQuery = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList,
})
// However, the following query below will NOT be invalidated
const todoListQuery = useQuery({
queryKey: ['todos', { type: 'done' }],
queryFn: fetchTodoList,
})
predicate 谓词匹配
如果还想要更细粒度的控制,可以向 invalidateQueries 传入谓词函数。该函数会收到 query cache 中的每一个 Query 实例,由你返回 true/false 决定是否失效:
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] === 'todos' && query.queryKey[1]?.version >= 10,
})
// The query below will be invalidated
const todoListQuery = useQuery({
queryKey: ['todos', { version: 20 }],
queryFn: fetchTodoList,
})
// The query below will be invalidated
const todoListQuery = useQuery({
queryKey: ['todos', { version: 10 }],
queryFn: fetchTodoList,
})
// However, the following query below will NOT be invalidated
const todoListQuery = useQuery({
queryKey: ['todos', { version: 5 }],
queryFn: fetchTodoList,
})
更多查询过滤器
invalidateQueries 等 API 接受的 QueryFilters 对象还支持 type(active/inactive/all,默认 all)、stale(匹配过期/新鲜查询)、fetchStatus(fetching/paused/idle)等过滤条件,详细说明可参考 Query Filters 文档。例如:
// Cancel all queries
await queryClient.cancelQueries()
// Remove all inactive queries that begin with `posts` in the key
queryClient.removeQueries({ queryKey: ['posts'], type: 'inactive' })
// Refetch all active queries that begin with `posts` in the key
await queryClient.refetchQueries({ queryKey: ['posts'], type: 'active' })
完整可运行示例
将三大核心概念串起来,就得到了 quick-start 文档中的完整示例(原示例使用 React 的 render 与 react-dom,对应 Preact 版本应改为从 preact 导入 render):创建 client → Provider 注入 → useQuery 拉取 todos 列表 → useMutation 提交新 todo → 成功后 invalidateQueries 失效并后台刷新列表:
import { render } from 'preact'
import {
useQuery,
useMutation,
useQueryClient,
QueryClient,
QueryClientProvider,
} from '@tanstack/preact-query'
import { getTodos, postTodo } from '../my-api'
// Create a client
const queryClient = new QueryClient()
function App() {
return (
// Provide the client to your App
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
)
}
function Todos() {
// Access the client
const queryClient = useQueryClient()
// Queries
const query = useQuery({ queryKey: ['todos'], queryFn: getTodos })
// Mutations
const mutation = useMutation({
mutationFn: postTodo,
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
return (
<div>
<ul>
{query.data?.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
<button
onClick={() => {
mutation.mutate({
id: Date.now(),
title: 'Do Laundry',
})
}}
>
Add Todo
</button>
</div>
)
}
render(<App />, document.getElementById('root'))
这三大概念构成了 @tanstack/preact-query 的大部分核心功能,文档的后续章节会逐一深入讲解。
仓库中还提供了可直接运行的真实示例 examples/preact/simple,它是一个 Vite + Preact 项目,通过 npm run dev 即可启动。其入口 examples/preact/simple/src/index.tsx 展示了相同的三要素:创建 QueryClient → QueryClientProvider 注入 → useQuery 拉取 GitHub API 仓库信息,并用 isPending/error/isFetching 处理加载、错误与后台更新提示:
import { render } from 'preact'
import {
QueryClient,
QueryClientProvider,
useQuery,
} from '@tanstack/preact-query'
const queryClient = new QueryClient()
export function App() {
return (
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>
)
}
const Example = () => {
const { isPending, error, data, isFetching } = useQuery({
queryKey: ['repoData'],
queryFn: async () => {
const response = await fetch(
'https://api.github.com/repos/TanStack/query',
)
return await response.json()
},
})
if (isPending) return 'Loading...'
if (error !== null) return 'An error has occurred: ' + error.message
return (
<div>
<h1>{data.full_name}</h1>
<p>{data.description}</p>
<strong>👀 {data.subscribers_count}</strong>{' '}
<strong>✨ {data.stargazers_count}</strong>{' '}
<strong>🍴 {data.forks_count}</strong>
<div>{isFetching ? 'Updating...' : ''}</div>
</div>
)
}
const app = document.getElementById('app')
if (!app) throw new Error('Missing #app element')
render(<App />, app)
总结与下一步
至此你已经掌握了 @tanstack/preact-query 的完整上手链路:
- 安装:包管理器或 CDN 两种方式皆可,Preact 10+ 均兼容;
- 注入:
new QueryClient()+QueryClientProvider将 client 下发到整个组件树(Provider 挂载/卸载会自动mount/unmount,联动焦点与联网事件); - 查询:
useQuery({ queryKey, queryFn })声明式拉取数据,用status(pending/error/success)判断数据是否存在,用fetchStatus(fetching/paused/idle)判断queryFn是否在运行; - 变更:
useMutation+mutate/mutateAsync执行服务端写操作,利用onSuccess/onError/onSettled编排副作用,支持重试、持久化与作用域串行; - 失效:
queryClient.invalidateQueries({ queryKey })定向失效查询并触发后台重新获取,配合前缀匹配、exact与predicate实现精确控制。
继续深入的话,建议阅读 Queries 详解、Mutations 详解 与 Query Invalidation 详解,并结合 packages/preact-query/src 下的源码(如 useQuery.ts、useMutation.ts、QueryClientProvider.tsx)理解适配层实现;Preact 框架的完整文档目录见 docs/framework/preact。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00