React Query Overview
TanStack Query (formerly React Query) is a powerful library for managing, caching, and synchronizing asynchronous and remote data in React applications. It provides hooks that make fetching, caching, synchronizing, and updating server state simple and efficient.
Why React Query?
React Query eliminates the need to manually manage loading states, error states, and cache invalidation. It provides:
Automatic caching - Intelligently caches your data with configurable retention
Background refetching - Keeps data fresh automatically
Request deduplication - Multiple components using the same query share a single request
Optimistic updates - Update UI before server response for better UX
Infinite scroll support - Built-in pagination and infinite query support
DevTools - Powerful debugging tools for development
Core Concepts
Queries
Queries are declarative dependencies on asynchronous data sources. Use the useQuery hook to fetch data:
import { useQuery } from '@tanstack/react-query'
function Posts () {
const { data , isLoading , error } = useQuery ({
queryKey: [ 'posts' ],
queryFn : async () => {
const response = await fetch ( '/api/posts' )
return response . json ()
},
})
if ( isLoading ) return < div > Loading... </ div >
if ( error ) return < div > Error: { error . message } </ div >
return (
< ul >
{ data . map (( post ) => (
< li key = { post . id } > { post . title } </ li >
)) }
</ ul >
)
}
Mutations
Mutations are used for create, update, and delete operations. Use the useMutation hook:
import { useMutation , useQueryClient } from '@tanstack/react-query'
function CreatePost () {
const queryClient = useQueryClient ()
const mutation = useMutation ({
mutationFn : ( newPost ) => {
return fetch ( '/api/posts' , {
method: 'POST' ,
body: JSON . stringify ( newPost ),
})
},
onSuccess : () => {
// Invalidate and refetch
queryClient . invalidateQueries ({ queryKey: [ 'posts' ] })
},
})
return (
< button onClick = { () => mutation . mutate ({ title: 'New Post' }) } >
{ mutation . isPending ? 'Creating...' : 'Create Post' }
</ button >
)
}
Query Client
The QueryClient manages all queries and mutations. Wrap your app with QueryClientProvider:
import { QueryClient , QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient ()
function App () {
return (
< QueryClientProvider client = { queryClient } >
< YourApp />
</ QueryClientProvider >
)
}
Key Features
Query Keys
Query keys uniquely identify queries. They can be strings or arrays: // Simple key
queryKey : [ 'posts' ]
// With parameters
queryKey : [ 'posts' , { userId: 1 }]
// Nested keys
queryKey : [ 'posts' , postId , 'comments' ]
Stale Time
Configure how long data stays fresh before refetching: useQuery ({
queryKey: [ 'posts' ],
queryFn: fetchPosts ,
staleTime: 5000 , // 5 seconds
})
Cache Time
Control how long unused data stays in cache: useQuery ({
queryKey: [ 'posts' ],
queryFn: fetchPosts ,
gcTime: 10 * 60 * 1000 , // 10 minutes
})
Query States
React Query provides detailed status information:
Property Description isLoadingQuery is fetching for the first time isPendingQuery has no data yet isErrorQuery encountered an error isSuccessQuery has data isFetchingQuery is fetching (including background refetch) dataThe actual data from the query errorError object if query failed
const { data , isLoading , isError , error , isFetching } = useQuery ({
queryKey: [ 'posts' ],
queryFn: fetchPosts ,
})
if ( isLoading ) return < Spinner />
if ( isError ) return < ErrorMessage error = { error } />
return (
< div >
{ isFetching && < RefreshIndicator /> }
< PostsList data = { data } />
</ div >
)
The difference between isLoading and isPending: isLoading is true only during the first fetch, while isPending is true whenever there’s no data yet.
Automatic Refetching
React Query automatically refetches data in several scenarios:
On mount - When a component mounts
On window focus - When the user returns to the tab
On network reconnect - When internet connection is restored
On interval - At a specified interval if configured
useQuery ({
queryKey: [ 'posts' ],
queryFn: fetchPosts ,
refetchOnWindowFocus: true ,
refetchOnMount: true ,
refetchInterval: 30000 , // Refetch every 30 seconds
})
Avoid setting very short refetchInterval values in production as this can lead to unnecessary server load and increased costs.
Dependent Queries
Enable queries conditionally based on other data:
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
})
return < div > ... </ div >
}
Parallel Queries
Fetch multiple queries simultaneously:
function Dashboard () {
const users = useQuery ({ queryKey: [ 'users' ], queryFn: fetchUsers })
const posts = useQuery ({ queryKey: [ 'posts' ], queryFn: fetchPosts })
const comments = useQuery ({ queryKey: [ 'comments' ], queryFn: fetchComments })
if ( users . isLoading || posts . isLoading || comments . isLoading ) {
return < Loading />
}
return < div > ... </ div >
}
Or use useQueries for dynamic parallel queries:
import { useQueries } from '@tanstack/react-query'
function MultipleUsers ({ userIds }) {
const results = useQueries ({
queries: userIds . map (( id ) => ({
queryKey: [ 'user' , id ],
queryFn : () => fetchUser ( id ),
})),
})
return < div > ... </ div >
}
React Query automatically handles request deduplication, so if multiple components request the same data, only one network request is made.
Next Steps
Installation Install and configure React Query in your project
Quick Start Build your first React Query app
TypeScript Learn about type-safe queries and mutations
DevTools Debug your queries with React Query DevTools