Skip
Arish's avatar

23. TanStack Query (React Query)


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-query

Setup

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!