Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/tanstack/query/llms.txt

Use this file to discover all available pages before exploring further.

This guide will walk you through creating your first Svelte Query application, covering queries, mutations, and common patterns.

Prerequisites

Before starting, make sure you have:
  • Svelte 5.25.0 or higher installed
  • A Svelte or SvelteKit project set up
  • @tanstack/svelte-query installed
If you haven’t installed Svelte Query yet, see the Installation Guide.

Your First Query

1

Set up the QueryClient

First, create a QueryClient and wrap your app with QueryClientProvider in your root layout:
+layout.svelte
<script lang="ts">
import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query'

const queryClient = new QueryClient()
const { children } = $props()
</script>

<QueryClientProvider client={queryClient}>
  {@render children()}
</QueryClientProvider>
2

Create your first query

Now create a component that fetches data using createQuery:
Posts.svelte
<script lang="ts">
import { createQuery } from '@tanstack/svelte-query'

interface Post {
  id: number
  title: string
  body: string
}

async function fetchPosts(): Promise<Post[]> {
  const response = await fetch('https://jsonplaceholder.typicode.com/posts')
  if (!response.ok) throw new Error('Failed to fetch posts')
  return response.json()
}

const postsQuery = createQuery(() => ({
  queryKey: ['posts'],
  queryFn: fetchPosts,
}))
</script>

<div>
  <h1>Posts</h1>
  
  {#if postsQuery.isPending}
    <p>Loading posts...</p>
  {:else if postsQuery.isError}
    <p>Error: {postsQuery.error.message}</p>
  {:else}
    <ul>
      {#each postsQuery.data as post}
        <li>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </li>
      {/each}
    </ul>
  {/if}
</div>
The createQuery function takes an accessor function () => options that returns the query configuration. This allows the query to react to changes in dependencies.
3

Understanding query states

Svelte Query provides several state properties to handle different scenarios:
{#if postsQuery.isPending}
  <!-- Query is loading for the first time -->
  <Spinner />
{:else if postsQuery.isError}
  <!-- Query encountered an error -->
  <ErrorMessage error={postsQuery.error} />
{:else if postsQuery.isSuccess}
  <!-- Query succeeded and data is available -->
  <DataView data={postsQuery.data} />
{/if}

<!-- Background refetch indicator -->
{#if postsQuery.isFetching}
  <div class="refetch-indicator">Updating...</div>
{/if}
Key states:
  • isPending - Query has no data yet (initial load)
  • isError - Query failed
  • isSuccess - Query succeeded
  • isFetching - Query is fetching (includes background refetches)
  • data - The actual query data
  • error - The error object if query failed

Dynamic Queries

Queries can depend on reactive variables. The query automatically refetches when dependencies change:
PostDetail.svelte
<script lang="ts">
import { createQuery } from '@tanstack/svelte-query'

let postId = $state(1)

const postQuery = createQuery(() => ({
  queryKey: ['post', postId],
  queryFn: async () => {
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/posts/${postId}`
    )
    return response.json()
  },
}))
</script>

<div>
  <button onclick={() => postId--}>Previous</button>
  <button onclick={() => postId++}>Next</button>

  {#if postQuery.data}
    <h1>{postQuery.data.title}</h1>
    <p>{postQuery.data.body}</p>
  {/if}
</div>
When postId changes, the query key ['post', postId] changes, triggering an automatic refetch with the new ID.

Mutations

Use createMutation to create, update, or delete data:
CreatePost.svelte
<script lang="ts">
import { createMutation, useQueryClient } from '@tanstack/svelte-query'

const queryClient = useQueryClient()

interface NewPost {
  title: string
  body: string
}

const createPostMutation = createMutation(() => ({
  mutationFn: async (newPost: NewPost) => {
    const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(newPost),
    })
    return response.json()
  },
  onSuccess: () => {
    // Invalidate and refetch posts query
    queryClient.invalidateQueries({ queryKey: ['posts'] })
  },
}))

let title = $state('')
let body = $state('')

function handleSubmit() {
  createPostMutation.mutate({ title, body })
  title = ''
  body = ''
}
</script>

<form onsubmit={handleSubmit}>
  <input bind:value={title} placeholder="Title" />
  <textarea bind:value={body} placeholder="Body" />
  
  <button 
    type="submit" 
    disabled={createPostMutation.isPending}
  >
    {createPostMutation.isPending ? 'Creating...' : 'Create Post'}
  </button>

  {#if createPostMutation.isError}
    <p class="error">{createPostMutation.error.message}</p>
  {/if}
</form>

Mutation States

Mutations provide similar state properties:
  • isPending - Mutation is in progress
  • isError - Mutation failed
  • isSuccess - Mutation succeeded
  • data - The mutation result data
  • error - The error object if mutation failed
  • mutate() - Function to trigger the mutation
  • mutateAsync() - Promise-based mutation function

Query Options

Customize query behavior with various options:
const query = createQuery(() => ({
  queryKey: ['posts', { status: 'published' }],
  queryFn: fetchPublishedPosts,
  
  // Refetch interval (ms)
  refetchInterval: 10000,
  
  // Only refetch on window focus if data is stale
  refetchOnWindowFocus: 'always',
  
  // How long data stays fresh
  staleTime: 5 * 60 * 1000, // 5 minutes
  
  // Cache time
  gcTime: 10 * 60 * 1000, // 10 minutes
  
  // Retry failed requests
  retry: 3,
  
  // Enable/disable the query
  enabled: true,
}))
All time values are in milliseconds. Use staleTime to control when data is considered “stale” and needs refetching.

Infinite Queries

For paginated or infinite scroll data, use createInfiniteQuery:
InfinitePosts.svelte
<script lang="ts">
import { createInfiniteQuery } from '@tanstack/svelte-query'

interface PostsResponse {
  posts: Array<{ id: number; title: string }>
  nextCursor: number | null
}

const query = createInfiniteQuery(() => ({
  queryKey: ['posts', 'infinite'],
  queryFn: async ({ pageParam = 1 }) => {
    const response = await fetch(`/api/posts?page=${pageParam}`)
    return response.json() as Promise<PostsResponse>
  },
  initialPageParam: 1,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
}))
</script>

<div>
  {#if query.data}
    {#each query.data.pages as page}
      {#each page.posts as post}
        <article>
          <h2>{post.title}</h2>
        </article>
      {/each}
    {/each}
  {/if}

  <button
    onclick={() => query.fetchNextPage()}
    disabled={!query.hasNextPage || query.isFetchingNextPage}
  >
    {#if query.isFetchingNextPage}
      Loading more...
    {:else if query.hasNextPage}
      Load More
    {:else}
      No more posts
    {/if}
  </button>
</div>

Infinite Query Properties

  • data.pages - Array of all fetched pages
  • data.pageParams - Array of all page parameters
  • hasNextPage - Whether more pages are available
  • hasPreviousPage - Whether previous pages are available
  • fetchNextPage() - Load the next page
  • fetchPreviousPage() - Load the previous page
  • isFetchingNextPage - Next page is loading
  • isFetchingPreviousPage - Previous page is loading

Query Invalidation

Invalidate queries to force them to refetch:
<script lang="ts">
import { useQueryClient } from '@tanstack/svelte-query'

const queryClient = useQueryClient()

// Invalidate all queries
queryClient.invalidateQueries()

// Invalidate specific query
queryClient.invalidateQueries({ queryKey: ['posts'] })

// Invalidate queries matching a pattern
queryClient.invalidateQueries({ queryKey: ['posts', { status: 'draft' }] })
</script>

Using queryOptions Helper

For better type safety and reusability, use the queryOptions helper:
queries.ts
import { queryOptions } from '@tanstack/svelte-query'

export const postsQueryOptions = queryOptions({
  queryKey: ['posts'],
  queryFn: async () => {
    const response = await fetch('/api/posts')
    return response.json()
  },
  staleTime: 5 * 60 * 1000,
})
Then use it in your components:
Posts.svelte
<script lang="ts">
import { createQuery } from '@tanstack/svelte-query'
import { postsQueryOptions } from './queries'

const postsQuery = createQuery(() => postsQueryOptions)
</script>
The queryOptions helper provides better type inference and makes it easier to share query configurations across components.

Best Practices

1

Use meaningful query keys

Query keys should describe the data uniquely:
// Good
['posts', { status: 'published', author: userId }]
['user', userId]
['todos', { filter: 'completed' }]

// Bad
['data']
['fetch']
['query1']
2

Handle loading and error states

Always provide feedback for pending and error states:
{#if query.isPending}
  <LoadingSpinner />
{:else if query.isError}
  <ErrorMessage error={query.error} />
{:else}
  <DataDisplay data={query.data} />
{/if}
3

Configure staleTime appropriately

Set staleTime based on how often your data changes:
// User profile (rarely changes)
staleTime: 10 * 60 * 1000 // 10 minutes

// Real-time data (changes frequently)
staleTime: 0

// Dashboard stats (moderate updates)
staleTime: 60 * 1000 // 1 minute
4

Invalidate queries after mutations

Keep your UI in sync by invalidating related queries:
onSuccess: () => {
  queryClient.invalidateQueries({ queryKey: ['posts'] })
  queryClient.invalidateQueries({ queryKey: ['user', userId] })
}

Common Patterns

Dependent Queries

Execute a query only after another query succeeds:
<script lang="ts">
let userId = $state(1)

const userQuery = createQuery(() => ({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
}))

const projectsQuery = createQuery(() => ({
  queryKey: ['projects', userQuery.data?.id],
  queryFn: () => fetchUserProjects(userQuery.data!.id),
  enabled: !!userQuery.data,
}))
</script>

Optimistic Updates

Update UI immediately before server confirmation:
const mutation = createMutation(() => ({
  mutationFn: updatePost,
  onMutate: async (newPost) => {
    await queryClient.cancelQueries({ queryKey: ['posts'] })
    const previousPosts = queryClient.getQueryData(['posts'])
    queryClient.setQueryData(['posts'], (old) => [...old, newPost])
    return { previousPosts }
  },
  onError: (err, newPost, context) => {
    queryClient.setQueryData(['posts'], context.previousPosts)
  },
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ['posts'] })
  },
}))

Prefetching

Prefetch data before it’s needed:
<script lang="ts">
import { useQueryClient } from '@tanstack/svelte-query'

const queryClient = useQueryClient()

function handleMouseEnter(postId: number) {
  queryClient.prefetchQuery({
    queryKey: ['post', postId],
    queryFn: () => fetchPost(postId),
  })
}
</script>

<a href="/post/{post.id}" onmouseenter={() => handleMouseEnter(post.id)}>
  {post.title}
</a>

Next Steps

1

TypeScript Integration

Learn how to get full type safety with TypeScript.TypeScript Guide →
2

DevTools

Install and use the Svelte Query DevTools for debugging.DevTools Setup →
3

Advanced Guides

Explore advanced patterns like SSR, persisting, and more.Guides →

Build docs developers (and LLMs) love