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