TanStack Query for Preact provides hooks for fetching, caching, and updating asynchronous data in your Preact applications. The API is nearly identical to React Query.
Installation
npm install @tanstack/preact-query
# or
pnpm add @tanstack/preact-query
# or
yarn add @tanstack/preact-query
Setup
Wrap your application with QueryClientProvider:
import { QueryClient, QueryClientProvider } from '@tanstack/preact-query'
import { render } from 'preact'
import App from './App'
const queryClient = new QueryClient()
render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>,
document.getElementById('root')!
)
Core Hooks
useQuery
Fetch and cache data with the useQuery hook:
import { useQuery } from '@tanstack/preact-query'
function Todos() {
const { data, isLoading, error } = useQuery({
queryKey: ['todos'],
queryFn: async () => {
const res = await fetch('/api/todos')
return res.json()
},
})
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
useMutation
Perform side effects with mutations:
import { useMutation, useQueryClient } from '@tanstack/preact-query'
function AddTodo() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: async (newTodo) => {
const res = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
})
return res.json()
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
return (
<button onClick={() => mutation.mutate({ title: 'New Todo' })}>
{mutation.isPending ? 'Adding...' : 'Add Todo'}
</button>
)
}
useInfiniteQuery
Implement infinite scrolling:
import { useInfiniteQuery } from '@tanstack/preact-query'
function Posts() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: async ({ pageParam = 0 }) => {
const res = await fetch(`/api/posts?page=${pageParam}`)
return res.json()
},
getNextPageParam: (lastPage) => lastPage.nextCursor,
initialPageParam: 0,
})
return (
<div>
{data?.pages.map((page) => (
page.posts.map((post) => <Post key={post.id} post={post} />)
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
</div>
)
}
Suspense Support
useSuspenseQuery
Preact Query has full support for Suspense:
import { Suspense } from 'preact/compat'
import { useSuspenseQuery } from '@tanstack/preact-query'
function TodoList() {
const { data } = useSuspenseQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<TodoList />
</Suspense>
)
}
Use preact/compat to access Suspense features. The API is identical to React’s Suspense.
useSuspenseInfiniteQuery
import { Suspense } from 'preact/compat'
import { useSuspenseInfiniteQuery } from '@tanstack/preact-query'
function InfinitePosts() {
const { data, fetchNextPage, hasNextPage } = useSuspenseInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 0 }) => fetchPosts(pageParam),
getNextPageParam: (lastPage) => lastPage.nextCursor,
initialPageParam: 0,
})
return <PostList data={data} onLoadMore={fetchNextPage} />
}
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<InfinitePosts />
</Suspense>
)
}
Advanced Features
useQueries
Execute multiple queries in parallel:
import { useQueries } from '@tanstack/preact-query'
function UserData({ userIds }) {
const results = useQueries({
queries: userIds.map((id) => ({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
})),
})
const isLoading = results.some((result) => result.isLoading)
return <div>{/* render results */}</div>
}
Query Options Factory
import { queryOptions, useQuery } from '@tanstack/preact-query'
const todoQueries = {
all: () => queryOptions({
queryKey: ['todos'],
queryFn: fetchTodos,
}),
detail: (id: number) => queryOptions({
queryKey: ['todos', id],
queryFn: () => fetchTodo(id),
}),
}
function TodoDetail({ id }) {
const { data } = useQuery(todoQueries.detail(id))
return <div>{data.title}</div>
}
Error Boundaries
import { QueryErrorResetBoundary } from '@tanstack/preact-query'
import { ErrorBoundary } from 'preact-error-boundary'
function App() {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
fallback={(error, resetError) => (
<div>
Error: {error.message}
<button onClick={resetError}>Try again</button>
</div>
)}
>
<YourApp />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
)
}
Preact-Specific Considerations
Size Optimization
Preact Query is optimized for small bundle sizes, perfect for Preact’s lightweight philosophy:
import { QueryClient, QueryClientProvider } from '@tanstack/preact-query'
// Minimal configuration for smallest bundle
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
refetchOnWindowFocus: false,
},
},
})
Preact Signals Integration
While Preact Query uses hooks, you can integrate with Preact Signals:
import { signal } from '@preact/signals'
import { useQuery } from '@tanstack/preact-query'
const todoId = signal(1)
function TodoDetail() {
const { data } = useQuery({
queryKey: ['todo', todoId.value],
queryFn: () => fetchTodo(todoId.value),
})
return (
<div>
<button onClick={() => todoId.value++}>Next Todo</button>
{data && <h1>{data.title}</h1>}
</div>
)
}
Access signal values with .value when using them in query keys or functions.
Hydration
For SSR with Preact:
import { HydrationBoundary, dehydrate } from '@tanstack/preact-query'
// Server-side
export async function getServerData() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
return {
dehydratedState: dehydrate(queryClient),
}
}
// Client-side
function MyApp({ dehydratedState }) {
return (
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={dehydratedState}>
<YourApp />
</HydrationBoundary>
</QueryClientProvider>
)
}
TypeScript
Full TypeScript support:
import { useQuery } from '@tanstack/preact-query'
interface Todo {
id: number
title: string
completed: boolean
}
function Todos() {
const { data } = useQuery({
queryKey: ['todos'],
queryFn: async (): Promise<Todo[]> => {
const res = await fetch('/api/todos')
return res.json()
},
})
// data is typed as Todo[] | undefined
return <div>{data?.length} todos</div>
}
React vs Preact Differences
API Compatibility
The API is virtually identical to React Query:
import { useQuery } from '@tanstack/preact-query'
function Component() {
const query = useQuery({ queryKey: ['data'], queryFn: fetchData })
return <div>{query.data}</div>
}
import { useQuery } from '@tanstack/react-query'
function Component() {
const query = useQuery({ queryKey: ['data'], queryFn: fetchData })
return <div>{query.data}</div>
}
Suspense with Compat
// Import from preact/compat for Suspense features
import { Suspense } from 'preact/compat'
import { useSuspenseQuery } from '@tanstack/preact-query'
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<DataComponent />
</Suspense>
)
}
Automatic Request Cancellation
function SearchResults({ query }) {
const { data } = useQuery({
queryKey: ['search', query],
queryFn: async ({ signal }) => {
const res = await fetch(`/api/search?q=${query}`, { signal })
return res.json()
},
})
return <div>{/* results */}</div>
}
useMutationState
import { useMutationState } from '@tanstack/preact-query'
function GlobalLoadingIndicator() {
const pendingMutations = useMutationState({
filters: { status: 'pending' },
})
if (pendingMutations.length === 0) return null
return <div>Saving {pendingMutations.length} changes...</div>
}
useIsFetching
import { useIsFetching } from '@tanstack/preact-query'
function LoadingBar() {
const isFetching = useIsFetching()
if (!isFetching) return null
return <div class="loading-bar" />
}
Migration from React Query
Migrating from React Query is straightforward:
- Change package imports:
// Before (React)
import { useQuery } from '@tanstack/react-query'
// After (Preact)
import { useQuery } from '@tanstack/preact-query'
- Use
preact/compat for Suspense:
import { Suspense } from 'preact/compat'
- Everything else remains the same.
Preact Query maintains API parity with React Query, making migration seamless.
Use Preact Query DevTools for debugging:
npm install @tanstack/preact-query-devtools
Setup:
import { PrefactQueryDevtools } from '@tanstack/preact-query-devtools'
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<PrefactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
Best Practices
Bundle Size
Preact is known for small bundles. Optimize further:
// Import only what you need
import { useQuery } from '@tanstack/preact-query'
// Avoid importing everything
// import * as PreactQuery from '@tanstack/preact-query' ❌
Preact Compat
Use compat mode for maximum React compatibility:
{
"alias": {
"react": "preact/compat",
"react-dom": "preact/compat"
}
}