queryOptions example with multiple query hooks
The queryOptions helper returns an object that can be used with useQuery, useSuspenseQuery, useQueries, queryClient.prefetchQuery, and queryClient.setQueryData. Example: function groupOptions(id: number) { return queryOptions({ queryKey: ['groups', id], queryFn: () => fetchGroups(id), staleTime: 5 * 1000 }); } useQuery(groupOptions(1)); useSuspenseQuery(groupOptions(5)); useQueries({ queries: [groupOptions(1), groupOptions(2)] }); queryClient.prefetchQuery(groupOptions(23)); queryClient.setQueryData(groupOptions(42).queryKey, newGroups);
Query keys act as dependencies for query functions
Query keys act as dependencies for your query functions. Adding dependent variables to your query key will ensure that queries are cached independently and that queries will be refetched automatically when a variable changes, depending on staleTime settings. See the exhaustive-deps ESLint plugin documentation for more information.
Resources for organizing query keys
For tips on organizing query keys in larger applications, refer to Effective React Query Keys blog post by tkdodo and the Query Key Factory Package from Community Resources.
Simple query keys for generic resources
Simple query keys are arrays with constant values. This format is useful for generic List/Index resources and non-hierarchical resources. Examples include useQuery({ queryKey: ['todos'], ... }) for a list of todos or useQuery({ queryKey: ['something', 'special'], ... }) for other resources.
Query keys with variables for hierarchical resources
When a query needs more information to uniquely describe its data, use an array with a string and any number of serializable objects. This is useful for hierarchical or nested resources where you pass an ID, index, or other primitive to uniquely identify an item, and for queries with additional parameters where you pass an object of additional options. Examples: useQuery({ queryKey: ['todo', 5], ... }) for an individual todo, useQuery({ queryKey: ['todo', 5, { preview: true }], ... }) for a todo in preview format, and useQuery({ queryKey: ['todos', { type: 'done' }], ... }) for filtered todos.
Query keys are hashed deterministically with object key order independence
Query keys are hashed deterministically, meaning the order of keys within objects does not matter for equality. useQuery({ queryKey: ['todos', { status, page }], ... }), useQuery({ queryKey: ['todos', { page, status }], ... }), and useQuery({ queryKey: ['todos', { page, status, other: undefined }], ... }) are all considered equal. However, array item order does matter: useQuery({ queryKey: ['todos', status, page], ... }) and useQuery({ queryKey: ['todos', page, status], ... }) are not equal.
Include query function variables in query key
If your query function depends on a variable, include it in your query key. Query keys should include any variables used in the query function that change. This ensures queries are cached independently, and that any time a variable changes, queries will be refetched automatically depending on staleTime settings. Example: function Todos({ todoId }) { const result = useQuery({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId), }) }
Query keys must be arrays at top level
Query keys in TanStack Query must be an Array at the top level. They can be as simple as an array with a single string, or as complex as an array of many strings and nested objects. As long as the query key is serializable using JSON.stringify and unique to the query's data, it can be used.
select operates on cached data, not for error handling
The select function operates on successfully cached data and is not appropriate for throwing errors. The source of truth for errors is the queryFn. If a select function returns an error, data becomes undefined but isSuccess remains true. Handle errors in the queryFn or outside the query hook instead.
select function memoization requirements
The select function only re-runs when the select function itself changes referentially or when data changes. An inlined select function will run on every render. Wrap it in useCallback or extract it to a stable function reference to avoid unnecessary re-runs.
Example: select with useCallback for memoization
To memoize a select function, wrap it in useCallback:
export const useTodoCount = () => {
return useTodos(useCallback((data) => data.length, []))
}
Example: custom hook with select option
Create a reusable custom hook that accepts a select option:
export const useTodos = (select) => {
return useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select,
})
}
export const useTodoCount = () => {
return useTodos((data) => data.length)
}
Proxy optimization disabled with object rest destructuring
Using object rest destructuring disables the proxy-based tracked properties optimization, as it bypasses the proxy's get trap. React Query provides a lint rule (no-rest-destructuring) to guard against this pitfall. Access properties via direct access or selective destructuring to maintain the optimization.
Tracked properties prevent unnecessary re-renders
React Query uses Proxy objects to track which properties returned from useQuery are actually used in a component. Only properties that are accessed trigger re-renders, avoiding unnecessary re-renders from properties like isFetching or isStale that change frequently but are unused. This behavior can be customized with the notifyOnChangeProps option, set globally or per-query. Setting notifyOnChangeProps: 'all' disables this optimization.
Top-level hook return objects are not referentially stable
The top-level object returned from useQuery, useInfiniteQuery, useMutation, and the array returned from useQueries is not referentially stable and will be a new reference on every render. However, the data properties returned from these hooks are as stable as possible.
Structural sharing keeps data references stable
React Query uses structural sharing to maintain referential integrity between re-renders. If fetched data has no changes, the original reference is kept. If only a subset changed, React Query preserves unchanged parts and only replaces the changed parts. This optimization only works with JSON-compatible data and can be disabled by setting structuralSharing: false globally or per-query, or by implementing custom structural sharing via a function.
Example: select with stable function reference
Extract the select function to a stable reference outside the component:
const selectTodoCount = (data) => data.length
export const useTodoCount = () => {
return useTodos(selectTodoCount)
}
select option subscribes to data subset
The select option in useQuery allows subscribing to a subset of data, useful for optimized data transformations or avoiding unnecessary re-renders. A component using select will only re-render when the selected subset changes, not when other parts of the data change.
Background retry behavior with refetchInterval
When using refetchInterval with refetchIntervalInBackground: true, retries will pause when the browser tab is inactive because retries respect the same focus behavior as regular refetches.
Default retry count for queries
When a query fails, TanStack Query will automatically retry the query up to 3 times by default before showing the final error.
retry option: false disables retries
Setting retry = false will disable all retry attempts for a query.
retry option: number of retries
Setting retry = 6 will retry failing requests 6 times before showing the final error thrown by the function.
retry option: infinite retries
Setting retry = true will infinitely retry failing requests.
retry option: custom function
Setting retry = (failureCount, error) => ... allows for custom logic to determine if a retry should be attempted based on the failure count and error. The failureCount starts at 0 for the first retry attempt.
error vs failureReason in retry attempts
The contents of the error property will be part of the failureReason response property of useQuery until the last retry attempt. Only after the final retry attempt, if the error persists, will the error be moved to the error property.
Default retryDelay exponential backoff
By default, retries use exponential backoff starting at 1000ms and doubling with each attempt, but not exceeding 30 seconds. The formula is: Math.min(1000 * 2 ** attemptIndex, 30000).
retryDelay as function for global configuration
The retryDelay can be configured as a function in defaultOptions for all queries. Example: retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000).
retryDelay as fixed integer
The retryDelay can be set to an integer for a fixed delay. If set as an integer instead of a function, the delay will always be the same amount of time regardless of retry count.
Continuous background retries workaround
To enable continuous retries in the background, consider disabling built-in retries with retry: false and implementing a custom refetch strategy using refetchInterval with a function that returns different intervals based on query status, combined with refetchIntervalInBackground: true.
Example: useQuery with retry count
const result = useQuery({
queryKey: ['todos', 1],
queryFn: fetchTodoListPage,
retry: 10,
})
This example sets a specific query to retry failed requests 10 times before displaying an error.
Example: configure retryDelay globally
import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
},
},
})
function App() {
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}
This example shows how to configure the retryDelay function globally for all queries.
Example: fixed retryDelay on individual query
const result = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList,
retryDelay: 1000,
})
This example sets a fixed 1000ms delay between retries on an individual query, regardless of how many retries have occurred.
Example: background refetch with custom retry timing
const result = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
refetchInterval: (query) => {
return query.state.status === 'error' ? 5000 : 30000
},
refetchIntervalInBackground: true,
retry: false,
})
This example disables built-in retries and uses a custom refetch strategy that refetches every 5 seconds when in error state and every 30 seconds otherwise, continuing in the background.
What is a request waterfall
A request waterfall occurs when a request for a resource does not start until after another request has finished. Each waterfall level represents at least one roundtrip to the server (unless the resource is locally cached). Request waterfalls are especially problematic on high-latency networks; for example, a triple waterfall (4 roundtrips) with 250ms latency results in 1000ms latency just for roundtrips, compared to 500ms for a flattened waterfall with 2 roundtrips.
Browser devtools Network tab for analyzing waterfalls
The best way to spot and analyze request waterfalls is by opening the browser's devtools Network tab.
Dependent queries cause request waterfalls
When a single component first fetches one query and then another query that depends on the result of the first query, this creates a request waterfall. The second query cannot execute until the first query completes. This is a Single Component Waterfall or Serial Query pattern.
Flatten dependent queries by restructuring the API
To avoid request waterfalls caused by dependent queries, restructure your API so you can fetch both pieces of data in a single query. For example, instead of fetching getUserByEmail first and then getProjectsByUser, create a getProjectsByUserEmail query that combines both operations.
useSuspenseQueries executes queries in parallel
When using multiple useSuspenseQuery hooks in a single component, the queries execute serially (one after another), causing separate roundtrips to the server. To fetch multiple queries in parallel when using Suspense, use useSuspenseQueries with an array of query objects instead.
useQuery hooks execute in parallel automatically
Multiple useQuery hooks in the same component execute in parallel, not serially. The waterfall problem with Suspense is specific to useSuspenseQuery when multiple instances are called.
Nested component waterfalls
A nested component waterfall occurs when both a parent and child component contain queries, and the parent does not render the child until its query completes. This happens with both useQuery and useSuspenseQuery. If the child is not dependent on the parent's data but still waits for the parent query to complete, this creates an unnecessary waterfall.
Flatten nested component waterfalls by hoisting queries
When a child component's query does not depend on data from the parent but still waits for the parent to finish, hoist both queries to the parent component so they execute in parallel. The child can then conditionally render based on the parent's loading state.
Prefetch to flatten nested component waterfalls
Another way to flatten nested component waterfalls is to prefetch the child component's query in the parent component, or to prefetch both queries at the router level on page load or navigation.
Dependent nested component waterfalls
A dependent nested component waterfall occurs when a child component has a query that depends on data from its parent in two ways: the child may only render conditionally based on the parent's data, and the child's query requires an ID or other data passed down from the parent. This creates a multi-level waterfall that may not be trivially fixable by hoisting.
Refactor API to flatten dependent nested waterfalls
For dependent nested component waterfalls that cannot be flattened by hoisting, consider refactoring your API to include nested data in a single parent query. Alternatively, use Server Components to move the waterfall to the server where latency is lower.
Code splitting introduces additional waterfalls
Code splitting can introduce request waterfalls because lazy-loaded code chunks must be downloaded before components render. If a lazy-loaded component contains a query, this adds extra roundtrips. For example: markup, JS for parent component, parent query, JS for lazy child component, child query.
Hoist queries out of code-split components
For code-split components with queries, consider hoisting the query to the parent component and making it conditional, or adding conditional prefetching. This allows the query to execute in parallel with the code split bundle download. This is a tradeoff: data fetching code is included in the main bundle, but request waterfalls are reduced.
Request waterfalls are common performance concern
Request waterfalls are a very common and complex performance concern with many tradeoffs. It is easy to accidentally introduce them by adding queries to child components without realizing a parent already has a query, or vice versa. Regularly examine the Network tab to spot high-impact waterfalls.
React Native focus management with AppState
In React Native, use the AppState module's 'change' event to trigger focus updates. Call focusManager.setFocused(status === 'active') in the AppState change listener to update the focus state based on the app state. This replaces the window event listener approach used in web browsers.
Manual focus state control with focusManager.setFocused
Use focusManager.setFocused(boolean) to manually override the focus state. Pass true to set focused, false to set unfocused, or undefined to fallback to the default focus check.
Window focus refetching automatic behavior
TanStack Query automatically requests fresh data in the background when a user leaves the application and returns if the query data is stale. This behavior is enabled by default.
Disable window focus refetching globally
Set refetchOnWindowFocus to false in the defaultOptions.queries configuration when creating a QueryClient. The default value is true. Example: const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false } } })
Disable window focus refetching per-query
Set refetchOnWindowFocus to false in the useQuery options for individual queries. Example: useQuery({ queryKey: ['todos'], queryFn: fetchTodos, refetchOnWindowFocus: false })
Custom window focus event listener
Use focusManager.setEventListener to manage custom window focus events. Pass a callback function that receives a handleFocus callback to fire when the window is focused. The previously set handler is removed and replaced with the new handler. The default implementation listens to the visibilitychange event and calls handleFocus(document.visibilityState === 'visible'). Remember to return an unsubscribe function that removes the event listener.
Custom window focus event default implementation
The default focusManager.setEventListener handler listens to the 'visibilitychange' event on window and calls handleFocus(true) when document.visibilityState equals 'visible', and handleFocus(false) otherwise. It returns an unsubscribe function that removes the event listener.
TanStack Query works out-of-the-box with zero-config
TanStack Query works amazingly well out-of-the-box with zero-config and can be customized to your liking as your application grows.
useQuery hook returns isPending, error, and data
The useQuery hook returns an object with isPending (boolean indicating if the query is loading), error (contains error information if the query fails), and data (contains the fetched data on success).
TanStack Query definition and purpose
TanStack Query (formerly known as React Query) is a data-fetching library that makes fetching, caching, synchronizing and updating server state in web applications easier.
Server state characteristics
Server state is persisted remotely in a location you may not control or own, requires asynchronous APIs for fetching and updating, implies shared ownership and can be changed by other people without your knowledge, and can potentially become out of date in your applications if you are not careful.
Key server state management challenges
Server state management challenges include caching, deduping multiple requests for the same data into a single request, updating out of date data in the background, knowing when data is out of date, reflecting updates to data as quickly as possible, performance optimizations like pagination and lazy loading data, managing memory and garbage collection of server state, and memoizing query results with structural sharing.
Basic TanStack Query example with useQuery
This example demonstrates how to fetch GitHub repository data using TanStack Query. It creates a QueryClient, wraps the app with QueryClientProvider, and uses the useQuery hook with a queryKey of ['repoData'] and a queryFn that fetches from the GitHub API. The hook returns isPending, error, and data properties that can be used to handle loading, error, and success states.