Essential Concepts
Understand the fundamental concepts that make TanStack Query powerful and easy to use.
Query Keys
Query keys are the foundation of TanStack Query’s caching system. They uniquely identify each query in your application.
Basic Keys
The simplest query key is an array with a single string:
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
Keys with Variables
Include variables in your query key to create unique cache entries:
useQuery({
queryKey: ['todo', todoId],
queryFn: () => fetchTodo(todoId),
})
Query keys are hashed deterministically, so the order and structure matter. ['todos', 1] and ['todos', '1'] are different keys.
Complex Keys
Query keys can include objects for more complex scenarios:
useQuery({
queryKey: ['todos', { status: 'done', page: 1 }],
queryFn: () => fetchTodos({ status: 'done', page: 1 }),
})
Object keys are sorted automatically, so {a: 1, b: 2} and {b: 2, a: 1} produce the same cache key.
Query Functions
The query function is where you fetch your data. It must return a Promise that resolves to data or throws an error.
Basic Query Function
const { data } = useQuery({
queryKey: ['repos'],
queryFn: async () => {
const response = await fetch('https://api.github.com/repos/TanStack/query')
if (!response.ok) {
throw new Error('Network response was not ok')
}
return response.json()
},
})
Using Query Key in Function
The query function receives a context object with the query key:
const { data } = useQuery({
queryKey: ['todo', todoId],
queryFn: ({ queryKey }) => {
const [_key, id] = queryKey
return fetchTodo(id)
},
})
Signal for Cancellation
Use the AbortSignal for request cancellation:
const { data } = useQuery({
queryKey: ['todos'],
queryFn: ({ signal }) => {
return fetch('/api/todos', { signal })
},
})
Always throw errors in your query function instead of returning them. TanStack Query uses thrown errors to determine failure states.
Stale Time vs Cache Time
Understanding these two concepts is crucial for effective caching:
Stale Time
staleTime determines how long data is considered fresh:
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 1000 * 60 * 5, // 5 minutes
})
- Fresh data: Won’t refetch automatically
- Stale data: Will refetch in the background when conditions trigger it
- Default:
0 (immediately stale)
Cache Time (gcTime)
gcTime (garbage collection time) determines how long unused data stays in cache:
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
gcTime: 1000 * 60 * 60, // 1 hour
})
- Data remains in cache after all observers unmount
- After
gcTime expires, data is garbage collected
- Default:
5 minutes
staleTime affects when queries refetch. gcTime affects when cache entries are removed from memory.
Query Status
Queries can be in one of several states:
const { status, fetchStatus, data, error } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
Status
pending - No cached data and query is currently fetching
error - Query encountered an error
success - Query succeeded and data is available
Fetch Status
fetching - Query function is executing
paused - Query wants to fetch but is paused (offline)
idle - Query is not fetching
Use isPending for initial loading states and isFetching to show background update indicators.
Mutations
While queries fetch data, mutations modify data on the server:
import { useMutation, useQueryClient } from '@tanstack/react-query'
function TodoForm() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (newTodo) => {
return fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
})
},
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
return (
<button
onClick={() => {
mutation.mutate({ title: 'New Todo' })
}}
>
Add Todo
</button>
)
}
Mutation Status
const { mutate, isPending, isError, isSuccess, error } = useMutation({
mutationFn: createTodo,
})
if (isPending) return <Spinner />
if (isError) return <ErrorMessage error={error} />
if (isSuccess) return <SuccessMessage />
Optimistic Updates
Update the UI immediately before the server responds:
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['todos'] })
// Snapshot previous value
const previousTodos = queryClient.getQueryData(['todos'])
// Optimistically update
queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
// Return context with snapshot
return { previousTodos }
},
onError: (err, newTodo, context) => {
// Rollback on error
queryClient.setQueryData(['todos'], context.previousTodos)
},
onSettled: () => {
// Refetch after error or success
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
Always implement error handling with optimistic updates to ensure data consistency if the mutation fails.
Dependent Queries
Some queries depend on data from other queries:
function User({ userId }) {
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
})
const { data: projects } = useQuery({
queryKey: ['projects', user?.id],
queryFn: () => fetchProjects(user.id),
enabled: !!user?.id, // Only run when user.id exists
})
}
Use the enabled option to control when queries execute based on other data availability.
Query Invalidation
Invalidate queries to mark them as stale and trigger refetches:
const queryClient = useQueryClient()
// Invalidate specific query
queryClient.invalidateQueries({ queryKey: ['todos'] })
// Invalidate all queries starting with key
queryClient.invalidateQueries({ queryKey: ['todos', 'list'] })
// Invalidate all queries
queryClient.invalidateQueries()
Prefetching
Fetch data before it’s needed for better user experience:
const queryClient = useQueryClient()
function TodosList() {
const { data: todos } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
return (
<ul>
{todos.map((todo) => (
<li
key={todo.id}
onMouseEnter={() => {
// Prefetch todo details on hover
queryClient.prefetchQuery({
queryKey: ['todo', todo.id],
queryFn: () => fetchTodo(todo.id),
})
}}
>
{todo.title}
</li>
))}
</ul>
)
}
Prefetching is great for predictable user navigation patterns, like hovering over links or buttons.
Next Steps
TypeScript
Learn how to use TanStack Query with TypeScript
DevTools
Set up DevTools to visualize your queries