Skip to main content

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

Build docs developers (and LLMs) love