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.

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:
1
Install Solid Query
2
npm install @tanstack/solid-query
3
Set Up the Query Client
4
Create a QueryClient instance and wrap your app with QueryClientProvider:
5
import { QueryClient, QueryClientProvider } from '@tanstack/solid-query'

const queryClient = new QueryClient()

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* Your app components */}
    </QueryClientProvider>
  )
}
6
The QueryClientProvider makes the query client available to all components in your app.
7
Create Your First Query
8
Use useQuery to fetch data:
9
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>
  )
}
10
The queryKey uniquely identifies your query. It’s used for caching, refetching, and more.
11
Add a Mutation
12
Use useMutation to modify data:
13
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>
  )
}
14
Handle Loading and Error States
15
Solid Query provides reactive state for all query states:
16
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>
  )
}

Pagination Example

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:
  1. Use Query Keys Consistently: Keep your query keys organized and consistent across your application
  2. Set Appropriate Stale Time: Configure staleTime based on how often your data changes
  3. Handle All States: Always handle loading, error, and success states
  4. Use Query Options Helper: Create reusable query configurations with queryOptions
  5. Leverage Suspense: Use Suspense boundaries for better loading experiences
  6. 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