Skip to main content
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>
}

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>
  )
}

Performance

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:
  1. Change package imports:
// Before (React)
import { useQuery } from '@tanstack/react-query'

// After (Preact)
import { useQuery } from '@tanstack/preact-query'
  1. Use preact/compat for Suspense:
import { Suspense } from 'preact/compat'
  1. Everything else remains the same.
Preact Query maintains API parity with React Query, making migration seamless.

DevTools

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"
  }
}

Build docs developers (and LLMs) love