Quick Start Guide
Get up and running with Solid Query in just a few minutes. This guide will walk you through creating a simple application that fetches and displays data.
Complete Example
Here’s a complete working example to get you started:
import { render } from 'solid-js/web'
import { For , Show , Suspense } from 'solid-js'
import {
QueryClient ,
QueryClientProvider ,
useQuery ,
useMutation ,
useQueryClient ,
} from '@tanstack/solid-query'
// Create a client
const queryClient = new QueryClient ()
// Define your data types
interface Post {
id : number
title : string
body : string
}
// Fetch function
async function fetchPosts () : Promise < Post []> {
const response = await fetch ( 'https://jsonplaceholder.typicode.com/posts' )
if ( ! response . ok ) throw new Error ( 'Network response was not ok' )
return response . json ()
}
// Component that uses the query
function Posts () {
const postsQuery = useQuery (() => ({
queryKey: [ 'posts' ],
queryFn: fetchPosts ,
staleTime: 5 * 60 * 1000 , // Consider data fresh for 5 minutes
}))
return (
< div >
< h2 > Posts </ h2 >
< Show when = { postsQuery . isLoading } >
< div > Loading... </ div >
</ Show >
< Show when = { postsQuery . isError } >
< div > Error: { postsQuery . error ?. message } </ div >
</ Show >
< Show when = { postsQuery . data } >
{ ( posts ) => (
< ul >
< For each = { posts (). slice ( 0 , 10 ) } >
{ ( post ) => (
< li >
< strong > { post . title } </ strong >
< p > { post . body } </ p >
</ li >
) }
</ For >
</ ul >
) }
</ Show >
< button onClick = { () => postsQuery . refetch () } >
Refetch
</ button >
</ div >
)
}
// App component with provider
function App () {
return (
< QueryClientProvider client = { queryClient } >
< div style = { { padding: '20px' } } >
< h1 > Solid Query Quick Start </ h1 >
< Posts />
</ div >
</ QueryClientProvider >
)
}
render (() => < App /> , document . getElementById ( 'root' ) ! )
Step-by-Step Guide
Let’s break down the key concepts:
npm install @tanstack/solid-query
Create a QueryClient instance and wrap your app with QueryClientProvider:
import { QueryClient , QueryClientProvider } from '@tanstack/solid-query'
const queryClient = new QueryClient ()
function App () {
return (
< QueryClientProvider client = { queryClient } >
{ /* Your app components */ }
</ QueryClientProvider >
)
}
The QueryClientProvider makes the query client available to all components in your app.
Use useQuery to fetch data:
import { useQuery } from '@tanstack/solid-query'
function TodoList () {
const todosQuery = useQuery (() => ({
queryKey: [ 'todos' ],
queryFn : async () => {
const response = await fetch ( 'https://api.example.com/todos' )
return response . json ()
},
}))
return (
< Show when = { todosQuery . data } >
{ ( todos ) => (
< For each = { todos () } >
{ ( todo ) => < div > { todo . title } </ div > }
</ For >
) }
</ Show >
)
}
The queryKey uniquely identifies your query. It’s used for caching, refetching, and more.
Use useMutation to modify data:
import { useMutation , useQueryClient } from '@tanstack/solid-query'
function AddTodo () {
const queryClient = useQueryClient ()
const mutation = useMutation (() => ({
mutationFn : async ( newTodo : { title : string }) => {
const response = await fetch ( 'https://api.example.com/todos' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ( newTodo ),
})
return response . json ()
},
onSuccess : () => {
// Invalidate and refetch
queryClient . invalidateQueries ({ queryKey: [ 'todos' ] })
},
}))
const handleSubmit = ( e : Event ) => {
e . preventDefault ()
const formData = new FormData ( e . target as HTMLFormElement )
const title = formData . get ( 'title' ) as string
mutation . mutate ({ title })
}
return (
< form onSubmit = { handleSubmit } >
< input name = "title" required />
< button type = "submit" disabled = { mutation . isPending } >
{ mutation . isPending ? 'Adding...' : 'Add Todo' }
</ button >
</ form >
)
}
Handle Loading and Error States
Solid Query provides reactive state for all query states:
function TodoList () {
const todosQuery = useQuery (() => ({
queryKey: [ 'todos' ],
queryFn: fetchTodos ,
}))
return (
< div >
< Show when = { todosQuery . isLoading } >
< div > Loading todos... </ div >
</ Show >
< Show when = { todosQuery . isError } >
< div > Error: { todosQuery . error ?. message } </ div >
</ Show >
< Show when = { todosQuery . isSuccess } >
< Show when = { todosQuery . data } >
{ ( todos ) => (
< For each = { todos () } >
{ ( todo ) => < TodoItem todo = { todo } /> }
</ For >
) }
</ Show >
</ Show >
< Show when = { todosQuery . isFetching } >
< div > Refreshing... </ div >
</ Show >
</ div >
)
}
Query Keys
Query keys are how Solid Query identifies and caches your data. They can be simple strings or arrays:
// Simple key
const query1 = useQuery (() => ({
queryKey: [ 'todos' ],
queryFn: fetchTodos ,
}))
// Key with parameters
const query2 = useQuery (() => ({
queryKey: [ 'todo' , props . todoId ],
queryFn : () => fetchTodo ( props . todoId ),
}))
// Complex key with filters
const query3 = useQuery (() => ({
queryKey: [ 'todos' , { status: 'done' , page: 1 }],
queryFn : () => fetchTodos ({ status: 'done' , page: 1 }),
}))
Query keys must be serializable and should include all variables that affect the query function.
Working with Suspense
Solid Query integrates seamlessly with SolidJS Suspense:
import { Suspense } from 'solid-js'
import { useQuery } from '@tanstack/solid-query'
function UserProfile ( props : { userId : string }) {
const userQuery = useQuery (() => ({
queryKey: [ 'user' , props . userId ],
queryFn : () => fetchUser ( props . userId ),
}))
// Accessing .data will suspend automatically
return (
< div >
< h2 > { userQuery . data . name } </ h2 >
< p > { userQuery . data . email } </ p >
</ div >
)
}
function App () {
return (
< Suspense fallback = { < div > Loading user... </ div > } >
< UserProfile userId = "123" />
</ Suspense >
)
}
The data property is a SolidJS Resource that automatically suspends when the query is loading.
Dependent Queries
Sometimes you need to fetch data that depends on other data:
function UserPosts ( props : { userId : string }) {
// First query: fetch user
const userQuery = useQuery (() => ({
queryKey: [ 'user' , props . userId ],
queryFn : () => fetchUser ( props . userId ),
}))
// Second query: fetch posts (depends on user)
const postsQuery = useQuery (() => ({
queryKey: [ 'posts' , props . userId ],
queryFn : () => fetchUserPosts ( props . userId ),
// Only run when we have the user data
enabled: !! userQuery . data ,
}))
return (
< div >
< Show when = { userQuery . data } >
{ ( user ) => (
< div >
< h2 > { user (). name } 's Posts </ h2 >
< Show when = { postsQuery . data } >
{ ( posts ) => (
< For each = { posts () } >
{ ( post ) => < div > { post . title } </ div > }
</ For >
) }
</ Show >
</ div >
) }
</ Show >
</ div >
)
}
Handle paginated data with ease:
import { createSignal } from 'solid-js'
import { useQuery } from '@tanstack/solid-query'
function PaginatedPosts () {
const [ page , setPage ] = createSignal ( 1 )
const postsQuery = useQuery (() => ({
queryKey: [ 'posts' , page ()],
queryFn : () => fetchPosts ( page ()),
// Keep previous data while fetching new page
placeholderData : ( previousData ) => previousData ,
}))
return (
< div >
< Show when = { postsQuery . data } >
{ ( posts ) => (
< For each = { posts () } >
{ ( post ) => < div > { post . title } </ div > }
</ For >
) }
</ Show >
< div >
< button
onClick = { () => setPage (( p ) => Math . max ( 1 , p - 1 )) }
disabled = { page () === 1 }
>
Previous
</ button >
< span > Page { page () } </ span >
< button
onClick = { () => setPage (( p ) => p + 1 ) }
disabled = { postsQuery . data ?. length === 0 }
>
Next
</ button >
</ div >
< Show when = { postsQuery . isFetching } >
< div > Loading... </ div >
</ Show >
</ div >
)
}
Infinite Queries
For infinite scroll or “load more” functionality:
import { useInfiniteQuery } from '@tanstack/solid-query'
function InfinitePosts () {
const query = useInfiniteQuery (() => ({
queryKey: [ 'posts' ],
queryFn : async ({ pageParam = 0 }) => {
const response = await fetch ( `/api/posts?cursor= ${ pageParam } ` )
return response . json ()
},
initialPageParam: 0 ,
getNextPageParam : ( lastPage ) => lastPage . nextCursor ,
getPreviousPageParam : ( firstPage ) => firstPage . previousCursor ,
}))
return (
< div >
< For each = { query . data ?. pages } >
{ ( page ) => (
< For each = { page . posts } >
{ ( post ) => < div > { post . title } </ div > }
</ For >
) }
</ For >
< button
onClick = { () => query . fetchNextPage () }
disabled = { ! query . hasNextPage || query . isFetchingNextPage }
>
{ query . isFetchingNextPage
? 'Loading more...'
: query . hasNextPage
? 'Load More'
: 'Nothing more to load' }
</ button >
</ div >
)
}
Optimistic Updates
Update the UI immediately while the mutation is in progress:
function TodoList () {
const queryClient = useQueryClient ()
const updateMutation = useMutation (() => ({
mutationFn : ( updatedTodo : Todo ) => updateTodo ( updatedTodo ),
// Optimistic update
onMutate : async ( updatedTodo ) => {
// Cancel outgoing refetches
await queryClient . cancelQueries ({ queryKey: [ 'todos' ] })
// Snapshot the previous value
const previousTodos = queryClient . getQueryData ([ 'todos' ])
// Optimistically update to the new value
queryClient . setQueryData ([ 'todos' ], ( old : Todo []) =>
old . map (( todo ) => todo . id === updatedTodo . id ? updatedTodo : todo )
)
// Return context with the snapshot
return { previousTodos }
},
// If mutation fails, roll back
onError : ( err , updatedTodo , context ) => {
queryClient . setQueryData ([ 'todos' ], context ?. previousTodos )
},
// Always refetch after error or success
onSettled : () => {
queryClient . invalidateQueries ({ queryKey: [ 'todos' ] })
},
}))
return < div > { /* Your component */ } </ div >
}
Best Practices
Follow these best practices for optimal performance and maintainability:
Use Query Keys Consistently : Keep your query keys organized and consistent across your application
Set Appropriate Stale Time : Configure staleTime based on how often your data changes
Handle All States : Always handle loading, error, and success states
Use Query Options Helper : Create reusable query configurations with queryOptions
Leverage Suspense : Use Suspense boundaries for better loading experiences
Invalidate Wisely : Invalidate queries after mutations to keep data fresh
Next Steps
TypeScript Learn about TypeScript integration and type safety
DevTools Debug your queries with Solid Query DevTools