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
Set up the QueryClient
First, create a QueryClient and wrap your app with QueryClientProvider in your root layout:<script lang="ts">
import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query'
const queryClient = new QueryClient()
const { children } = $props()
</script>
<QueryClientProvider client={queryClient}>
{@render children()}
</QueryClientProvider>
Create your first query
Now create a component that fetches data using createQuery:<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.
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:
<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:
<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:
<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:
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:
<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
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']
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}
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
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
Advanced Guides
Explore advanced patterns like SSR, persisting, and more.Guides →