Hooks for managing, caching and syncing asynchronous and remote data in React
—
Components and hooks for managing QueryClient context, hydration, and error boundaries.
Provider component for QueryClient context
const QueryClientProvider: React.FC<QueryClientProviderProps>
interface QueryClientProviderProps {
client: QueryClient
children?: React.ReactNode
}import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
// Create a client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 10 * 60 * 1000, // 10 minutes
retry: (failureCount, error) => {
if (error.status === 404) return false
return failureCount < 3
}
},
mutations: {
retry: 1
}
}
})
function App() {
return (
<QueryClientProvider client={queryClient}>
<div className="app">
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/users/:id" element={<UserProfile />} />
</Routes>
</Router>
</div>
</QueryClientProvider>
)
}import { QueryClient, QueryClientProvider, MutationCache, QueryCache } from '@tanstack/react-query'
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
console.error(`Query failed:`, error)
// Global query error handling
if (error.status === 401) {
// Redirect to login
window.location.href = '/login'
}
},
onSuccess: (data, query) => {
// Global success handling
console.log(`Query succeeded for key:`, query.queryKey)
}
}),
mutationCache: new MutationCache({
onError: (error, variables, context, mutation) => {
console.error(`Mutation failed:`, error)
// Show global error notification
toast.error(`Operation failed: ${error.message}`)
},
onSuccess: (data, variables, context, mutation) => {
// Show global success notification
if (mutation.options.meta?.successMessage) {
toast.success(mutation.options.meta.successMessage)
}
}
}),
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
refetchOnWindowFocus: false,
retry: (failureCount, error) => {
// Custom retry logic
if (error.status === 404 || error.status === 403) return false
return failureCount < 3
}
},
mutations: {
retry: (failureCount, error) => {
if (error.status >= 400 && error.status < 500) return false
return failureCount < 2
}
}
}
})
function App() {
return (
<QueryClientProvider client={queryClient}>
<ErrorBoundary>
<Suspense fallback={<GlobalLoadingSpinner />}>
<AppContent />
</Suspense>
</ErrorBoundary>
</QueryClientProvider>
)
}React context that holds the QueryClient instance
const QueryClientContext: React.Context<QueryClient | undefined>import { useContext } from 'react'
import { QueryClientContext } from '@tanstack/react-query'
function MyComponent() {
const queryClient = useContext(QueryClientContext)
if (!queryClient) {
throw new Error('QueryClient not found. Make sure you are using QueryClientProvider.')
}
const handleInvalidate = () => {
queryClient.invalidateQueries({ queryKey: ['posts'] })
}
return (
<button onClick={handleInvalidate}>
Refresh Posts
</button>
)
}Hook to access the current QueryClient instance
function useQueryClient(queryClient?: QueryClient): QueryClientimport { useQueryClient } from '@tanstack/react-query'
function RefreshButton() {
const queryClient = useQueryClient()
const handleRefreshAll = () => {
// Invalidate all queries
queryClient.invalidateQueries()
}
const handleRefreshPosts = () => {
// Invalidate specific queries
queryClient.invalidateQueries({ queryKey: ['posts'] })
}
return (
<div>
<button onClick={handleRefreshPosts}>Refresh Posts</button>
<button onClick={handleRefreshAll}>Refresh All</button>
</div>
)
}function PostActions({ postId }: { postId: number }) {
const queryClient = useQueryClient()
const handleOptimisticUpdate = () => {
queryClient.setQueryData(['post', postId], (oldPost: Post) => ({
...oldPost,
likes: oldPost.likes + 1
}))
}
const handlePrefetch = () => {
queryClient.prefetchQuery({
queryKey: ['post-comments', postId],
queryFn: () => fetchPostComments(postId),
staleTime: 5 * 60 * 1000
})
}
const handleRemoveFromCache = () => {
queryClient.removeQueries({ queryKey: ['post', postId] })
}
return (
<div>
<button onClick={handleOptimisticUpdate}>Like</button>
<button onClick={handlePrefetch}>Prefetch Comments</button>
<button onClick={handleRemoveFromCache}>Remove from Cache</button>
</div>
)
}function usePostCache() {
const queryClient = useQueryClient()
const updatePost = (postId: number, updates: Partial<Post>) => {
queryClient.setQueryData(['post', postId], (oldPost: Post) => ({
...oldPost,
...updates
}))
}
const addPost = (newPost: Post) => {
// Update posts list
queryClient.setQueryData(['posts'], (oldPosts: Post[]) =>
[newPost, ...oldPosts]
)
// Set individual post data
queryClient.setQueryData(['post', newPost.id], newPost)
}
const removePost = (postId: number) => {
// Remove from posts list
queryClient.setQueryData(['posts'], (oldPosts: Post[]) =>
oldPosts.filter(post => post.id !== postId)
)
// Remove individual post data
queryClient.removeQueries({ queryKey: ['post', postId] })
}
return { updatePost, addPost, removePost }
}Component for hydrating server-side rendered queries on the client
const HydrationBoundary: React.FC<HydrationBoundaryProps>
interface HydrationBoundaryProps {
state: DehydratedState | null | undefined
options?: HydrateOptions
children?: React.ReactNode
queryClient?: QueryClient
}// Server-side (Next.js example)
import { QueryClient, dehydrate } from '@tanstack/react-query'
export async function getServerSideProps() {
const queryClient = new QueryClient()
// Prefetch data on the server
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: fetchPosts
})
await queryClient.prefetchQuery({
queryKey: ['user', 'me'],
queryFn: fetchCurrentUser
})
return {
props: {
dehydratedState: dehydrate(queryClient)
}
}
}
// Client-side
function PostsPage({ dehydratedState }: { dehydratedState: DehydratedState }) {
return (
<HydrationBoundary state={dehydratedState}>
<PostsList />
<UserProfile />
</HydrationBoundary>
)
}function App({ dehydratedState }: { dehydratedState: DehydratedState }) {
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000 // 1 minute
}
}
}))
return (
<QueryClientProvider client={queryClient}>
<HydrationBoundary
state={dehydratedState}
options={{
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000 // Override stale time for hydrated queries
}
}
}}
>
<Router>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/posts" element={<PostsPage />} />
</Routes>
</Router>
</HydrationBoundary>
</QueryClientProvider>
)
}// Server-side with selective dehydration
export async function getServerSideProps() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: fetchPosts
})
await queryClient.prefetchQuery({
queryKey: ['user-settings'],
queryFn: fetchUserSettings
})
return {
props: {
dehydratedState: dehydrate(queryClient, {
shouldDehydrateQuery: (query) => {
// Only dehydrate posts, not user settings for security
return query.queryKey[0] === 'posts'
}
})
}
}
}function Layout({ globalDehydratedState, children }: {
globalDehydratedState: DehydratedState
children: React.ReactNode
}) {
return (
<HydrationBoundary state={globalDehydratedState}>
<Header />
<main>{children}</main>
<Footer />
</HydrationBoundary>
)
}
function PostPage({ pageDehydratedState }: { pageDehydratedState: DehydratedState }) {
return (
<HydrationBoundary state={pageDehydratedState}>
<PostContent />
<PostComments />
</HydrationBoundary>
)
}Component for providing error reset functionality to child queries
const QueryErrorResetBoundary: React.FC<QueryErrorResetBoundaryProps>
interface QueryErrorResetBoundaryProps {
children: QueryErrorResetBoundaryFunction | React.ReactNode
}
type QueryErrorResetBoundaryFunction = (
value: QueryErrorResetBoundaryValue,
) => React.ReactNode
interface QueryErrorResetBoundaryValue {
clearReset: QueryErrorClearResetFunction
isReset: QueryErrorIsResetFunction
reset: QueryErrorResetFunction
}import { QueryErrorResetBoundary } from '@tanstack/react-query'
import { ErrorBoundary } from 'react-error-boundary'
function App() {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
fallbackRender={({ error, resetErrorBoundary }) => (
<div className="error-fallback">
<h2>Something went wrong:</h2>
<pre className="error-message">{error.message}</pre>
<button onClick={resetErrorBoundary}>
Try again
</button>
</div>
)}
>
<Suspense fallback={<div>Loading...</div>}>
<UserDashboard />
</Suspense>
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
)
}function useQueryErrorResetBoundary(): QueryErrorResetBoundaryValueimport { useQueryErrorResetBoundary } from '@tanstack/react-query'
function CustomErrorBoundary({ children }: { children: React.ReactNode }) {
const { reset, isReset } = useQueryErrorResetBoundary()
const [hasError, setHasError] = useState(false)
useEffect(() => {
if (isReset()) {
setHasError(false)
}
}, [isReset])
if (hasError) {
return (
<div className="error-boundary">
<h2>Oops! Something went wrong</h2>
<button
onClick={() => {
reset()
setHasError(false)
}}
>
Reset and try again
</button>
</div>
)
}
return (
<ErrorBoundary
onError={() => setHasError(true)}
fallback={null}
>
{children}
</ErrorBoundary>
)
}function Layout() {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<div>
<ErrorBoundary
onReset={reset}
fallbackRender={({ resetErrorBoundary }) => (
<div className="header-error">
<span>Header failed to load</span>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
)}
>
<Header />
</ErrorBoundary>
<main>
<ErrorBoundary
onReset={reset}
fallbackRender={({ resetErrorBoundary }) => (
<div className="content-error">
<h2>Content failed to load</h2>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
)}
>
<MainContent />
</ErrorBoundary>
</main>
</div>
)}
</QueryErrorResetBoundary>
)
}Provider component for tracking restoration state
const IsRestoringProvider: React.Provider<boolean>function useIsRestoring(): booleanimport { useIsRestoring } from '@tanstack/react-query'
function GlobalLoadingIndicator() {
const isRestoring = useIsRestoring()
const isFetching = useIsFetching()
if (isRestoring) {
return (
<div className="restoration-indicator">
Restoring queries from server...
</div>
)
}
if (isFetching > 0) {
return (
<div className="fetching-indicator">
Loading... ({isFetching} queries)
</div>
)
}
return null
}function App({ dehydratedState }: { dehydratedState: DehydratedState }) {
const [queryClient] = useState(() => new QueryClient())
return (
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={dehydratedState}>
<IsRestoringProvider value={true}>
<RestorationAwareApp />
</IsRestoringProvider>
</HydrationBoundary>
</QueryClientProvider>
)
}
function RestorationAwareApp() {
const isRestoring = useIsRestoring()
// Don't render interactive elements during restoration
if (isRestoring) {
return <SkeletonLoader />
}
return <InteractiveApp />
}The following core classes are available for advanced use cases and custom implementations:
Manages the cache of queries and their states
class QueryCache {
constructor(config?: QueryCacheConfig)
add(query: Query): void
remove(query: Query): void
find(filters: QueryFilters): Query | undefined
findAll(filters?: QueryFilters): Query[]
notify(event: QueryCacheNotifyEvent): void
subscribe(callback: (event: QueryCacheNotifyEvent) => void): () => void
clear(): void
}
interface QueryCacheConfig {
onError?: (error: unknown, query: Query) => void
onSuccess?: (data: unknown, query: Query) => void
onSettled?: (data: unknown | undefined, error: unknown | null, query: Query) => void
}Example:
import { QueryCache } from '@tanstack/react-query'
const queryCache = new QueryCache({
onError: (error, query) => {
console.error('Query failed:', query.queryKey, error)
},
onSuccess: (data, query) => {
console.log('Query succeeded:', query.queryKey)
}
})
// Use in QueryClient
const queryClient = new QueryClient({ queryCache })Manages the cache of mutations and their states
class MutationCache {
constructor(config?: MutationCacheConfig)
add(mutation: Mutation): void
remove(mutation: Mutation): void
find(filters: MutationFilters): Mutation | undefined
findAll(filters?: MutationFilters): Mutation[]
notify(event: MutationCacheNotifyEvent): void
subscribe(callback: (event: MutationCacheNotifyEvent) => void): () => void
clear(): void
}
interface MutationCacheConfig {
onError?: (error: unknown, variables: unknown, context: unknown, mutation: Mutation) => void
onSuccess?: (data: unknown, variables: unknown, context: unknown, mutation: Mutation) => void
onSettled?: (data: unknown | undefined, error: unknown | null, variables: unknown, context: unknown, mutation: Mutation) => void
}Manages window focus detection for automatic refetching
const focusManager: {
subscribe(callback: (focused: boolean) => void): () => void
setFocused(focused?: boolean): void
isFocused(): boolean
setEventListener(handleFocus: () => void): void
}Example:
import { focusManager } from '@tanstack/react-query'
// Custom focus management
focusManager.setEventListener(() => {
// Custom logic to determine if window is focused
const hasFocus = document.hasFocus()
focusManager.setFocused(hasFocus)
})
// Subscribe to focus changes
const unsubscribe = focusManager.subscribe((focused) => {
console.log('Window focus changed:', focused)
})Manages network connectivity detection for automatic refetching
const onlineManager: {
subscribe(callback: (online: boolean) => void): () => void
setOnline(online?: boolean): void
isOnline(): boolean
setEventListener(handleOnline: () => void): void
}Example:
import { onlineManager } from '@tanstack/react-query'
// Custom online detection
onlineManager.setEventListener(() => {
onlineManager.setOnline(navigator.onLine)
})
// Subscribe to online status changes
const unsubscribe = onlineManager.subscribe((online) => {
if (online) {
console.log('Connection restored')
} else {
console.log('Connection lost')
}
})Manages batching and scheduling of notifications
const notifyManager: {
schedule(fn: () => void): void
batchCalls<T extends Array<unknown>>(fn: (...args: T) => void): (...args: T) => void
flush(): void
}Advanced observer classes for custom query management:
Lower-level observer for individual queries
class QueryObserver<TQueryFnData, TError, TData, TQueryKey extends QueryKey> {
constructor(client: QueryClient, options: QueryObserverOptions<TQueryFnData, TError, TData, TQueryKey>)
subscribe(listener?: (result: QueryObserverResult<TData, TError>) => void): () => void
getCurrentResult(): QueryObserverResult<TData, TError>
trackResult(result: QueryObserverResult<TData, TError>): QueryObserverResult<TData, TError>
getOptimisticResult(options: QueryObserverOptions<TQueryFnData, TError, TData, TQueryKey>): QueryObserverResult<TData, TError>
updateResult(): void
setOptions(options: QueryObserverOptions<TQueryFnData, TError, TData, TQueryKey>): void
destroy(): void
}Observer for infinite/paginated queries
class InfiniteQueryObserver<TQueryFnData, TError, TData, TQueryKey extends QueryKey, TPageParam> {
constructor(client: QueryClient, options: InfiniteQueryObserverOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>)
subscribe(listener?: (result: InfiniteQueryObserverResult<TData, TError>) => void): () => void
getCurrentResult(): InfiniteQueryObserverResult<TData, TError>
// ... similar methods to QueryObserver
}Observer for individual mutations
class MutationObserver<TData, TError, TVariables, TContext> {
constructor(client: QueryClient, options: MutationObserverOptions<TData, TError, TVariables, TContext>)
subscribe(listener?: (result: MutationObserverResult<TData, TError, TVariables, TContext>) => void): () => void
getCurrentResult(): MutationObserverResult<TData, TError, TVariables, TContext>
mutate(variables: TVariables, options?: MutateOptions<TData, TError, TVariables, TContext>): Promise<TData>
reset(): void
destroy(): void
}Serializes QueryClient state for SSR
function dehydrate(
client: QueryClient,
options?: DehydrateOptions
): DehydratedState
interface DehydrateOptions {
shouldDehydrateMutation?: (mutation: Mutation) => boolean
shouldDehydrateQuery?: (query: Query) => boolean
}Restores QueryClient state from serialized data
function hydrate(
client: QueryClient,
dehydratedState: DehydratedState,
options?: HydrateOptions
): void
interface HydrateOptions {
defaultOptions?: DefaultOptions
}Default filter for query dehydration
function defaultShouldDehydrateQuery(query: Query): booleanDefault filter for mutation dehydration
function defaultShouldDehydrateMutation(mutation: Mutation): booleanExample:
import {
dehydrate,
hydrate,
defaultShouldDehydrateQuery,
defaultShouldDehydrateMutation
} from '@tanstack/react-query'
// Custom dehydration with selective queries
export async function getServerSideProps() {
const queryClient = new QueryClient()
// Prefetch data
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: fetchPosts
})
return {
props: {
dehydratedState: dehydrate(queryClient, {
shouldDehydrateQuery: (query) => {
// Use default logic plus custom conditions
return defaultShouldDehydrateQuery(query) &&
!query.queryKey.includes('sensitive')
}
})
}
}
}// ❌ Don't create a new QueryClient on every render
function App() {
return (
<QueryClientProvider client={new QueryClient()}>
{/* This creates a new client on every render */}
</QueryClientProvider>
)
}
// ✅ Create QueryClient outside component or use useState
const queryClient = new QueryClient()
function App() {
return (
<QueryClientProvider client={queryClient}>
{/* QueryClient is stable across renders */}
</QueryClientProvider>
)
}
// ✅ Or use useState for client-side apps
function App() {
const [queryClient] = useState(() => new QueryClient())
return (
<QueryClientProvider client={queryClient}>
{/* QueryClient is created once and stable */}
</QueryClientProvider>
)
}function App() {
return (
<QueryClientProvider client={queryClient}>
{/* Global error boundary for unrecoverable errors */}
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
fallbackRender={GlobalErrorFallback}
>
<Router>
<Routes>
<Route path="/*" element={
/* Page-level error boundaries for recoverable errors */
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
fallbackRender={PageErrorFallback}
>
<PageContent />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
} />
</Routes>
</Router>
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
</QueryClientProvider>
)
}// ✅ Use the useQueryClient hook instead of context directly
function MyComponent() {
const queryClient = useQueryClient()
// Hook provides better error messages and type safety
}
// ❌ Don't use context directly unless necessary
function MyComponent() {
const queryClient = useContext(QueryClientContext)
// Manual error checking required
if (!queryClient) throw new Error('...')
}The provider and context system in React Query offers a robust foundation for managing query state, error handling, and SSR hydration across your entire React application.
Install with Tessl CLI
npx tessl i tessl/npm-tanstack--react-query