TanStack Query
TanStack Query (formerly React Query) is the best library for fetching, caching, and updating server state in React.
Installation
bash
1npm install @tanstack/react-querySetup
jsx
1import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
2import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
3
4const queryClient = new QueryClient({
5 defaultOptions: {
6 queries: {
7 staleTime: 60 * 1000, // 1 minute
8 retry: 1,
9 }
10 }
11})
12
13function App() {
14 return (
15 <QueryClientProvider client={queryClient}>
16 <Router />
17 <ReactQueryDevtools initialIsOpen={false} />
18 </QueryClientProvider>
19 )
20}Basic Query
jsx
1import { useQuery } from '@tanstack/react-query'
2
3function UserList() {
4 const { data, isLoading, isError, error } = useQuery({
5 queryKey: ['users'],
6 queryFn: async () => {
7 const response = await fetch('/api/users')
8 if (!response.ok) throw new Error('Failed to fetch')
9 return response.json()
10 }
11 })
12
13 if (isLoading) return <Spinner />
14 if (isError) return <p>Error: {error.message}</p>
15
16 return (
17 <ul>
18 {data.map(user => (
19 <li key={user.id}>{user.name}</li>
20 ))}
21 </ul>
22 )
23}Query Keys
Query keys uniquely identify data in the cache:
jsx
1// Simple key
2useQuery({ queryKey: ['todos'], ... })
3
4// With variables
5useQuery({ queryKey: ['todos', todoId], ... })
6
7// With filters
8useQuery({ queryKey: ['todos', { status, page }], ... })
9
10// Nested
11useQuery({ queryKey: ['users', userId, 'posts'], ... })Query with Parameters
jsx
1function UserProfile({ userId }) {
2 const { data: user, isLoading } = useQuery({
3 queryKey: ['users', userId],
4 queryFn: async () => {
5 const response = await fetch(`/api/users/${userId}`)
6 return response.json()
7 },
8 enabled: !!userId // Only fetch if userId exists
9 })
10
11 if (isLoading) return <Spinner />
12
13 return <h1>{user.name}</h1>
14}Mutations
For creating, updating, or deleting data:
jsx
1import { useMutation, useQueryClient } from '@tanstack/react-query'
2
3function CreateTodo() {
4 const queryClient = useQueryClient()
5
6 const mutation = useMutation({
7 mutationFn: async (newTodo) => {
8 const response = await fetch('/api/todos', {
9 method: 'POST',
10 headers: { 'Content-Type': 'application/json' },
11 body: JSON.stringify(newTodo)
12 })
13 return response.json()
14 },
15 onSuccess: () => {
16 // Invalidate and refetch
17 queryClient.invalidateQueries({ queryKey: ['todos'] })
18 },
19 onError: (error) => {
20 console.error('Failed to create todo:', error)
21 }
22 })
23
24 const handleSubmit = (e) => {
25 e.preventDefault()
26 mutation.mutate({ title: 'New Todo' })
27 }
28
29 return (
30 <form onSubmit={handleSubmit}>
31 <button
32 type="submit"
33 disabled={mutation.isPending}
34 >
35 {mutation.isPending ? 'Creating...' : 'Create Todo'}
36 </button>
37 {mutation.isError && <p>Error: {mutation.error.message}</p>}
38 </form>
39 )
40}Optimistic Updates
Update UI immediately, rollback on error:
jsx
1const queryClient = useQueryClient()
2
3const mutation = useMutation({
4 mutationFn: updateTodo,
5
6 onMutate: async (newTodo) => {
7 // Cancel outgoing refetches
8 await queryClient.cancelQueries({ queryKey: ['todos'] })
9
10 // Snapshot previous value
11 const previousTodos = queryClient.getQueryData(['todos'])
12
13 // Optimistically update
14 queryClient.setQueryData(['todos'], (old) =>
15 old.map(todo =>
16 todo.id === newTodo.id ? newTodo : todo
17 )
18 )
19
20 // Return context with snapshot
21 return { previousTodos }
22 },
23
24 onError: (err, newTodo, context) => {
25 // Rollback on error
26 queryClient.setQueryData(['todos'], context.previousTodos)
27 },
28
29 onSettled: () => {
30 // Refetch after error or success
31 queryClient.invalidateQueries({ queryKey: ['todos'] })
32 }
33})Infinite Queries
For pagination/infinite scroll:
jsx
1import { useInfiniteQuery } from '@tanstack/react-query'
2
3function InfinitePosts() {
4 const {
5 data,
6 fetchNextPage,
7 hasNextPage,
8 isFetchingNextPage,
9 isLoading
10 } = useInfiniteQuery({
11 queryKey: ['posts'],
12 queryFn: async ({ pageParam = 1 }) => {
13 const response = await fetch(`/api/posts?page=${pageParam}`)
14 return response.json()
15 },
16 getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
17 initialPageParam: 1
18 })
19
20 if (isLoading) return <Spinner />
21
22 return (
23 <div>
24 {data.pages.map((page) =>
25 page.posts.map(post => (
26 <Post key={post.id} post={post} />
27 ))
28 )}
29
30 {hasNextPage && (
31 <button
32 onClick={() => fetchNextPage()}
33 disabled={isFetchingNextPage}
34 >
35 {isFetchingNextPage ? 'Loading...' : 'Load More'}
36 </button>
37 )}
38 </div>
39 )
40}Parallel Queries
jsx
1function Dashboard() {
2 const usersQuery = useQuery({
3 queryKey: ['users'],
4 queryFn: fetchUsers
5 })
6
7 const projectsQuery = useQuery({
8 queryKey: ['projects'],
9 queryFn: fetchProjects
10 })
11
12 if (usersQuery.isLoading || projectsQuery.isLoading) {
13 return <Spinner />
14 }
15
16 return (
17 <div>
18 <UserList users={usersQuery.data} />
19 <ProjectList projects={projectsQuery.data} />
20 </div>
21 )
22}
23
24// Or use useQueries for dynamic queries
25import { useQueries } from '@tanstack/react-query'
26
27function UserProfiles({ userIds }) {
28 const userQueries = useQueries({
29 queries: userIds.map(id => ({
30 queryKey: ['users', id],
31 queryFn: () => fetchUser(id)
32 }))
33 })
34
35 // userQueries is an array of query results
36}Dependent Queries
jsx
1function UserPosts({ userId }) {
2 // First query
3 const { data: user } = useQuery({
4 queryKey: ['users', userId],
5 queryFn: () => fetchUser(userId)
6 })
7
8 // Second query depends on first
9 const { data: posts } = useQuery({
10 queryKey: ['posts', user?.id],
11 queryFn: () => fetchPosts(user.id),
12 enabled: !!user?.id // Only run when user exists
13 })
14}Prefetching
jsx
1const queryClient = useQueryClient()
2
3// Prefetch on hover
4function ProjectLink({ projectId }) {
5 const prefetch = () => {
6 queryClient.prefetchQuery({
7 queryKey: ['projects', projectId],
8 queryFn: () => fetchProject(projectId)
9 })
10 }
11
12 return (
13 <Link
14 to={`/projects/${projectId}`}
15 onMouseEnter={prefetch}
16 >
17 View Project
18 </Link>
19 )
20}Query Options
jsx
1useQuery({
2 queryKey: ['todos'],
3 queryFn: fetchTodos,
4
5 // Timing
6 staleTime: 5 * 60 * 1000, // 5 minutes
7 gcTime: 10 * 60 * 1000, // 10 minutes (was cacheTime)
8 refetchInterval: 30000, // Refetch every 30s
9
10 // Behavior
11 enabled: true, // Enable/disable query
12 retry: 3, // Retry failed requests
13 retryDelay: 1000, // Delay between retries
14
15 // Refetching
16 refetchOnWindowFocus: true,
17 refetchOnMount: true,
18 refetchOnReconnect: true,
19
20 // Placeholders
21 placeholderData: [], // Show while loading
22 initialData: cachedData // Use if available
23})TanStack Query makes server state management elegant and powerful!
