W↓
All docs
🔑
Sign Up/Sign In
tanstack.com/query/latest/docs/framework/react/guides/
Public Link
Apr 16, 2025, 6:03:40 PM - complete - 1 MB
Apr 16, 2025, 6:03:40 PM - complete - 1 MB
Apr 16, 2025, 6:02:54 PM - complete - 8.2 kB
Apr 16, 2025, 6:02:54 PM - complete - 8.2 kB
Apr 16, 2025, 6:02:37 PM - complete -
Apr 16, 2025, 6:02:36 PM - complete -
Apr 16, 2025, 6:00:45 PM - complete -
Starting URLs:
https://tanstack.com/query/latest/docs/framework/react/overview
https://tanstack.com/query/latest/docs/framework/react/installation
https://tanstack.com/query/latest/docs/framework/react/guides/important-defaults
Crawl Prefixes:
https://tanstack.com/query/latest/docs/framework/react/guides/
Exclude Patterns:
https://tanstack.com/query/latest/docs/framework/react/guides/testing
https://tanstack.com/query/latest/docs/framework/react/guides/does-this-replace-client-state
https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-react-query-3
https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-react-query-4
https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5
## Page: https://tanstack.com/query/latest/docs/framework/react/overview TanStack Query (formerly known as React Query) is often described as the missing data-fetching library for web applications, but in more technical terms, it makes **fetching, caching, synchronizing and updating server state** in your web applications a breeze. ## Motivation Most core web frameworks **do not** come with an opinionated way of fetching or updating data in a holistic way. Because of this developers end up building either meta-frameworks which encapsulate strict opinions about data-fetching, or they invent their own ways of fetching data. This usually means cobbling together component-based state and side-effects, or using more general purpose state management libraries to store and provide asynchronous data throughout their apps. While most traditional state management libraries are great for working with client state, they are **not so great at working with async or server state**. This is because **server state is totally different**. For starters, 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 * Can potentially become "out of date" in your applications if you're not careful Once you grasp the nature of server state in your application, **even more challenges will arise** as you go, for example: * Caching... (possibly the hardest thing to do in programming) * 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 * Memoizing query results with structural sharing If you're not overwhelmed by that list, then that must mean that you've probably solved all of your server state problems already and deserve an award. However, if you are like a vast majority of people, you either have yet to tackle all or most of these challenges and we're only scratching the surface! TanStack Query is hands down one of the _best_ libraries for managing server state. It works amazingly well **out-of-the-box, with zero-config, and can be customized** to your liking as your application grows. TanStack Query allows you to defeat and overcome the tricky challenges and hurdles of _server state_ and control your app data before it starts to control you. On a more technical note, TanStack Query will likely: * Help you remove **many** lines of complicated and misunderstood code from your application and replace with just a handful of lines of TanStack Query logic. * Make your application more maintainable and easier to build new features without worrying about wiring up new server state data sources * Have a direct impact on your end-users by making your application feel faster and more responsive than ever before. * Potentially help you save on bandwidth and increase memory performance ## Enough talk, show me some code already! In the example below, you can see TanStack Query in its most basic and simple form being used to fetch the GitHub stats for the TanStack Query GitHub project itself: Open in StackBlitz tsx import { QueryClient, QueryClientProvider, useQuery, } from '@tanstack/react-query' const queryClient = new QueryClient() export default function App() { return ( <QueryClientProvider client={queryClient}> <Example /> </QueryClientProvider> ) } function Example() { const { isPending, error, data } = useQuery({ queryKey: ['repoData'], queryFn: () => fetch('https://api.github.com/repos/TanStack/query').then((res) => res.json(), ), }) if (isPending) return 'Loading...' if (error) return 'An error has occurred: ' + error.message return ( <div> <h1>{data.name}</h1> <p>{data.description}</p> <strong>👀 {data.subscribers_count}</strong>{' '} <strong>✨ {data.stargazers_count}</strong>{' '} <strong>🍴 {data.forks_count}</strong> </div> ) } import { QueryClient, QueryClientProvider, useQuery, } from '@tanstack/react-query' const queryClient = new QueryClient() export default function App() { return ( <QueryClientProvider client={queryClient}> <Example /> </QueryClientProvider> ) } function Example() { const { isPending, error, data } = useQuery({ queryKey: ['repoData'], queryFn: () => fetch('https://api.github.com/repos/TanStack/query').then((res) => res.json(), ), }) if (isPending) return 'Loading...' if (error) return 'An error has occurred: ' + error.message return ( <div> <h1>{data.name}</h1> <p>{data.description}</p> <strong>👀 {data.subscribers_count}</strong>{' '} <strong>✨ {data.stargazers_count}</strong>{' '} <strong>🍴 {data.forks_count}</strong> </div> ) } ## You talked me into it, so what now? * Consider taking the official TanStack Query Course (or buying it for your whole team!) * Learn TanStack Query at your own pace with our amazingly thorough Walkthrough Guide and API Reference --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/important-defaults Out of the box, TanStack Query is configured with **aggressive but sane** defaults. **Sometimes these defaults can catch new users off guard or make learning/debugging difficult if they are unknown by the user.** Keep them in mind as you continue to learn and use TanStack Query: * Query instances via useQuery or useInfiniteQuery by default **consider cached data as stale**. > To change this behavior, you can configure your queries both globally and per-query using the staleTime option. Specifying a longer staleTime means queries will not refetch their data as often * Stale queries are refetched automatically in the background when: * New instances of the query mount * The window is refocused * The network is reconnected * The query is optionally configured with a refetch interval > To change this functionality, you can use options like refetchOnMount, refetchOnWindowFocus, refetchOnReconnect and refetchInterval. * Query results that have no more active instances of useQuery, useInfiniteQuery or query observers are labeled as "inactive" and remain in the cache in case they are used again at a later time. * By default, "inactive" queries are garbage collected after **5 minutes**. > To change this, you can alter the default gcTime for queries to something other than 1000 \* 60 \* 5 milliseconds. * Queries that fail are **silently retried 3 times, with exponential backoff delay** before capturing and displaying an error to the UI. > To change this, you can alter the default retry and retryDelay options for queries to something other than 3 and the default exponential backoff function. * Query results by default are **structurally shared to detect if data has actually changed** and if not, **the data reference remains unchanged** to better help with value stabilization with regards to useMemo and useCallback. If this concept sounds foreign, then don't worry about it! 99.9% of the time you will not need to disable this and it makes your app more performant at zero cost to you. > Structural sharing only works with JSON-compatible values, any other value types will always be considered as changed. If you are seeing performance issues because of large responses for example, you can disable this feature with the config.structuralSharing flag. If you are dealing with non-JSON compatible values in your query responses and still want to detect if data has changed or not, you can provide your own custom function as config.structuralSharing to compute a value from the old and new responses, retaining references as required. ## Further Reading Have a look at the following articles from our Community Resources for further explanations of the defaults: * Practical React Query * React Query as a State Manager --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/queries ## Query Basics A query is a declarative dependency on an asynchronous source of data that is tied to a **unique key**. A query can be used with any Promise based method (including GET and POST methods) to fetch data from a server. If your method modifies data on the server, we recommend using Mutations instead. To subscribe to a query in your components or custom hooks, call the useQuery hook with at least: * A **unique key for the query** * A function that returns a promise that: * Resolves the data, or * Throws an error tsx import { useQuery } from '@tanstack/react-query' function App() { const info = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList }) } import { useQuery } from '@tanstack/react-query' function App() { const info = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList }) } The **unique key** you provide is used internally for refetching, caching, and sharing your queries throughout your application. The query result returned by useQuery contains all of the information about the query that you'll need for templating and any other usage of the data: tsx const result = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList }) const result = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList }) The result object contains a few very important states you'll need to be aware of to be productive. A query can only be in one of the following states at any given moment: * isPending or status === 'pending' - The query has no data yet * isError or status === 'error' - The query encountered an error * isSuccess or status === 'success' - The query was successful and data is available Beyond those primary states, more information is available depending on the state of the query: * error - If the query is in an isError state, the error is available via the error property. * data - If the query is in an isSuccess state, the data is available via the data property. * isFetching - In any state, if the query is fetching at any time (including background refetching) isFetching will be true. For **most** queries, it's usually sufficient to check for the isPending state, then the isError state, then finally, assume that the data is available and render the successful state: tsx function Todos() { const { isPending, isError, data, error } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) if (isPending) { return <span>Loading...</span> } if (isError) { return <span>Error: {error.message}</span> } // We can assume by this point that `isSuccess === true` return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) } function Todos() { const { isPending, isError, data, error } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) if (isPending) { return <span>Loading...</span> } if (isError) { return <span>Error: {error.message}</span> } // We can assume by this point that `isSuccess === true` return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) } If booleans aren't your thing, you can always use the status state as well: tsx function Todos() { const { status, data, error } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) if (status === 'pending') { return <span>Loading...</span> } if (status === 'error') { return <span>Error: {error.message}</span> } // also status === 'success', but "else" logic works, too return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) } function Todos() { const { status, data, error } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) if (status === 'pending') { return <span>Loading...</span> } if (status === 'error') { return <span>Error: {error.message}</span> } // also status === 'success', but "else" logic works, too return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) } TypeScript will also narrow the type of data correctly if you've checked for pending and error before accessing it. ### FetchStatus In addition to the status field, you will also get an additional fetchStatus property with the following options: * fetchStatus === 'fetching' - The query is currently fetching. * fetchStatus === 'paused' - The query wanted to fetch, but it is paused. Read more about this in the Network Mode guide. * fetchStatus === 'idle' - The query is not doing anything at the moment. ### Why two different states? Background refetches and stale-while-revalidate logic make all combinations for status and fetchStatus possible. For example: * a query in success status will usually be in idle fetchStatus, but it could also be in fetching if a background refetch is happening. * a query that mounts and has no data will usually be in pending status and fetching fetchStatus, but it could also be paused if there is no network connection. So keep in mind that a query can be in pending state without actually fetching data. As a rule of thumb: * The status gives information about the data: Do we have any or not? * The fetchStatus gives information about the queryFn: Is it running or not? ## Further Reading For an alternative way of performing status checks, have a look at the Community Resources. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/query-keys At its core, TanStack Query manages query caching for you based on query keys. Query keys have to be an Array at the top level, and 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, and **unique to the query's data**, you can use it! ## Simple Query Keys The simplest form of a key is an array with constants values. This format is useful for: * Generic List/Index resources * Non-hierarchical resources tsx // A list of todos useQuery({ queryKey: ['todos'], ... }) // Something else, whatever! useQuery({ queryKey: ['something', 'special'], ... }) // A list of todos useQuery({ queryKey: ['todos'], ... }) // Something else, whatever! useQuery({ queryKey: ['something', 'special'], ... }) ## Array Keys with variables When a query needs more information to uniquely describe its data, you can use an array with a string and any number of serializable objects to describe it. This is useful for: * Hierarchical or nested resources * It's common to pass an ID, index, or other primitive to uniquely identify the item * Queries with additional parameters * It's common to pass an object of additional options tsx // An individual todo useQuery({ queryKey: ['todo', 5], ... }) // An individual todo in a "preview" format useQuery({ queryKey: ['todo', 5, { preview: true }], ...}) // A list of todos that are "done" useQuery({ queryKey: ['todos', { type: 'done' }], ... }) // An individual todo useQuery({ queryKey: ['todo', 5], ... }) // An individual todo in a "preview" format useQuery({ queryKey: ['todo', 5, { preview: true }], ...}) // A list of todos that are "done" useQuery({ queryKey: ['todos', { type: 'done' }], ... }) ## Query Keys are hashed deterministically! This means that no matter the order of keys in objects, all of the following queries are considered equal: tsx useQuery({ queryKey: ['todos', { status, page }], ... }) useQuery({ queryKey: ['todos', { page, status }], ...}) useQuery({ queryKey: ['todos', { page, status, other: undefined }], ... }) useQuery({ queryKey: ['todos', { status, page }], ... }) useQuery({ queryKey: ['todos', { page, status }], ...}) useQuery({ queryKey: ['todos', { page, status, other: undefined }], ... }) The following query keys, however, are not equal. Array item order matters! tsx useQuery({ queryKey: ['todos', status, page], ... }) useQuery({ queryKey: ['todos', page, status], ...}) useQuery({ queryKey: ['todos', undefined, page, status], ...}) useQuery({ queryKey: ['todos', status, page], ... }) useQuery({ queryKey: ['todos', page, status], ...}) useQuery({ queryKey: ['todos', undefined, page, status], ...}) ## If your query function depends on a variable, include it in your query key Since query keys uniquely describe the data they are fetching, they should include any variables you use in your query function that **change**. For example: tsx function Todos({ todoId }) { const result = useQuery({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId), }) } function Todos({ todoId }) { const result = useQuery({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId), }) } Note that 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 any time a variable changes, _queries will be refetched automatically_ (depending on your staleTime settings). See the exhaustive-deps section for more information and examples. ## Further reading For tips on organizing Query Keys in larger applications, have a look at Effective React Query Keys and check the Query Key Factory Package from the Community Resources. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/query-functions A query function can be literally any function that **returns a promise**. The promise that is returned should either **resolve the data** or **throw an error**. All of the following are valid query function configurations: tsx useQuery({ queryKey: ['todos'], queryFn: fetchAllTodos }) useQuery({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId) }) useQuery({ queryKey: ['todos', todoId], queryFn: async () => { const data = await fetchTodoById(todoId) return data }, }) useQuery({ queryKey: ['todos', todoId], queryFn: ({ queryKey }) => fetchTodoById(queryKey[1]), }) useQuery({ queryKey: ['todos'], queryFn: fetchAllTodos }) useQuery({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId) }) useQuery({ queryKey: ['todos', todoId], queryFn: async () => { const data = await fetchTodoById(todoId) return data }, }) useQuery({ queryKey: ['todos', todoId], queryFn: ({ queryKey }) => fetchTodoById(queryKey[1]), }) ## Handling and Throwing Errors For TanStack Query to determine a query has errored, the query function **must throw** or return a **rejected Promise**. Any error that is thrown in the query function will be persisted on the error state of the query. tsx const { error } = useQuery({ queryKey: ['todos', todoId], queryFn: async () => { if (somethingGoesWrong) { throw new Error('Oh no!') } if (somethingElseGoesWrong) { return Promise.reject(new Error('Oh no!')) } return data }, }) const { error } = useQuery({ queryKey: ['todos', todoId], queryFn: async () => { if (somethingGoesWrong) { throw new Error('Oh no!') } if (somethingElseGoesWrong) { return Promise.reject(new Error('Oh no!')) } return data }, }) ## Usage with fetch and other clients that do not throw by default While most utilities like axios or graphql-request automatically throw errors for unsuccessful HTTP calls, some utilities like fetch do not throw errors by default. If that's the case, you'll need to throw them on your own. Here is a simple way to do that with the popular fetch API: tsx useQuery({ queryKey: ['todos', todoId], queryFn: async () => { const response = await fetch('/todos/' + todoId) if (!response.ok) { throw new Error('Network response was not ok') } return response.json() }, }) useQuery({ queryKey: ['todos', todoId], queryFn: async () => { const response = await fetch('/todos/' + todoId) if (!response.ok) { throw new Error('Network response was not ok') } return response.json() }, }) ## Query Function Variables Query keys are not just for uniquely identifying the data you are fetching, but are also conveniently passed into your query function as part of the QueryFunctionContext. While not always necessary, this makes it possible to extract your query functions if needed: tsx function Todos({ status, page }) { const result = useQuery({ queryKey: ['todos', { status, page }], queryFn: fetchTodoList, }) } // Access the key, status and page variables in your query function! function fetchTodoList({ queryKey }) { const [_key, { status, page }] = queryKey return new Promise() } function Todos({ status, page }) { const result = useQuery({ queryKey: ['todos', { status, page }], queryFn: fetchTodoList, }) } // Access the key, status and page variables in your query function! function fetchTodoList({ queryKey }) { const [_key, { status, page }] = queryKey return new Promise() } ### QueryFunctionContext The QueryFunctionContext is the object passed to each query function. It consists of: * queryKey: QueryKey: Query Keys * client: QueryClient: QueryClient * signal?: AbortSignal * AbortSignal instance provided by TanStack Query * Can be used for Query Cancellation * meta: Record<string, unknown> | undefined * an optional field you can fill with additional information about your query Additionally, Infinite Queries get the following options passed: * pageParam: TPageParam * the page parameter used to fetch the current page * direction: 'forward' | 'backward' * **deprecated** * the direction of the current page fetch * To get access to the direction of the current page fetch, please add a direction to pageParam from getNextPageParam and getPreviousPageParam. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/query-options One of the best ways to share queryKey and queryFn between multiple places, yet keep them co-located to one another, is to use the queryOptions helper. At runtime, this helper just returns whatever you pass into it, but it has a lot of advantages when using it with TypeScript. You can define all possible options for a query in one place, and you'll also get type inference and type safety for all of them. ts import { queryOptions } from '@tanstack/react-query' function groupOptions(id: number) { return queryOptions({ queryKey: ['groups', id], queryFn: () => fetchGroups(id), staleTime: 5 * 1000, }) } // usage: useQuery(groupOptions(1)) useSuspenseQuery(groupOptions(5)) useQueries({ queries: [groupOptions(1), groupOptions(2)], }) queryClient.prefetchQuery(groupOptions(23)) queryClient.setQueryData(groupOptions(42).queryKey, newGroups) import { queryOptions } from '@tanstack/react-query' function groupOptions(id: number) { return queryOptions({ queryKey: ['groups', id], queryFn: () => fetchGroups(id), staleTime: 5 * 1000, }) } // usage: useQuery(groupOptions(1)) useSuspenseQuery(groupOptions(5)) useQueries({ queries: [groupOptions(1), groupOptions(2)], }) queryClient.prefetchQuery(groupOptions(23)) queryClient.setQueryData(groupOptions(42).queryKey, newGroups) For Infinite Queries, a separate infiniteQueryOptions helper is available. You can still override some options at the component level. A very common and useful pattern is to create per-component select functions: ts // Type inference still works, so query.data will be the return type of select instead of queryFn const query = useQuery({ ...groupOptions(1), select: (data) => data.groupName, }) // Type inference still works, so query.data will be the return type of select instead of queryFn const query = useQuery({ ...groupOptions(1), select: (data) => data.groupName, }) --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/network-mode TanStack Query provides three different network modes to distinguish how Queries and Mutations should behave if you have no network connection. This mode can be set for each Query / Mutation individually, or globally via the query / mutation defaults. Since TanStack Query is most often used for data fetching in combination with data fetching libraries, the default network mode is online. ## Network Mode: online In this mode, Queries and Mutations will not fire unless you have network connection. This is the default mode. If a fetch is initiated for a query, it will always stay in the state (pending, error, success) it is in if the fetch cannot be made because there is no network connection. However, a fetchStatus is exposed additionally. This can be either: * fetching: The queryFn is really executing - a request is in-flight. * paused: The query is not executing - it is paused until you have connection again * idle: The query is not fetching and not paused The flags isFetching and isPaused are derived from this state and exposed for convenience. > Keep in mind that it might not be enough to check for pending state to show a loading spinner. Queries can be in state: 'pending', but fetchStatus: 'paused' if they are mounting for the first time, and you have no network connection. If a query runs because you are online, but you go offline while the fetch is still happening, TanStack Query will also pause the retry mechanism. Paused queries will then continue to run once you re-gain network connection. This is independent of refetchOnReconnect (which also defaults to true in this mode), because it is not a refetch, but rather a continue. If the query has been cancelled in the meantime, it will not continue. ## Network Mode: always In this mode, TanStack Query will always fetch and ignore the online / offline state. This is likely the mode you want to choose if you use TanStack Query in an environment where you don't need an active network connection for your Queries to work - e.g. if you just read from AsyncStorage, or if you just want to return Promise.resolve(5) from your queryFn. * Queries will never be paused because you have no network connection. * Retries will also not pause - your Query will go to error state if it fails. * refetchOnReconnect defaults to false in this mode, because reconnecting to the network is not a good indicator anymore that stale queries should be refetched. You can still turn it on if you want. ## Network Mode: offlineFirst This mode is the middle ground between the first two options, where TanStack Query will run the queryFn once, but then pause retries. This is very handy if you have a serviceWorker that intercepts a request for caching like in an offline-first PWA, or if you use HTTP caching via the Cache-Control header. In those situations, the first fetch might succeed because it comes from an offline storage / cache. However, if there is a cache miss, the network request will go out and fail, in which case this mode behaves like an online query - pausing retries. ## Devtools The TanStack Query Devtools will show Queries in a paused state if they would be fetching, but there is no network connection. There is also a toggle button to _Mock offline behavior_. Please note that this button will _not_ actually mess with your network connection (you can do that in the browser devtools), but it will set the OnlineManager in an offline state. ## Signature * networkMode: 'online' | 'always' | 'offlineFirst' * optional * defaults to 'online' --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/parallel-queries "Parallel" queries are queries that are executed in parallel, or at the same time so as to maximize fetching concurrency. ## Manual Parallel Queries When the number of parallel queries does not change, there is **no extra effort** to use parallel queries. Just use any number of TanStack Query's useQuery and useInfiniteQuery hooks side-by-side! tsx function App () { // The following queries will execute in parallel const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers }) const teamsQuery = useQuery({ queryKey: ['teams'], queryFn: fetchTeams }) const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: fetchProjects }) ... } function App () { // The following queries will execute in parallel const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers }) const teamsQuery = useQuery({ queryKey: ['teams'], queryFn: fetchTeams }) const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: fetchProjects }) ... } > When using React Query in suspense mode, this pattern of parallelism does not work, since the first query would throw a promise internally and would suspend the component before the other queries run. To get around this, you'll either need to use the useSuspenseQueries hook (which is suggested) or orchestrate your own parallelism with separate components for each useSuspenseQuery instance. ## Dynamic Parallel Queries with useQueries If the number of queries you need to execute is changing from render to render, you cannot use manual querying since that would violate the rules of hooks. Instead, TanStack Query provides a useQueries hook, which you can use to dynamically execute as many queries in parallel as you'd like. useQueries accepts an **options object** with a **queries key** whose value is an **array of query objects**. It returns an **array of query results**: tsx function App({ users }) { const userQueries = useQueries({ queries: users.map((user) => { return { queryKey: ['user', user.id], queryFn: () => fetchUserById(user.id), } }), }) } function App({ users }) { const userQueries = useQueries({ queries: users.map((user) => { return { queryKey: ['user', user.id], queryFn: () => fetchUserById(user.id), } }), }) } --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/dependent-queries ## useQuery dependent Query Dependent (or serial) queries depend on previous ones to finish before they can execute. To achieve this, it's as easy as using the enabled option to tell a query when it is ready to run: tsx // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) The projects query will start in: tsx status: 'pending' isPending: true fetchStatus: 'idle' status: 'pending' isPending: true fetchStatus: 'idle' As soon as the user is available, the projects query will be enabled and will then transition to: tsx status: 'pending' isPending: true fetchStatus: 'fetching' status: 'pending' isPending: true fetchStatus: 'fetching' Once we have the projects, it will go to: tsx status: 'success' isPending: false fetchStatus: 'idle' status: 'success' isPending: false fetchStatus: 'idle' ## useQueries dependent Query Dynamic parallel query - useQueries can depend on a previous query also, here's how to achieve this: tsx // Get the users ids const { data: userIds } = useQuery({ queryKey: ['users'], queryFn: getUsersData, select: (users) => users.map((user) => user.id), }) // Then get the users messages const usersMessages = useQueries({ queries: userIds ? userIds.map((id) => { return { queryKey: ['messages', id], queryFn: () => getMessagesByUsers(id), } }) : [], // if users is undefined, an empty array will be returned }) // Get the users ids const { data: userIds } = useQuery({ queryKey: ['users'], queryFn: getUsersData, select: (users) => users.map((user) => user.id), }) // Then get the users messages const usersMessages = useQueries({ queries: userIds ? userIds.map((id) => { return { queryKey: ['messages', id], queryFn: () => getMessagesByUsers(id), } }) : [], // if users is undefined, an empty array will be returned }) **Note** that useQueries return an **array of query results** ## A note about performance Dependent queries by definition constitutes a form of request waterfall, which hurts performance. If we pretend both queries take the same amount of time, doing them serially instead of in parallel always takes twice as much time, which is especially hurtful when it happens on a client that has high latency. If you can, it's always better to restructure the backend APIs so that both queries can be fetched in parallel, though that might not always be practically feasible. In the example above, instead of first fetching getUserByEmail to be able to getProjectsByUser, introducing a new getProjectsByUserEmail query would flatten the waterfall. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/background-fetching-indicators A query's status === 'pending' state is sufficient enough to show the initial hard-loading state for a query, but sometimes you may want to display an additional indicator that a query is refetching in the background. To do this, queries also supply you with an isFetching boolean that you can use to show that it's in a fetching state, regardless of the state of the status variable: tsx function Todos() { const { status, data: todos, error, isFetching, } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, }) return status === 'pending' ? ( <span>Loading...</span> ) : status === 'error' ? ( <span>Error: {error.message}</span> ) : ( <> {isFetching ? <div>Refreshing...</div> : null} <div> {todos.map((todo) => ( <Todo todo={todo} /> ))} </div> </> ) } function Todos() { const { status, data: todos, error, isFetching, } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, }) return status === 'pending' ? ( <span>Loading...</span> ) : status === 'error' ? ( <span>Error: {error.message}</span> ) : ( <> {isFetching ? <div>Refreshing...</div> : null} <div> {todos.map((todo) => ( <Todo todo={todo} /> ))} </div> </> ) } ## Displaying Global Background Fetching Loading State In addition to individual query loading states, if you would like to show a global loading indicator when **any** queries are fetching (including in the background), you can use the useIsFetching hook: tsx import { useIsFetching } from '@tanstack/react-query' function GlobalLoadingIndicator() { const isFetching = useIsFetching() return isFetching ? ( <div>Queries are fetching in the background...</div> ) : null } import { useIsFetching } from '@tanstack/react-query' function GlobalLoadingIndicator() { const isFetching = useIsFetching() return isFetching ? ( <div>Queries are fetching in the background...</div> ) : null } --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/window-focus-refetching If a user leaves your application and returns and the query data is stale, **TanStack Query automatically requests fresh data for you in the background**. You can disable this globally or per-query using the refetchOnWindowFocus option: #### Disabling Globally tsx // const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, // default: true }, }, }) function App() { return <QueryClientProvider client={queryClient}>...</QueryClientProvider> } // const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, // default: true }, }, }) function App() { return <QueryClientProvider client={queryClient}>...</QueryClientProvider> } #### Disabling Per-Query tsx useQuery({ queryKey: ['todos'], queryFn: fetchTodos, refetchOnWindowFocus: false, }) useQuery({ queryKey: ['todos'], queryFn: fetchTodos, refetchOnWindowFocus: false, }) ## Custom Window Focus Event In rare circumstances, you may want to manage your own window focus events that trigger TanStack Query to revalidate. To do this, TanStack Query provides a focusManager.setEventListener function that supplies you the callback that should be fired when the window is focused and allows you to set up your own events. When calling focusManager.setEventListener, the previously set handler is removed (which in most cases will be the default handler) and your new handler is used instead. For example, this is the default handler: tsx focusManager.setEventListener((handleFocus) => { // Listen to visibilitychange if (typeof window !== 'undefined' && window.addEventListener) { const visibilitychangeHandler = () => { handleFocus(document.visibilityState === 'visible') } window.addEventListener('visibilitychange', visibilitychangeHandler, false) return () => { // Be sure to unsubscribe if a new handler is set window.removeEventListener('visibilitychange', visibilitychangeHandler) } } }) focusManager.setEventListener((handleFocus) => { // Listen to visibilitychange if (typeof window !== 'undefined' && window.addEventListener) { const visibilitychangeHandler = () => { handleFocus(document.visibilityState === 'visible') } window.addEventListener('visibilitychange', visibilitychangeHandler, false) return () => { // Be sure to unsubscribe if a new handler is set window.removeEventListener('visibilitychange', visibilitychangeHandler) } } }) ## Managing Focus in React Native Instead of event listeners on window, React Native provides focus information through the AppState module. You can use the AppState "change" event to trigger an update when the app state changes to "active": tsx import { AppState } from 'react-native' import { focusManager } from '@tanstack/react-query' function onAppStateChange(status: AppStateStatus) { if (Platform.OS !== 'web') { focusManager.setFocused(status === 'active') } } useEffect(() => { const subscription = AppState.addEventListener('change', onAppStateChange) return () => subscription.remove() }, []) import { AppState } from 'react-native' import { focusManager } from '@tanstack/react-query' function onAppStateChange(status: AppStateStatus) { if (Platform.OS !== 'web') { focusManager.setFocused(status === 'active') } } useEffect(() => { const subscription = AppState.addEventListener('change', onAppStateChange) return () => subscription.remove() }, []) ## Managing focus state tsx import { focusManager } from '@tanstack/react-query' // Override the default focus state focusManager.setFocused(true) // Fallback to the default focus check focusManager.setFocused(undefined) import { focusManager } from '@tanstack/react-query' // Override the default focus state focusManager.setFocused(true) // Fallback to the default focus check focusManager.setFocused(undefined) --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/disabling-queries If you ever want to disable a query from automatically running, you can use the enabled = false option. The enabled option also accepts a callback that returns a boolean. When enabled is false: * If the query has cached data, then the query will be initialized in the status === 'success' or isSuccess state. * If the query does not have cached data, then the query will start in the status === 'pending' and fetchStatus === 'idle' state. * The query will not automatically fetch on mount. * The query will not automatically refetch in the background. * The query will ignore query client invalidateQueries and refetchQueries calls that would normally result in the query refetching. * refetch returned from useQuery can be used to manually trigger the query to fetch. However, it will not work with skipToken. > TypeScript users may prefer to use skipToken as an alternative to enabled = false. tsx function Todos() { const { isLoading, isError, data, error, refetch, isFetching } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, enabled: false, }) return ( <div> <button onClick={() => refetch()}>Fetch Todos</button> {data ? ( <> <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> </> ) : isError ? ( <span>Error: {error.message}</span> ) : isLoading ? ( <span>Loading...</span> ) : ( <span>Not ready ...</span> )} <div>{isFetching ? 'Fetching...' : null}</div> </div> ) } function Todos() { const { isLoading, isError, data, error, refetch, isFetching } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, enabled: false, }) return ( <div> <button onClick={() => refetch()}>Fetch Todos</button> {data ? ( <> <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> </> ) : isError ? ( <span>Error: {error.message}</span> ) : isLoading ? ( <span>Loading...</span> ) : ( <span>Not ready ...</span> )} <div>{isFetching ? 'Fetching...' : null}</div> </div> ) } Permanently disabling a query opts out of many great features that TanStack Query has to offer (like background refetches), and it's also not the idiomatic way. It takes you from the declarative approach (defining dependencies when your query should run) into an imperative mode (fetch whenever I click here). It is also not possible to pass parameters to refetch. Oftentimes, all you want is a lazy query that defers the initial fetch: ## Lazy Queries The enabled option can not only be used to permanently disable a query, but also to enable / disable it at a later time. A good example would be a filter form where you only want to fire off the first request once the user has entered a filter value: tsx function Todos() { const [filter, setFilter] = React.useState('') const { data } = useQuery({ queryKey: ['todos', filter], queryFn: () => fetchTodos(filter), // ⬇️ disabled as long as the filter is empty enabled: !!filter, }) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data} />} </div> ) } function Todos() { const [filter, setFilter] = React.useState('') const { data } = useQuery({ queryKey: ['todos', filter], queryFn: () => fetchTodos(filter), // ⬇️ disabled as long as the filter is empty enabled: !!filter, }) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data} />} </div> ) } ### isLoading (Previously: isInitialLoading) Lazy queries will be in status: 'pending' right from the start because pending means that there is no data yet. This is technically true, however, since we are not currently fetching any data (as the query is not _enabled_), it also means you likely cannot use this flag to show a loading spinner. If you are using disabled or lazy queries, you can use the isLoading flag instead. It's a derived flag that is computed from: isPending && isFetching so it will only be true if the query is currently fetching for the first time. ## Typesafe disabling of queries using skipToken If you are using TypeScript, you can use the skipToken to disable a query. This is useful when you want to disable a query based on a condition, but you still want to keep the query to be type safe. > IMPORTANT: refetch from useQuery will not work with skipToken. Other than that, skipToken works the same as enabled: false. tsx import { skipToken, useQuery } from '@tanstack/react-query' function Todos() { const [filter, setFilter] = React.useState<string | undefined>() const { data } = useQuery({ queryKey: ['todos', filter], // ⬇️ disabled as long as the filter is undefined or empty queryFn: filter ? () => fetchTodos(filter) : skipToken, }) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data} />} </div> ) } import { skipToken, useQuery } from '@tanstack/react-query' function Todos() { const [filter, setFilter] = React.useState<string | undefined>() const { data } = useQuery({ queryKey: ['todos', filter], // ⬇️ disabled as long as the filter is undefined or empty queryFn: filter ? () => fetchTodos(filter) : skipToken, }) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data} />} </div> ) } --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/query-retries When a useQuery query fails (the query function throws an error), TanStack Query will automatically retry the query if that query's request has not reached the max number of consecutive retries (defaults to 3) or a function is provided to determine if a retry is allowed. You can configure retries both on a global level and an individual query level. * Setting retry = false will disable retries. * Setting retry = 6 will retry failing requests 6 times before showing the final error thrown by the function. * Setting retry = true will infinitely retry failing requests. * Setting retry = (failureCount, error) => ... allows for custom logic based on why the request failed. > On the server, retries default to 0 to make server rendering as fast as possible. tsx import { useQuery } from '@tanstack/react-query' // Make a specific query retry a certain number of times const result = useQuery({ queryKey: ['todos', 1], queryFn: fetchTodoListPage, retry: 10, // Will retry failed requests 10 times before displaying an error }) import { useQuery } from '@tanstack/react-query' // Make a specific query retry a certain number of times const result = useQuery({ queryKey: ['todos', 1], queryFn: fetchTodoListPage, retry: 10, // Will retry failed requests 10 times before displaying an error }) > Info: Contents of the error property will be part of failureReason response property of useQuery until the last retry attempt. So in above example any error contents will be part of failureReason property for first 9 retry attempts (Overall 10 attempts) and finally they will be part of error after last attempt if error persists after all retry attempts. ## Retry Delay By default, retries in TanStack Query do not happen immediately after a request fails. As is standard, a back-off delay is gradually applied to each retry attempt. The default retryDelay is set to double (starting at 1000ms) with each attempt, but not exceed 30 seconds: tsx // Configure for all queries 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> } // Configure for all queries 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> } Though it is not recommended, you can obviously override the retryDelay function/integer in both the Provider and individual query options. If set to an integer instead of a function the delay will always be the same amount of time: tsx const result = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, retryDelay: 1000, // Will always wait 1000ms to retry, regardless of how many retries }) const result = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, retryDelay: 1000, // Will always wait 1000ms to retry, regardless of how many retries }) --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/paginated-queries Rendering paginated data is a very common UI pattern and in TanStack Query, it "just works" by including the page information in the query key: tsx const result = useQuery({ queryKey: ['projects', page], queryFn: fetchProjects, }) const result = useQuery({ queryKey: ['projects', page], queryFn: fetchProjects, }) However, if you run this simple example, you might notice something strange: **The UI jumps in and out of the success and pending states because each new page is treated like a brand new query.** This experience is not optimal and unfortunately is how many tools today insist on working. But not TanStack Query! As you may have guessed, TanStack Query comes with an awesome feature called placeholderData that allows us to get around this. ## Better Paginated Queries with placeholderData Consider the following example where we would ideally want to increment a pageIndex (or cursor) for a query. If we were to use useQuery, **it would still technically work fine**, but the UI would jump in and out of the success and pending states as different queries are created and destroyed for each page or cursor. By setting placeholderData to (previousData) => previousData or keepPreviousData function exported from TanStack Query, we get a few new things: * **The data from the last successful fetch is available while new data is being requested, even though the query key has changed**. * When the new data arrives, the previous data is seamlessly swapped to show the new data. * isPlaceholderData is made available to know what data the query is currently providing you tsx import { keepPreviousData, useQuery } from '@tanstack/react-query' import React from 'react' function Todos() { const [page, setPage] = React.useState(0) const fetchProjects = (page = 0) => fetch('/api/projects?page=' + page).then((res) => res.json()) const { isPending, isError, error, data, isFetching, isPlaceholderData } = useQuery({ queryKey: ['projects', page], queryFn: () => fetchProjects(page), placeholderData: keepPreviousData, }) return ( <div> {isPending ? ( <div>Loading...</div> ) : isError ? ( <div>Error: {error.message}</div> ) : ( <div> {data.projects.map((project) => ( <p key={project.id}>{project.name}</p> ))} </div> )} <span>Current Page: {page + 1}</span> <button onClick={() => setPage((old) => Math.max(old - 1, 0))} disabled={page === 0} > Previous Page </button> <button onClick={() => { if (!isPlaceholderData && data.hasMore) { setPage((old) => old + 1) } }} // Disable the Next Page button until we know a next page is available disabled={isPlaceholderData || !data?.hasMore} > Next Page </button> {isFetching ? <span> Loading...</span> : null} </div> ) } import { keepPreviousData, useQuery } from '@tanstack/react-query' import React from 'react' function Todos() { const [page, setPage] = React.useState(0) const fetchProjects = (page = 0) => fetch('/api/projects?page=' + page).then((res) => res.json()) const { isPending, isError, error, data, isFetching, isPlaceholderData } = useQuery({ queryKey: ['projects', page], queryFn: () => fetchProjects(page), placeholderData: keepPreviousData, }) return ( <div> {isPending ? ( <div>Loading...</div> ) : isError ? ( <div>Error: {error.message}</div> ) : ( <div> {data.projects.map((project) => ( <p key={project.id}>{project.name}</p> ))} </div> )} <span>Current Page: {page + 1}</span> <button onClick={() => setPage((old) => Math.max(old - 1, 0))} disabled={page === 0} > Previous Page </button> <button onClick={() => { if (!isPlaceholderData && data.hasMore) { setPage((old) => old + 1) } }} // Disable the Next Page button until we know a next page is available disabled={isPlaceholderData || !data?.hasMore} > Next Page </button> {isFetching ? <span> Loading...</span> : null} </div> ) } ## Lagging Infinite Query results with placeholderData While not as common, the placeholderData option also works flawlessly with the useInfiniteQuery hook, so you can seamlessly allow your users to continue to see cached data while infinite query keys change over time. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries Rendering lists that can additively "load more" data onto an existing set of data or "infinite scroll" is also a very common UI pattern. TanStack Query supports a useful version of useQuery called useInfiniteQuery for querying these types of lists. When using useInfiniteQuery, you'll notice a few things are different: * data is now an object containing infinite query data: * data.pages array containing the fetched pages * data.pageParams array containing the page params used to fetch the pages * The fetchNextPage and fetchPreviousPage functions are now available (fetchNextPage is required) * The initialPageParam option is now available (and required) to specify the initial page param * The getNextPageParam and getPreviousPageParam options are available for both determining if there is more data to load and the information to fetch it. This information is supplied as an additional parameter in the query function * A hasNextPage boolean is now available and is true if getNextPageParam returns a value other than null or undefined * A hasPreviousPage boolean is now available and is true if getPreviousPageParam returns a value other than null or undefined * The isFetchingNextPage and isFetchingPreviousPage booleans are now available to distinguish between a background refresh state and a loading more state > Note: Options initialData or placeholderData need to conform to the same structure of an object with data.pages and data.pageParams properties. ## Example Let's assume we have an API that returns pages of projects 3 at a time based on a cursor index along with a cursor that can be used to fetch the next group of projects: tsx fetch('/api/projects?cursor=0') // { data: [...], nextCursor: 3} fetch('/api/projects?cursor=3') // { data: [...], nextCursor: 6} fetch('/api/projects?cursor=6') // { data: [...], nextCursor: 9} fetch('/api/projects?cursor=9') // { data: [...] } fetch('/api/projects?cursor=0') // { data: [...], nextCursor: 3} fetch('/api/projects?cursor=3') // { data: [...], nextCursor: 6} fetch('/api/projects?cursor=6') // { data: [...], nextCursor: 9} fetch('/api/projects?cursor=9') // { data: [...] } With this information, we can create a "Load More" UI by: * Waiting for useInfiniteQuery to request the first group of data by default * Returning the information for the next query in getNextPageParam * Calling fetchNextPage function tsx import { useInfiniteQuery } from '@tanstack/react-query' function Projects() { const fetchProjects = async ({ pageParam }) => { const res = await fetch('/api/projects?cursor=' + pageParam) return res.json() } const { data, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, status, } = useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, }) return status === 'pending' ? ( <p>Loading...</p> ) : status === 'error' ? ( <p>Error: {error.message}</p> ) : ( <> {data.pages.map((group, i) => ( <React.Fragment key={i}> {group.data.map((project) => ( <p key={project.id}>{project.name}</p> ))} </React.Fragment> ))} <div> <button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage} > {isFetchingNextPage ? 'Loading more...' : hasNextPage ? 'Load More' : 'Nothing more to load'} </button> </div> <div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div> </> ) } import { useInfiniteQuery } from '@tanstack/react-query' function Projects() { const fetchProjects = async ({ pageParam }) => { const res = await fetch('/api/projects?cursor=' + pageParam) return res.json() } const { data, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, status, } = useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, }) return status === 'pending' ? ( <p>Loading...</p> ) : status === 'error' ? ( <p>Error: {error.message}</p> ) : ( <> {data.pages.map((group, i) => ( <React.Fragment key={i}> {group.data.map((project) => ( <p key={project.id}>{project.name}</p> ))} </React.Fragment> ))} <div> <button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage} > {isFetchingNextPage ? 'Loading more...' : hasNextPage ? 'Load More' : 'Nothing more to load'} </button> </div> <div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div> </> ) } It's essential to understand that calling fetchNextPage while an ongoing fetch is in progress runs the risk of overwriting data refreshes happening in the background. This situation becomes particularly critical when rendering a list and triggering fetchNextPage simultaneously. Remember, there can only be a single ongoing fetch for an InfiniteQuery. A single cache entry is shared for all pages, attempting to fetch twice simultaneously might lead to data overwrites. If you intend to enable simultaneous fetching, you can utilize the { cancelRefetch: false } option (default: true) within fetchNextPage. To ensure a seamless querying process without conflicts, it's highly recommended to verify that the query is not in an isFetching state, especially if the user won't directly control that call. jsx <List onEndReached={() => !isFetchingNextPage && fetchNextPage()} /> <List onEndReached={() => !isFetchingNextPage && fetchNextPage()} /> ## What happens when an infinite query needs to be refetched? When an infinite query becomes stale and needs to be refetched, each group is fetched sequentially, starting from the first one. This ensures that even if the underlying data is mutated, we're not using stale cursors and potentially getting duplicates or skipping records. If an infinite query's results are ever removed from the queryCache, the pagination restarts at the initial state with only the initial group being requested. ## What if I want to implement a bi-directional infinite list? Bi-directional lists can be implemented by using the getPreviousPageParam, fetchPreviousPage, hasPreviousPage and isFetchingPreviousPage properties and functions. tsx useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor, }) useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor, }) ## What if I want to show the pages in reversed order? Sometimes you may want to show the pages in reversed order. If this is case, you can use the select option: tsx useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, select: (data) => ({ pages: [...data.pages].reverse(), pageParams: [...data.pageParams].reverse(), }), }) useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, select: (data) => ({ pages: [...data.pages].reverse(), pageParams: [...data.pageParams].reverse(), }), }) ## What if I want to manually update the infinite query? ### Manually removing first page: tsx queryClient.setQueryData(['projects'], (data) => ({ pages: data.pages.slice(1), pageParams: data.pageParams.slice(1), })) queryClient.setQueryData(['projects'], (data) => ({ pages: data.pages.slice(1), pageParams: data.pageParams.slice(1), })) ### Manually removing a single value from an individual page: tsx const newPagesArray = oldPagesArray?.pages.map((page) => page.filter((val) => val.id !== updatedId), ) ?? [] queryClient.setQueryData(['projects'], (data) => ({ pages: newPagesArray, pageParams: data.pageParams, })) const newPagesArray = oldPagesArray?.pages.map((page) => page.filter((val) => val.id !== updatedId), ) ?? [] queryClient.setQueryData(['projects'], (data) => ({ pages: newPagesArray, pageParams: data.pageParams, })) ### Keep only the first page: tsx queryClient.setQueryData(['projects'], (data) => ({ pages: data.pages.slice(0, 1), pageParams: data.pageParams.slice(0, 1), })) queryClient.setQueryData(['projects'], (data) => ({ pages: data.pages.slice(0, 1), pageParams: data.pageParams.slice(0, 1), })) Make sure to always keep the same data structure of pages and pageParams! ## What if I want to limit the number of pages? In some use cases you may want to limit the number of pages stored in the query data to improve the performance and UX: * when the user can load a large number of pages (memory usage) * when you have to refetch an infinite query that contains dozens of pages (network usage: all the pages are sequentially fetched) The solution is to use a "Limited Infinite Query". This is made possible by using the maxPages option in conjunction with getNextPageParam and getPreviousPageParam to allow fetching pages when needed in both directions. In the following example only 3 pages are kept in the query data pages array. If a refetch is needed, only 3 pages will be refetched sequentially. tsx useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor, maxPages: 3, }) useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor, maxPages: 3, }) ## What if my API doesn't return a cursor? If your API doesn't return a cursor, you can use the pageParam as a cursor. Because getNextPageParam and getPreviousPageParam also get the pageParamof the current page, you can use it to calculate the next / previous page param. tsx return useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, allPages, lastPageParam) => { if (lastPage.length === 0) { return undefined } return lastPageParam + 1 }, getPreviousPageParam: (firstPage, allPages, firstPageParam) => { if (firstPageParam <= 1) { return undefined } return firstPageParam - 1 }, }) return useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, allPages, lastPageParam) => { if (lastPage.length === 0) { return undefined } return lastPageParam + 1 }, getPreviousPageParam: (firstPage, allPages, firstPageParam) => { if (firstPageParam <= 1) { return undefined } return firstPageParam - 1 }, }) --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/initial-query-data There are many ways to supply initial data for a query to the cache before you need it: * Declaratively: * Provide initialData to a query to prepopulate its cache if empty * Imperatively: * Prefetch the data using queryClient.prefetchQuery * Manually place the data into the cache using queryClient.setQueryData ## Using initialData to prepopulate a query There may be times when you already have the initial data for a query available in your app and can simply provide it directly to your query. If and when this is the case, you can use the config.initialData option to set the initial data for a query and skip the initial loading state! > IMPORTANT: initialData is persisted to the cache, so it is not recommended to provide placeholder, partial or incomplete data to this option and instead use placeholderData tsx const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, }) const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, }) ### staleTime and initialDataUpdatedAt By default, initialData is treated as totally fresh, as if it were just fetched. This also means that it will affect how it is interpreted by the staleTime option. * If you configure your query observer with initialData, and no staleTime (the default staleTime: 0), the query will immediately refetch when it mounts: tsx // Will show initialTodos immediately, but also immediately refetch todos after mount const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, }) // Will show initialTodos immediately, but also immediately refetch todos after mount const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, }) * If you configure your query observer with initialData and a staleTime of 1000 ms, the data will be considered fresh for that same amount of time, as if it was just fetched from your query function. tsx // Show initialTodos immediately, but won't refetch until another interaction event is encountered after 1000 ms const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, staleTime: 1000, }) // Show initialTodos immediately, but won't refetch until another interaction event is encountered after 1000 ms const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, staleTime: 1000, }) * So what if your initialData isn't totally fresh? That leaves us with the last configuration that is actually the most accurate and uses an option called initialDataUpdatedAt. This option allows you to pass a numeric JS timestamp in milliseconds of when the initialData itself was last updated, e.g. what Date.now() provides. Take note that if you have a unix timestamp, you'll need to convert it to a JS timestamp by multiplying it by 1000. tsx // Show initialTodos immediately, but won't refetch until another interaction event is encountered after 1000 ms const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, staleTime: 60 * 1000, // 1 minute // This could be 10 seconds ago or 10 minutes ago initialDataUpdatedAt: initialTodosUpdatedTimestamp, // eg. 1608412420052 }) // Show initialTodos immediately, but won't refetch until another interaction event is encountered after 1000 ms const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, staleTime: 60 * 1000, // 1 minute // This could be 10 seconds ago or 10 minutes ago initialDataUpdatedAt: initialTodosUpdatedTimestamp, // eg. 1608412420052 }) This option allows the staleTime to be used for its original purpose, determining how fresh the data needs to be, while also allowing the data to be refetched on mount if the initialData is older than the staleTime. In the example above, our data needs to be fresh within 1 minute, and we can hint to the query when the initialData was last updated so the query can decide for itself whether the data needs to be refetched again or not. > If you would rather treat your data as **prefetched data**, we recommend that you use the prefetchQuery or fetchQuery APIs to populate the cache beforehand, thus letting you configure your staleTime independently from your initialData ### Initial Data Function If the process for accessing a query's initial data is intensive or just not something you want to perform on every render, you can pass a function as the initialData value. This function will be executed only once when the query is initialized, saving you precious memory and/or CPU: tsx const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: () => getExpensiveTodos(), }) const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: () => getExpensiveTodos(), }) ### Initial Data from Cache In some circumstances, you may be able to provide the initial data for a query from the cached result of another. A good example of this would be searching the cached data from a todos list query for an individual todo item, then using that as the initial data for your individual todo query: tsx const result = useQuery({ queryKey: ['todo', todoId], queryFn: () => fetch('/todos'), initialData: () => { // Use a todo from the 'todos' query as the initial data for this todo query return queryClient.getQueryData(['todos'])?.find((d) => d.id === todoId) }, }) const result = useQuery({ queryKey: ['todo', todoId], queryFn: () => fetch('/todos'), initialData: () => { // Use a todo from the 'todos' query as the initial data for this todo query return queryClient.getQueryData(['todos'])?.find((d) => d.id === todoId) }, }) ### Initial Data from the cache with initialDataUpdatedAt Getting initial data from the cache means the source query you're using to look up the initial data from is likely old. Instead of using an artificial staleTime to keep your query from refetching immediately, it's suggested that you pass the source query's dataUpdatedAt to initialDataUpdatedAt. This provides the query instance with all the information it needs to determine if and when the query needs to be refetched, regardless of initial data being provided. tsx const result = useQuery({ queryKey: ['todos', todoId], queryFn: () => fetch(`/todos/${todoId}`), initialData: () => queryClient.getQueryData(['todos'])?.find((d) => d.id === todoId), initialDataUpdatedAt: () => queryClient.getQueryState(['todos'])?.dataUpdatedAt, }) const result = useQuery({ queryKey: ['todos', todoId], queryFn: () => fetch(`/todos/${todoId}`), initialData: () => queryClient.getQueryData(['todos'])?.find((d) => d.id === todoId), initialDataUpdatedAt: () => queryClient.getQueryState(['todos'])?.dataUpdatedAt, }) ### Conditional Initial Data from Cache If the source query you're using to look up the initial data from is old, you may not want to use the cached data at all and just fetch from the server. To make this decision easier, you can use the queryClient.getQueryState method instead to get more information about the source query, including a state.dataUpdatedAt timestamp you can use to decide if the query is "fresh" enough for your needs: tsx const result = useQuery({ queryKey: ['todo', todoId], queryFn: () => fetch(`/todos/${todoId}`), initialData: () => { // Get the query state const state = queryClient.getQueryState(['todos']) // If the query exists and has data that is no older than 10 seconds... if (state && Date.now() - state.dataUpdatedAt <= 10 * 1000) { // return the individual todo return state.data.find((d) => d.id === todoId) } // Otherwise, return undefined and let it fetch from a hard loading state! }, }) const result = useQuery({ queryKey: ['todo', todoId], queryFn: () => fetch(`/todos/${todoId}`), initialData: () => { // Get the query state const state = queryClient.getQueryState(['todos']) // If the query exists and has data that is no older than 10 seconds... if (state && Date.now() - state.dataUpdatedAt <= 10 * 1000) { // return the individual todo return state.data.find((d) => d.id === todoId) } // Otherwise, return undefined and let it fetch from a hard loading state! }, }) ## Further reading For a comparison between Initial Data and Placeholder Data, have a look at the Community Resources. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/placeholder-query-data ## What is placeholder data? Placeholder data allows a query to behave as if it already has data, similar to the initialData option, but **the data is not persisted to the cache**. This comes in handy for situations where you have enough partial (or fake) data to render the query successfully while the actual data is fetched in the background. > Example: An individual blog post query could pull "preview" data from a parent list of blog posts that only include title and a small snippet of the post body. You would not want to persist this partial data to the query result of the individual query, but it is useful for showing the content layout as quickly as possible while the actual query finishes to fetch the entire object. There are a few ways to supply placeholder data for a query to the cache before you need it: * Declaratively: * Provide placeholderData to a query to prepopulate its cache if empty * Imperatively: * Prefetch or fetch the data using queryClient and the placeholderData option When we use placeholderData, our Query will not be in a pending state - it will start out as being in success state, because we have data to display - even if that data is just "placeholder" data. To distinguish it from "real" data, we will also have the isPlaceholderData flag set to true on the Query result. ## Placeholder Data as a Value tsx function Todos() { const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), placeholderData: placeholderTodos, }) } function Todos() { const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), placeholderData: placeholderTodos, }) } ### Placeholder Data Memoization If the process for accessing a query's placeholder data is intensive or just not something you want to perform on every render, you can memoize the value: tsx function Todos() { const placeholderData = useMemo(() => generateFakeTodos(), []) const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), placeholderData, }) } function Todos() { const placeholderData = useMemo(() => generateFakeTodos(), []) const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), placeholderData, }) } ## Placeholder Data as a Function placeholderData can also be a function, where you can get access to the data and Query meta information of a "previous" successful Query. This is useful for situations where you want to use the data from one query as the placeholder data for another query. When the QueryKey changes, e.g. from \['todos', 1\] to \['todos', 2\], we can keep displaying "old" data instead of having to show a loading spinner while data is _transitioning_ from one Query to the next. For more information, see Paginated Queries. tsx const result = useQuery({ queryKey: ['todos', id], queryFn: () => fetch(`/todos/${id}`), placeholderData: (previousData, previousQuery) => previousData, }) const result = useQuery({ queryKey: ['todos', id], queryFn: () => fetch(`/todos/${id}`), placeholderData: (previousData, previousQuery) => previousData, }) ### Placeholder Data from Cache In some circumstances, you may be able to provide the placeholder data for a query from the cached result of another. A good example of this would be searching the cached data from a blog post list query for a preview version of the post, then using that as the placeholder data for your individual post query: tsx function Todo({ blogPostId }) { const queryClient = useQueryClient() const result = useQuery({ queryKey: ['blogPost', blogPostId], queryFn: () => fetch(`/blogPosts/${blogPostId}`), placeholderData: () => { // Use the smaller/preview version of the blogPost from the 'blogPosts' // query as the placeholder data for this blogPost query return queryClient .getQueryData(['blogPosts']) ?.find((d) => d.id === blogPostId) }, }) } function Todo({ blogPostId }) { const queryClient = useQueryClient() const result = useQuery({ queryKey: ['blogPost', blogPostId], queryFn: () => fetch(`/blogPosts/${blogPostId}`), placeholderData: () => { // Use the smaller/preview version of the blogPost from the 'blogPosts' // query as the placeholder data for this blogPost query return queryClient .getQueryData(['blogPosts']) ?.find((d) => d.id === blogPostId) }, }) } ## Further reading For a comparison between Placeholder Data and Initial Data, have a look at the Community Resources. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/mutations Unlike queries, mutations are typically used to create/update/delete data or perform server side-effects. For this purpose, TanStack Query exports a useMutation hook. Here's an example of a mutation that adds a new todo to the server: tsx function App() { const mutation = useMutation({ mutationFn: (newTodo) => { return axios.post('/todos', newTodo) }, }) return ( <div> {mutation.isPending ? ( 'Adding todo...' ) : ( <> {mutation.isError ? ( <div>An error occurred: {mutation.error.message}</div> ) : null} {mutation.isSuccess ? <div>Todo added!</div> : null} <button onClick={() => { mutation.mutate({ id: new Date(), title: 'Do Laundry' }) }} > Create Todo </button> </> )} </div> ) } function App() { const mutation = useMutation({ mutationFn: (newTodo) => { return axios.post('/todos', newTodo) }, }) return ( <div> {mutation.isPending ? ( 'Adding todo...' ) : ( <> {mutation.isError ? ( <div>An error occurred: {mutation.error.message}</div> ) : null} {mutation.isSuccess ? <div>Todo added!</div> : null} <button onClick={() => { mutation.mutate({ id: new Date(), title: 'Do Laundry' }) }} > Create Todo </button> </> )} </div> ) } A mutation can only be in one of the following states at any given moment: * isIdle or status === 'idle' - The mutation is currently idle or in a fresh/reset state * isPending or status === 'pending' - The mutation is currently running * isError or status === 'error' - The mutation encountered an error * isSuccess or status === 'success' - The mutation was successful and mutation data is available Beyond those primary states, more information is available depending on the state of the mutation: * error - If the mutation is in an error state, the error is available via the error property. * data - If the mutation is in a success state, the data is available via the data property. In the example above, you also saw that you can pass variables to your mutations function by calling the mutate function with a **single variable or object**. Even with just variables, mutations aren't all that special, but when used with the onSuccess option, the Query Client's invalidateQueries method and the Query Client's setQueryData method, mutations become a very powerful tool. > IMPORTANT: The mutate function is an asynchronous function, which means you cannot use it directly in an event callback in **React 16 and earlier**. If you need to access the event in onSubmit you need to wrap mutate in another function. This is due to React event pooling. tsx // This will not work in React 16 and earlier const CreateTodo = () => { const mutation = useMutation({ mutationFn: (event) => { event.preventDefault() return fetch('/api', new FormData(event.target)) }, }) return <form onSubmit={mutation.mutate}>...</form> } // This will work const CreateTodo = () => { const mutation = useMutation({ mutationFn: (formData) => { return fetch('/api', formData) }, }) const onSubmit = (event) => { event.preventDefault() mutation.mutate(new FormData(event.target)) } return <form onSubmit={onSubmit}>...</form> } // This will not work in React 16 and earlier const CreateTodo = () => { const mutation = useMutation({ mutationFn: (event) => { event.preventDefault() return fetch('/api', new FormData(event.target)) }, }) return <form onSubmit={mutation.mutate}>...</form> } // This will work const CreateTodo = () => { const mutation = useMutation({ mutationFn: (formData) => { return fetch('/api', formData) }, }) const onSubmit = (event) => { event.preventDefault() mutation.mutate(new FormData(event.target)) } return <form onSubmit={onSubmit}>...</form> } ## Resetting Mutation State It's sometimes the case that you need to clear the error or data of a mutation request. To do this, you can use the reset function to handle this: tsx const CreateTodo = () => { const [title, setTitle] = useState('') const mutation = useMutation({ mutationFn: createTodo }) const onCreateTodo = (e) => { e.preventDefault() mutation.mutate({ title }) } return ( <form onSubmit={onCreateTodo}> {mutation.error && ( <h5 onClick={() => mutation.reset()}>{mutation.error}</h5> )} <input type="text" value={title} onChange={(e) => setTitle(e.target.value)} /> <br /> <button type="submit">Create Todo</button> </form> ) } const CreateTodo = () => { const [title, setTitle] = useState('') const mutation = useMutation({ mutationFn: createTodo }) const onCreateTodo = (e) => { e.preventDefault() mutation.mutate({ title }) } return ( <form onSubmit={onCreateTodo}> {mutation.error && ( <h5 onClick={() => mutation.reset()}>{mutation.error}</h5> )} <input type="text" value={title} onChange={(e) => setTitle(e.target.value)} /> <br /> <button type="submit">Create Todo</button> </form> ) } ## Mutation Side Effects useMutation comes with some helper options that allow quick and easy side-effects at any stage during the mutation lifecycle. These come in handy for both invalidating and refetching queries after mutations and even optimistic updates tsx useMutation({ mutationFn: addTodo, onMutate: (variables) => { // A mutation is about to happen! // Optionally return a context containing data to use when for example rolling back return { id: 1 } }, onError: (error, variables, context) => { // An error happened! console.log(`rolling back optimistic update with id ${context.id}`) }, onSuccess: (data, variables, context) => { // Boom baby! }, onSettled: (data, error, variables, context) => { // Error or success... doesn't matter! }, }) useMutation({ mutationFn: addTodo, onMutate: (variables) => { // A mutation is about to happen! // Optionally return a context containing data to use when for example rolling back return { id: 1 } }, onError: (error, variables, context) => { // An error happened! console.log(`rolling back optimistic update with id ${context.id}`) }, onSuccess: (data, variables, context) => { // Boom baby! }, onSettled: (data, error, variables, context) => { // Error or success... doesn't matter! }, }) When returning a promise in any of the callback functions it will first be awaited before the next callback is called: tsx useMutation({ mutationFn: addTodo, onSuccess: async () => { console.log("I'm first!") }, onSettled: async () => { console.log("I'm second!") }, }) useMutation({ mutationFn: addTodo, onSuccess: async () => { console.log("I'm first!") }, onSettled: async () => { console.log("I'm second!") }, }) You might find that you want to **trigger additional callbacks** beyond the ones defined on useMutation when calling mutate. This can be used to trigger component-specific side effects. To do that, you can provide any of the same callback options to the mutate function after your mutation variable. Supported options include: onSuccess, onError and onSettled. Please keep in mind that those additional callbacks won't run if your component unmounts _before_ the mutation finishes. tsx useMutation({ mutationFn: addTodo, onSuccess: (data, variables, context) => { // I will fire first }, onError: (error, variables, context) => { // I will fire first }, onSettled: (data, error, variables, context) => { // I will fire first }, }) mutate(todo, { onSuccess: (data, variables, context) => { // I will fire second! }, onError: (error, variables, context) => { // I will fire second! }, onSettled: (data, error, variables, context) => { // I will fire second! }, }) useMutation({ mutationFn: addTodo, onSuccess: (data, variables, context) => { // I will fire first }, onError: (error, variables, context) => { // I will fire first }, onSettled: (data, error, variables, context) => { // I will fire first }, }) mutate(todo, { onSuccess: (data, variables, context) => { // I will fire second! }, onError: (error, variables, context) => { // I will fire second! }, onSettled: (data, error, variables, context) => { // I will fire second! }, }) ### Consecutive mutations There is a slight difference in handling onSuccess, onError and onSettled callbacks when it comes to consecutive mutations. When passed to the mutate function, they will be fired up only _once_ and only if the component is still mounted. This is due to the fact that mutation observer is removed and resubscribed every time when the mutate function is called. On the contrary, useMutation handlers execute for each mutate call. > Be aware that most likely, mutationFn passed to useMutation is asynchronous. In that case, the order in which mutations are fulfilled may differ from the order of mutate function calls. tsx useMutation({ mutationFn: addTodo, onSuccess: (data, variables, context) => { // Will be called 3 times }, }) const todos = ['Todo 1', 'Todo 2', 'Todo 3'] todos.forEach((todo) => { mutate(todo, { onSuccess: (data, variables, context) => { // Will execute only once, for the last mutation (Todo 3), // regardless which mutation resolves first }, }) }) useMutation({ mutationFn: addTodo, onSuccess: (data, variables, context) => { // Will be called 3 times }, }) const todos = ['Todo 1', 'Todo 2', 'Todo 3'] todos.forEach((todo) => { mutate(todo, { onSuccess: (data, variables, context) => { // Will execute only once, for the last mutation (Todo 3), // regardless which mutation resolves first }, }) }) ## Promises Use mutateAsync instead of mutate to get a promise which will resolve on success or throw on an error. This can for example be used to compose side effects. tsx const mutation = useMutation({ mutationFn: addTodo }) try { const todo = await mutation.mutateAsync(todo) console.log(todo) } catch (error) { console.error(error) } finally { console.log('done') } const mutation = useMutation({ mutationFn: addTodo }) try { const todo = await mutation.mutateAsync(todo) console.log(todo) } catch (error) { console.error(error) } finally { console.log('done') } ## Retry By default, TanStack Query will not retry a mutation on error, but it is possible with the retry option: tsx const mutation = useMutation({ mutationFn: addTodo, retry: 3, }) const mutation = useMutation({ mutationFn: addTodo, retry: 3, }) If mutations fail because the device is offline, they will be retried in the same order when the device reconnects. ## Persist mutations Mutations can be persisted to storage if needed and resumed at a later point. This can be done with the hydration functions: tsx const queryClient = new QueryClient() // Define the "addTodo" mutation queryClient.setMutationDefaults(['addTodo'], { mutationFn: addTodo, onMutate: async (variables) => { // Cancel current queries for the todos list await queryClient.cancelQueries({ queryKey: ['todos'] }) // Create optimistic todo const optimisticTodo = { id: uuid(), title: variables.title } // Add optimistic todo to todos list queryClient.setQueryData(['todos'], (old) => [...old, optimisticTodo]) // Return context with the optimistic todo return { optimisticTodo } }, onSuccess: (result, variables, context) => { // Replace optimistic todo in the todos list with the result queryClient.setQueryData(['todos'], (old) => old.map((todo) => todo.id === context.optimisticTodo.id ? result : todo, ), ) }, onError: (error, variables, context) => { // Remove optimistic todo from the todos list queryClient.setQueryData(['todos'], (old) => old.filter((todo) => todo.id !== context.optimisticTodo.id), ) }, retry: 3, }) // Start mutation in some component: const mutation = useMutation({ mutationKey: ['addTodo'] }) mutation.mutate({ title: 'title' }) // If the mutation has been paused because the device is for example offline, // Then the paused mutation can be dehydrated when the application quits: const state = dehydrate(queryClient) // The mutation can then be hydrated again when the application is started: hydrate(queryClient, state) // Resume the paused mutations: queryClient.resumePausedMutations() const queryClient = new QueryClient() // Define the "addTodo" mutation queryClient.setMutationDefaults(['addTodo'], { mutationFn: addTodo, onMutate: async (variables) => { // Cancel current queries for the todos list await queryClient.cancelQueries({ queryKey: ['todos'] }) // Create optimistic todo const optimisticTodo = { id: uuid(), title: variables.title } // Add optimistic todo to todos list queryClient.setQueryData(['todos'], (old) => [...old, optimisticTodo]) // Return context with the optimistic todo return { optimisticTodo } }, onSuccess: (result, variables, context) => { // Replace optimistic todo in the todos list with the result queryClient.setQueryData(['todos'], (old) => old.map((todo) => todo.id === context.optimisticTodo.id ? result : todo, ), ) }, onError: (error, variables, context) => { // Remove optimistic todo from the todos list queryClient.setQueryData(['todos'], (old) => old.filter((todo) => todo.id !== context.optimisticTodo.id), ) }, retry: 3, }) // Start mutation in some component: const mutation = useMutation({ mutationKey: ['addTodo'] }) mutation.mutate({ title: 'title' }) // If the mutation has been paused because the device is for example offline, // Then the paused mutation can be dehydrated when the application quits: const state = dehydrate(queryClient) // The mutation can then be hydrated again when the application is started: hydrate(queryClient, state) // Resume the paused mutations: queryClient.resumePausedMutations() ### Persisting Offline mutations If you persist offline mutations with the persistQueryClient plugin, mutations cannot be resumed when the page is reloaded unless you provide a default mutation function. This is a technical limitation. When persisting to an external storage, only the state of mutations is persisted, as functions cannot be serialized. After hydration, the component that triggers the mutation might not be mounted, so calling resumePausedMutations might yield an error: No mutationFn found. tsx const persister = createSyncStoragePersister({ storage: window.localStorage, }) const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 24 hours }, }, }) // we need a default mutation function so that paused mutations can resume after a page reload queryClient.setMutationDefaults(['todos'], { mutationFn: ({ id, data }) => { return api.updateTodo(id, data) }, }) export default function App() { return ( <PersistQueryClientProvider client={queryClient} persistOptions={{ persister }} onSuccess={() => { // resume mutations after initial restore from localStorage was successful queryClient.resumePausedMutations() }} > <RestOfTheApp /> </PersistQueryClientProvider> ) } const persister = createSyncStoragePersister({ storage: window.localStorage, }) const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 24 hours }, }, }) // we need a default mutation function so that paused mutations can resume after a page reload queryClient.setMutationDefaults(['todos'], { mutationFn: ({ id, data }) => { return api.updateTodo(id, data) }, }) export default function App() { return ( <PersistQueryClientProvider client={queryClient} persistOptions={{ persister }} onSuccess={() => { // resume mutations after initial restore from localStorage was successful queryClient.resumePausedMutations() }} > <RestOfTheApp /> </PersistQueryClientProvider> ) } We also have an extensive offline example that covers both queries and mutations. ## Mutation Scopes Per default, all mutations run in parallel - even if you invoke .mutate() of the same mutation multiple times. Mutations can be given a scope with an id to avoid that. All mutations with the same scope.id will run in serial, which means when they are triggered, they will start in isPaused: true state if there is already a mutation for that scope in progress. They will be put into a queue and will automatically resume once their time in the queue has come. tsx const mutation = useMutation({ mutationFn: addTodo, scope: { id: 'todo', }, }) const mutation = useMutation({ mutationFn: addTodo, scope: { id: 'todo', }, }) ## Further reading For more information about mutations, have a look at #12: Mastering Mutations in React Query from the Community Resources. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation Waiting for queries to become stale before they are fetched again doesn't always work, especially when you know for a fact that a query's data is out of date because of something the user has done. For that purpose, the QueryClient has an invalidateQueries method that lets you intelligently mark queries as stale and potentially refetch them too! tsx // Invalidate every query in the cache queryClient.invalidateQueries() // Invalidate every query with a key that starts with `todos` queryClient.invalidateQueries({ queryKey: ['todos'] }) // Invalidate every query in the cache queryClient.invalidateQueries() // Invalidate every query with a key that starts with `todos` queryClient.invalidateQueries({ queryKey: ['todos'] }) > Note: Where other libraries that use normalized caches would attempt to update local queries with the new data either imperatively or via schema inference, TanStack Query gives you the tools to avoid the manual labor that comes with maintaining normalized caches and instead prescribes **targeted invalidation, background-refetching and ultimately atomic updates**. When a query is invalidated with invalidateQueries, two things happen: * It is marked as stale. This stale state overrides any staleTime configurations being used in useQuery or related hooks * If the query is currently being rendered via useQuery or related hooks, it will also be refetched in the background ## Query Matching with invalidateQueries When using APIs like invalidateQueries and removeQueries (and others that support partial query matching), you can match multiple queries by their prefix, or get really specific and match an exact query. For information on the types of filters you can use, please see Query Filters. In this example, we can use the todos prefix to invalidate any queries that start with todos in their query key: tsx import { useQuery, useQueryClient } from '@tanstack/react-query' // Get QueryClient from the context const queryClient = useQueryClient() queryClient.invalidateQueries({ queryKey: ['todos'] }) // Both queries below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) const todoListQuery = useQuery({ queryKey: ['todos', { page: 1 }], queryFn: fetchTodoList, }) import { useQuery, useQueryClient } from '@tanstack/react-query' // Get QueryClient from the context const queryClient = useQueryClient() queryClient.invalidateQueries({ queryKey: ['todos'] }) // Both queries below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) const todoListQuery = useQuery({ queryKey: ['todos', { page: 1 }], queryFn: fetchTodoList, }) You can even invalidate queries with specific variables by passing a more specific query key to the invalidateQueries method: tsx queryClient.invalidateQueries({ queryKey: ['todos', { type: 'done' }], }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { type: 'done' }], queryFn: fetchTodoList, }) // However, the following query below will NOT be invalidated const todoListQuery = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) queryClient.invalidateQueries({ queryKey: ['todos', { type: 'done' }], }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { type: 'done' }], queryFn: fetchTodoList, }) // However, the following query below will NOT be invalidated const todoListQuery = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) The invalidateQueries API is very flexible, so even if you want to **only** invalidate todos queries that don't have any more variables or subkeys, you can pass an exact: true option to the invalidateQueries method: tsx queryClient.invalidateQueries({ queryKey: ['todos'], exact: true, }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) // However, the following query below will NOT be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { type: 'done' }], queryFn: fetchTodoList, }) queryClient.invalidateQueries({ queryKey: ['todos'], exact: true, }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) // However, the following query below will NOT be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { type: 'done' }], queryFn: fetchTodoList, }) If you find yourself wanting **even more** granularity, you can pass a predicate function to the invalidateQueries method. This function will receive each Query instance from the query cache and allow you to return true or false for whether you want to invalidate that query: tsx queryClient.invalidateQueries({ predicate: (query) => query.queryKey[0] === 'todos' && query.queryKey[1]?.version >= 10, }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { version: 20 }], queryFn: fetchTodoList, }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { version: 10 }], queryFn: fetchTodoList, }) // However, the following query below will NOT be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { version: 5 }], queryFn: fetchTodoList, }) queryClient.invalidateQueries({ predicate: (query) => query.queryKey[0] === 'todos' && query.queryKey[1]?.version >= 10, }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { version: 20 }], queryFn: fetchTodoList, }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { version: 10 }], queryFn: fetchTodoList, }) // However, the following query below will NOT be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { version: 5 }], queryFn: fetchTodoList, }) --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/invalidations-from-mutations Invalidating queries is only half the battle. Knowing **when** to invalidate them is the other half. Usually when a mutation in your app succeeds, it's VERY likely that there are related queries in your application that need to be invalidated and possibly refetched to account for the new changes from your mutation. For example, assume we have a mutation to post a new todo: tsx const mutation = useMutation({ mutationFn: postTodo }) const mutation = useMutation({ mutationFn: postTodo }) When a successful postTodo mutation happens, we likely want all todos queries to get invalidated and possibly refetched to show the new todo item. To do this, you can use useMutation's onSuccess options and the client's invalidateQueries function: tsx import { useMutation, useQueryClient } from '@tanstack/react-query' const queryClient = useQueryClient() // When this mutation succeeds, invalidate any queries with the `todos` or `reminders` query key const mutation = useMutation({ mutationFn: addTodo, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }) queryClient.invalidateQueries({ queryKey: ['reminders'] }) }, }) import { useMutation, useQueryClient } from '@tanstack/react-query' const queryClient = useQueryClient() // When this mutation succeeds, invalidate any queries with the `todos` or `reminders` query key const mutation = useMutation({ mutationFn: addTodo, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }) queryClient.invalidateQueries({ queryKey: ['reminders'] }) }, }) You can wire up your invalidations to happen using any of the callbacks available in the useMutation hook --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/updates-from-mutation-responses When dealing with mutations that **update** objects on the server, it's common for the new object to be automatically returned in the response of the mutation. Instead of refetching any queries for that item and wasting a network call for data we already have, we can take advantage of the object returned by the mutation function and update the existing query with the new data immediately using the Query Client's setQueryData method: tsx const queryClient = useQueryClient() const mutation = useMutation({ mutationFn: editTodo, onSuccess: (data) => { queryClient.setQueryData(['todo', { id: 5 }], data) }, }) mutation.mutate({ id: 5, name: 'Do the laundry', }) // The query below will be updated with the response from the // successful mutation const { status, data, error } = useQuery({ queryKey: ['todo', { id: 5 }], queryFn: fetchTodoById, }) const queryClient = useQueryClient() const mutation = useMutation({ mutationFn: editTodo, onSuccess: (data) => { queryClient.setQueryData(['todo', { id: 5 }], data) }, }) mutation.mutate({ id: 5, name: 'Do the laundry', }) // The query below will be updated with the response from the // successful mutation const { status, data, error } = useQuery({ queryKey: ['todo', { id: 5 }], queryFn: fetchTodoById, }) You might want to tie the onSuccess logic into a reusable mutation, for that you can create a custom hook like this: tsx const useMutateTodo = () => { const queryClient = useQueryClient() return useMutation({ mutationFn: editTodo, // Notice the second argument is the variables object that the `mutate` function receives onSuccess: (data, variables) => { queryClient.setQueryData(['todo', { id: variables.id }], data) }, }) } const useMutateTodo = () => { const queryClient = useQueryClient() return useMutation({ mutationFn: editTodo, // Notice the second argument is the variables object that the `mutate` function receives onSuccess: (data, variables) => { queryClient.setQueryData(['todo', { id: variables.id }], data) }, }) } ## Immutability Updates via setQueryData must be performed in an _immutable_ way. **DO NOT** attempt to write directly to the cache by mutating data (that you retrieved from the cache) in place. It might work at first but can lead to subtle bugs along the way. tsx queryClient.setQueryData(['posts', { id }], (oldData) => { if (oldData) { // ❌ do not try this oldData.title = 'my new post title' } return oldData }) queryClient.setQueryData( ['posts', { id }], // ✅ this is the way (oldData) => oldData ? { ...oldData, title: 'my new post title', } : oldData, ) queryClient.setQueryData(['posts', { id }], (oldData) => { if (oldData) { // ❌ do not try this oldData.title = 'my new post title' } return oldData }) queryClient.setQueryData( ['posts', { id }], // ✅ this is the way (oldData) => oldData ? { ...oldData, title: 'my new post title', } : oldData, ) --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates React Query provides two ways to optimistically update your UI before a mutation has completed. You can either use the onMutate option to update your cache directly, or leverage the returned variables to update your UI from the useMutation result. ## Via the UI This is the simpler variant, as it doesn't interact with the cache directly. tsx const addTodoMutation = useMutation({ mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }), // make sure to _return_ the Promise from the query invalidation // so that the mutation stays in `pending` state until the refetch is finished onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), }) const { isPending, submittedAt, variables, mutate, isError } = addTodoMutation const addTodoMutation = useMutation({ mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }), // make sure to _return_ the Promise from the query invalidation // so that the mutation stays in `pending` state until the refetch is finished onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), }) const { isPending, submittedAt, variables, mutate, isError } = addTodoMutation you will then have access to addTodoMutation.variables, which contain the added todo. In your UI list, where the query is rendered, you can append another item to the list while the mutation isPending: tsx <ul> {todoQuery.items.map((todo) => ( <li key={todo.id}>{todo.text}</li> ))} {isPending && <li style={{ opacity: 0.5 }}>{variables}</li>} </ul> <ul> {todoQuery.items.map((todo) => ( <li key={todo.id}>{todo.text}</li> ))} {isPending && <li style={{ opacity: 0.5 }}>{variables}</li>} </ul> We're rendering a temporary item with a different opacity as long as the mutation is pending. Once it completes, the item will automatically no longer be rendered. Given that the refetch succeeded, we should see the item as a "normal item" in our list. If the mutation errors, the item will also disappear. But we could continue to show it, if we want, by checking for the isError state of the mutation. variables are _not_ cleared when the mutation errors, so we can still access them, maybe even show a retry button: tsx { isError && ( <li style={{ color: 'red' }}> {variables} <button onClick={() => mutate(variables)}>Retry</button> </li> ) } { isError && ( <li style={{ color: 'red' }}> {variables} <button onClick={() => mutate(variables)}>Retry</button> </li> ) } ### If the mutation and the query don't live in the same component This approach works very well if the mutation and the query live in the same component, However, you also get access to all mutations in other components via the dedicated useMutationState hook. It is best combined with a mutationKey: tsx // somewhere in your app const { mutate } = useMutation({ mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }), onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), mutationKey: ['addTodo'], }) // access variables somewhere else const variables = useMutationState<string>({ filters: { mutationKey: ['addTodo'], status: 'pending' }, select: (mutation) => mutation.state.variables, }) // somewhere in your app const { mutate } = useMutation({ mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }), onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), mutationKey: ['addTodo'], }) // access variables somewhere else const variables = useMutationState<string>({ filters: { mutationKey: ['addTodo'], status: 'pending' }, select: (mutation) => mutation.state.variables, }) variables will be an Array, because there might be multiple mutations running at the same time. If we need a unique key for the items, we can also select mutation.state.submittedAt. This will even make displaying concurrent optimistic updates a breeze. ## Via the cache When you optimistically update your state before performing a mutation, there is a chance that the mutation will fail. In most of these failure cases, you can just trigger a refetch for your optimistic queries to revert them to their true server state. In some circumstances though, refetching may not work correctly and the mutation error could represent some type of server issue that won't make it possible to refetch. In this event, you can instead choose to roll back your update. To do this, useMutation's onMutate handler option allows you to return a value that will later be passed to both onError and onSettled handlers as the last argument. In most cases, it is most useful to pass a rollback function. ### Updating a list of todos when adding a new todo tsx const queryClient = useQueryClient() useMutation({ mutationFn: updateTodo, // When mutate is called: onMutate: async (newTodo) => { // Cancel any outgoing refetches // (so they don't overwrite our optimistic update) await queryClient.cancelQueries({ queryKey: ['todos'] }) // Snapshot the previous value const previousTodos = queryClient.getQueryData(['todos']) // Optimistically update to the new value queryClient.setQueryData(['todos'], (old) => [...old, newTodo]) // Return a context object with the snapshotted value return { previousTodos } }, // If the mutation fails, // use the context returned from onMutate to roll back onError: (err, newTodo, context) => { queryClient.setQueryData(['todos'], context.previousTodos) }, // Always refetch after error or success: onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), }) const queryClient = useQueryClient() useMutation({ mutationFn: updateTodo, // When mutate is called: onMutate: async (newTodo) => { // Cancel any outgoing refetches // (so they don't overwrite our optimistic update) await queryClient.cancelQueries({ queryKey: ['todos'] }) // Snapshot the previous value const previousTodos = queryClient.getQueryData(['todos']) // Optimistically update to the new value queryClient.setQueryData(['todos'], (old) => [...old, newTodo]) // Return a context object with the snapshotted value return { previousTodos } }, // If the mutation fails, // use the context returned from onMutate to roll back onError: (err, newTodo, context) => { queryClient.setQueryData(['todos'], context.previousTodos) }, // Always refetch after error or success: onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), }) ### Updating a single todo tsx useMutation({ mutationFn: updateTodo, // When mutate is called: onMutate: async (newTodo) => { // Cancel any outgoing refetches // (so they don't overwrite our optimistic update) await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] }) // Snapshot the previous value const previousTodo = queryClient.getQueryData(['todos', newTodo.id]) // Optimistically update to the new value queryClient.setQueryData(['todos', newTodo.id], newTodo) // Return a context with the previous and new todo return { previousTodo, newTodo } }, // If the mutation fails, use the context we returned above onError: (err, newTodo, context) => { queryClient.setQueryData( ['todos', context.newTodo.id], context.previousTodo, ) }, // Always refetch after error or success: onSettled: (newTodo) => queryClient.invalidateQueries({ queryKey: ['todos', newTodo.id] }), }) useMutation({ mutationFn: updateTodo, // When mutate is called: onMutate: async (newTodo) => { // Cancel any outgoing refetches // (so they don't overwrite our optimistic update) await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] }) // Snapshot the previous value const previousTodo = queryClient.getQueryData(['todos', newTodo.id]) // Optimistically update to the new value queryClient.setQueryData(['todos', newTodo.id], newTodo) // Return a context with the previous and new todo return { previousTodo, newTodo } }, // If the mutation fails, use the context we returned above onError: (err, newTodo, context) => { queryClient.setQueryData( ['todos', context.newTodo.id], context.previousTodo, ) }, // Always refetch after error or success: onSettled: (newTodo) => queryClient.invalidateQueries({ queryKey: ['todos', newTodo.id] }), }) You can also use the onSettled function in place of the separate onError and onSuccess handlers if you wish: tsx useMutation({ mutationFn: updateTodo, // ... onSettled: async (newTodo, error, variables, context) => { if (error) { // do something } }, }) useMutation({ mutationFn: updateTodo, // ... onSettled: async (newTodo, error, variables, context) => { if (error) { // do something } }, }) ## When to use what If you only have one place where the optimistic result should be shown, using variables and updating the UI directly is the approach that requires less code and is generally easier to reason about. For example, you don't need to handle rollbacks at all. However, if you have multiple places on the screen that would require to know about the update, manipulating the cache directly will take care of this for you automatically. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/query-cancellation TanStack Query provides each query function with an AbortSignal instance. When a query becomes out-of-date or inactive, this signal will become aborted. This means that all queries are cancellable, and you can respond to the cancellation inside your query function if desired. The best part about this is that it allows you to continue to use normal async/await syntax while getting all the benefits of automatic cancellation. The AbortController API is available in most runtime environments, but if your runtime environment does not support it, you will need to provide a polyfill. There are several available. ## Default behavior By default, queries that unmount or become unused before their promises are resolved are _not_ cancelled. This means that after the promise has resolved, the resulting data will be available in the cache. This is helpful if you've started receiving a query, but then unmount the component before it finishes. If you mount the component again and the query has not been garbage collected yet, data will be available. However, if you consume the AbortSignal, the Promise will be cancelled (e.g. aborting the fetch) and therefore, also the Query must be cancelled. Cancelling the query will result in its state being _reverted_ to its previous state. ## Using fetch tsx const query = useQuery({ queryKey: ['todos'], queryFn: async ({ signal }) => { const todosResponse = await fetch('/todos', { // Pass the signal to one fetch signal, }) const todos = await todosResponse.json() const todoDetails = todos.map(async ({ details }) => { const response = await fetch(details, { // Or pass it to several signal, }) return response.json() }) return Promise.all(todoDetails) }, }) const query = useQuery({ queryKey: ['todos'], queryFn: async ({ signal }) => { const todosResponse = await fetch('/todos', { // Pass the signal to one fetch signal, }) const todos = await todosResponse.json() const todoDetails = todos.map(async ({ details }) => { const response = await fetch(details, { // Or pass it to several signal, }) return response.json() }) return Promise.all(todoDetails) }, }) ## Using axios v0.22.0+ tsx import axios from 'axios' const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => axios.get('/todos', { // Pass the signal to `axios` signal, }), }) import axios from 'axios' const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => axios.get('/todos', { // Pass the signal to `axios` signal, }), }) ### Using axios with version lower than v0.22.0 tsx import axios from 'axios' const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { // Create a new CancelToken source for this request const CancelToken = axios.CancelToken const source = CancelToken.source() const promise = axios.get('/todos', { // Pass the source token to your request cancelToken: source.token, }) // Cancel the request if TanStack Query signals to abort signal?.addEventListener('abort', () => { source.cancel('Query was cancelled by TanStack Query') }) return promise }, }) import axios from 'axios' const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { // Create a new CancelToken source for this request const CancelToken = axios.CancelToken const source = CancelToken.source() const promise = axios.get('/todos', { // Pass the source token to your request cancelToken: source.token, }) // Cancel the request if TanStack Query signals to abort signal?.addEventListener('abort', () => { source.cancel('Query was cancelled by TanStack Query') }) return promise }, }) ## Using XMLHttpRequest tsx const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { return new Promise((resolve, reject) => { var oReq = new XMLHttpRequest() oReq.addEventListener('load', () => { resolve(JSON.parse(oReq.responseText)) }) signal?.addEventListener('abort', () => { oReq.abort() reject() }) oReq.open('GET', '/todos') oReq.send() }) }, }) const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { return new Promise((resolve, reject) => { var oReq = new XMLHttpRequest() oReq.addEventListener('load', () => { resolve(JSON.parse(oReq.responseText)) }) signal?.addEventListener('abort', () => { oReq.abort() reject() }) oReq.open('GET', '/todos') oReq.send() }) }, }) ## Using graphql-request An AbortSignal can be set in the client request method. tsx const client = new GraphQLClient(endpoint) const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { client.request({ document: query, signal }) }, }) const client = new GraphQLClient(endpoint) const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { client.request({ document: query, signal }) }, }) ## Using graphql-request with version lower than v4.0.0 An AbortSignal can be set in the GraphQLClient constructor. tsx const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { const client = new GraphQLClient(endpoint, { signal, }) return client.request(query, variables) }, }) const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { const client = new GraphQLClient(endpoint, { signal, }) return client.request(query, variables) }, }) ## Manual Cancellation You might want to cancel a query manually. For example, if the request takes a long time to finish, you can allow the user to click a cancel button to stop the request. To do this, you just need to call queryClient.cancelQueries({ queryKey }), which will cancel the query and revert it back to its previous state. If you have consumed the signal passed to the query function, TanStack Query will additionally also cancel the Promise. tsx const query = useQuery({ queryKey: ['todos'], queryFn: async ({ signal }) => { const resp = await fetch('/todos', { signal }) return resp.json() }, }) const queryClient = useQueryClient() return ( <button onClick={(e) => { e.preventDefault() queryClient.cancelQueries({ queryKey: ['todos'] }) }} > Cancel </button> ) const query = useQuery({ queryKey: ['todos'], queryFn: async ({ signal }) => { const resp = await fetch('/todos', { signal }) return resp.json() }, }) const queryClient = useQueryClient() return ( <button onClick={(e) => { e.preventDefault() queryClient.cancelQueries({ queryKey: ['todos'] }) }} > Cancel </button> ) ## Limitations Cancellation does not work when working with Suspense hooks: useSuspenseQuery, useSuspenseQueries and useSuspenseInfiniteQuery. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/scroll-restoration Traditionally, when you navigate to a previously visited page on a web browser, you would find that the page would be scrolled to the exact position where you were before you navigated away from that page. This is called **scroll restoration** and has been in a bit of a regression since web applications have started moving towards client side data fetching. With TanStack Query however, that's no longer the case. Out of the box, "scroll restoration" for all queries (including paginated and infinite queries) Just Works™️ in TanStack Query. The reason for this is that query results are cached and able to be retrieved synchronously when a query is rendered. As long as your queries are being cached long enough (the default time is 5 minutes) and have not been garbage collected, scroll restoration will work out of the box all the time. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/filters Some methods within TanStack Query accept a QueryFilters or MutationFilters object. ## Query Filters A query filter is an object with certain conditions to match a query with: tsx // Cancel all queries await queryClient.cancelQueries() // Remove all inactive queries that begin with `posts` in the key queryClient.removeQueries({ queryKey: ['posts'], type: 'inactive' }) // Refetch all active queries await queryClient.refetchQueries({ type: 'active' }) // Refetch all active queries that begin with `posts` in the key await queryClient.refetchQueries({ queryKey: ['posts'], type: 'active' }) // Cancel all queries await queryClient.cancelQueries() // Remove all inactive queries that begin with `posts` in the key queryClient.removeQueries({ queryKey: ['posts'], type: 'inactive' }) // Refetch all active queries await queryClient.refetchQueries({ type: 'active' }) // Refetch all active queries that begin with `posts` in the key await queryClient.refetchQueries({ queryKey: ['posts'], type: 'active' }) A query filter object supports the following properties: * queryKey?: QueryKey * Set this property to define a query key to match on. * exact?: boolean * If you don't want to search queries inclusively by query key, you can pass the exact: true option to return only the query with the exact query key you have passed. * type?: 'active' | 'inactive' | 'all' * Defaults to all * When set to active it will match active queries. * When set to inactive it will match inactive queries. * stale?: boolean * When set to true it will match stale queries. * When set to false it will match fresh queries. * fetchStatus?: FetchStatus * When set to fetching it will match queries that are currently fetching. * When set to paused it will match queries that wanted to fetch, but have been paused. * When set to idle it will match queries that are not fetching. * predicate?: (query: Query) => boolean * This predicate function will be used as a final filter on all matching queries. If no other filters are specified, this function will be evaluated against every query in the cache. ## Mutation Filters A mutation filter is an object with certain conditions to match a mutation with: tsx // Get the number of all fetching mutations await queryClient.isMutating() // Filter mutations by mutationKey await queryClient.isMutating({ mutationKey: ['post'] }) // Filter mutations using a predicate function await queryClient.isMutating({ predicate: (mutation) => mutation.state.variables?.id === 1, }) // Get the number of all fetching mutations await queryClient.isMutating() // Filter mutations by mutationKey await queryClient.isMutating({ mutationKey: ['post'] }) // Filter mutations using a predicate function await queryClient.isMutating({ predicate: (mutation) => mutation.state.variables?.id === 1, }) A mutation filter object supports the following properties: * mutationKey?: MutationKey * Set this property to define a mutation key to match on. * exact?: boolean * If you don't want to search mutations inclusively by mutation key, you can pass the exact: true option to return only the mutation with the exact mutation key you have passed. * status?: MutationStatus * Allows for filtering mutations according to their status. * predicate?: (mutation: Mutation) => boolean * This predicate function will be used as a final filter on all matching mutations. If no other filters are specified, this function will be evaluated against every mutation in the cache. ## Utils ### matchQuery tsx const isMatching = matchQuery(filters, query) const isMatching = matchQuery(filters, query) Returns a boolean that indicates whether a query matches the provided set of query filters. ### matchMutation tsx const isMatching = matchMutation(filters, mutation) const isMatching = matchMutation(filters, mutation) Returns a boolean that indicates whether a mutation matches the provided set of mutation filters. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/request-waterfalls Application performance is a broad and complex area and while React Query can't make your APIs faster, there are still things to be mindful about in how you use React Query to ensure the best performance. The biggest performance footgun when using React Query, or indeed any data fetching library that lets you fetch data inside of components, is request waterfalls. The rest of this page will explain what they are, how you can spot them and how you can restructure your application or APIs to avoid them. The Prefetching & Router Integration guide builds on this and teaches you how to prefetch data ahead of time when it's not possible or feasible to restructure your application or APIs. The Server Rendering & Hydration guide teaches you how to prefetch data on the server and pass that data down to the client so you don't have to fetch it again. The Advanced Server Rendering guide further teaches you how to apply these patterns to Server Components and Streaming Server Rendering. ## What is a Request Waterfall? A request waterfall is what happens when a request for a resource (code, css, images, data) does not start until _after_ another request for a resource has finished. Consider a web page. Before you can load things like the CSS, JS etc, the browser first needs to load the markup. This is a request waterfall. 1. |-> Markup 2. |-> CSS 2. |-> JS 2. |-> Image 1. |-> Markup 2. |-> CSS 2. |-> JS 2. |-> Image If you fetch your CSS inside a JS file, you now have a double waterfall: 1. |-> Markup 2. |-> JS 3. |-> CSS 1. |-> Markup 2. |-> JS 3. |-> CSS If that CSS uses a background image, it's a triple waterfall: 1. |-> Markup 2. |-> JS 3. |-> CSS 4. |-> Image 1. |-> Markup 2. |-> JS 3. |-> CSS 4. |-> Image The best way to spot and analyze your request waterfalls is usually by opening your browsers devtools "Network" tab. Each waterfall represents at least one roundtrip to the server, unless the resource is locally cached (in practice, some of these waterfalls might represent more than one roundtrip because the browser needs to establish a connection which requires some back and forth, but let's ignore that here). Because of this, the negative effects of request waterfalls are highly dependent on the users latency. Consider the example of the triple waterfall, which actually represents 4 server roundtrips. With 250ms latency, which is not uncommon on 3g networks or in bad network conditions, we end up with a total time of 4\*250=1000ms **only counting latency**. If we were able to flatten that to the first example with only 2 roundtrips, we get 500ms instead, possibly loading that background image in half the time! ## Request Waterfalls & React Query Now let's consider React Query. We'll focus on the case without Server Rendering first. Before we can even start making a query, we need to load the JS, so before we can show that data on the screen, we have a double waterfall: 1. |-> Markup 2. |-> JS 3. |-> Query 1. |-> Markup 2. |-> JS 3. |-> Query With this as a basis, let's look at a few different patterns that can lead to Request Waterfalls in React Query, and how to avoid them. * Single Component Waterfalls / Serial Queries * Nested Component Waterfalls * Code Splitting ### Single Component Waterfalls / Serial Queries When a single component first fetches one query, and then another, that's a request waterfall. This can happen when the second query is a Dependent Query, that is, it depends on data from the first query when fetching: tsx // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) While not always feasible, for optimal performance it's better to restructure your API so you can fetch both of these in a single query. In the example above, instead of first fetching getUserByEmail to be able to getProjectsByUser, introducing a new getProjectsByUserEmail query would flatten the waterfall. > Another way to mitigate dependent queries without restructuring your API is to move the waterfall to the server where latency is lower. This is the idea behind Server Components which are covered in the Advanced Server Rendering guide. Another example of serial queries is when you use React Query with Suspense: tsx function App () { // The following queries will execute in serial, causing separate roundtrips to the server: const usersQuery = useSuspenseQuery({ queryKey: ['users'], queryFn: fetchUsers }) const teamsQuery = useSuspenseQuery({ queryKey: ['teams'], queryFn: fetchTeams }) const projectsQuery = useSuspenseQuery({ queryKey: ['projects'], queryFn: fetchProjects }) // Note that since the queries above suspend rendering, no data // gets rendered until all of the queries finished ... } function App () { // The following queries will execute in serial, causing separate roundtrips to the server: const usersQuery = useSuspenseQuery({ queryKey: ['users'], queryFn: fetchUsers }) const teamsQuery = useSuspenseQuery({ queryKey: ['teams'], queryFn: fetchTeams }) const projectsQuery = useSuspenseQuery({ queryKey: ['projects'], queryFn: fetchProjects }) // Note that since the queries above suspend rendering, no data // gets rendered until all of the queries finished ... } Note that with regular useQuery these would happen in parallel. Luckily, this is easy to fix, by always using the hook useSuspenseQueries when you have multiple suspenseful queries in a component. tsx const [usersQuery, teamsQuery, projectsQuery] = useSuspenseQueries({ queries: [ { queryKey: ['users'], queryFn: fetchUsers }, { queryKey: ['teams'], queryFn: fetchTeams }, { queryKey: ['projects'], queryFn: fetchProjects }, ], }) const [usersQuery, teamsQuery, projectsQuery] = useSuspenseQueries({ queries: [ { queryKey: ['users'], queryFn: fetchUsers }, { queryKey: ['teams'], queryFn: fetchTeams }, { queryKey: ['projects'], queryFn: fetchProjects }, ], }) ### Nested Component Waterfalls Nested Component Waterfalls is when both a parent and a child component contains queries, and the parent does not render the child until its query is done. This can happen both with useQuery and useSuspenseQuery. If the child renders conditionally based on the data in the parent, or if the child relies on some part of the result being passed down as a prop from the parent to make its query, we have a _dependent_ nested component waterfall. Let's first look at an example where the child is **not** dependent on the parent. tsx function Article({ id }) { const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) if (isPending) { return 'Loading article...' } return ( <> <ArticleHeader articleData={articleData} /> <ArticleBody articleData={articleData} /> <Comments id={id} /> </> ) } function Comments({ id }) { const { data, isPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) ... } function Article({ id }) { const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) if (isPending) { return 'Loading article...' } return ( <> <ArticleHeader articleData={articleData} /> <ArticleBody articleData={articleData} /> <Comments id={id} /> </> ) } function Comments({ id }) { const { data, isPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) ... } Note that while <Comments> takes a prop id from the parent, that id is already available when the <Article> renders so there is no reason we could not fetch the comments at the same time as the article. In real world applications, the child might be nested far below the parent and these kinds of waterfalls are often trickier to spot and fix, but for our example, one way to flatten the waterfall would be to hoist the comments query to the parent instead: tsx function Article({ id }) { const { data: articleData, isPending: articlePending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) const { data: commentsData, isPending: commentsPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) if (articlePending) { return 'Loading article...' } return ( <> <ArticleHeader articleData={articleData} /> <ArticleBody articleData={articleData} /> {commentsPending ? ( 'Loading comments...' ) : ( <Comments commentsData={commentsData} /> )} </> ) } function Article({ id }) { const { data: articleData, isPending: articlePending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) const { data: commentsData, isPending: commentsPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) if (articlePending) { return 'Loading article...' } return ( <> <ArticleHeader articleData={articleData} /> <ArticleBody articleData={articleData} /> {commentsPending ? ( 'Loading comments...' ) : ( <Comments commentsData={commentsData} /> )} </> ) } The two queries will now fetch in parallel. Note that if you are using suspense, you'd want to combine these two queries into a single useSuspenseQueries instead. Another way to flatten this waterfall would be to prefetch the comments in the <Article> component, or prefetch both of these queries at the router level on page load or page navigation, read more about this in the Prefetching & Router Integration guide. Next, let's look at a _Dependent Nested Component Waterfall_. tsx function Feed() { const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: getFeed, }) if (isPending) { return 'Loading feed...' } return ( <> {data.map((feedItem) => { if (feedItem.type === 'GRAPH') { return <GraphFeedItem key={feedItem.id} feedItem={feedItem} /> } return <StandardFeedItem key={feedItem.id} feedItem={feedItem} /> })} </> ) } function GraphFeedItem({ feedItem }) { const { data, isPending } = useQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) ... } function Feed() { const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: getFeed, }) if (isPending) { return 'Loading feed...' } return ( <> {data.map((feedItem) => { if (feedItem.type === 'GRAPH') { return <GraphFeedItem key={feedItem.id} feedItem={feedItem} /> } return <StandardFeedItem key={feedItem.id} feedItem={feedItem} /> })} </> ) } function GraphFeedItem({ feedItem }) { const { data, isPending } = useQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) ... } The second query getGraphDataById is dependent on its parent in two different ways. First of all, it doesn't ever happen unless the feedItem is a graph, and second, it needs an id from the parent. 1. |> getFeed() 2. |> getGraphDataById() 1. |> getFeed() 2. |> getGraphDataById() In this example, we can't trivially flatten the waterfall by just hoisting the query to the parent, or even adding prefetching. Just like the dependent query example at the beginning of this guide, one option is to refactor our API to include the graph data in the getFeed query. Another more advanced solution is to leverage Server Components to move the waterfall to the server where latency is lower (read more about this in the Advanced Server Rendering guide) but note that this can be a very big architectural change. You can have good performance even with a few query waterfalls here and there, just know they are a common performance concern and be mindful about them. An especially insidious version is when Code Splitting is involved, let's take a look at this next. ### Code Splitting Splitting an applications JS-code into smaller chunks and only loading the necessary parts is usually a critical step in achieving good performance. It does have a downside however, in that it often introduces request waterfalls. When that code split code also has a query inside it, this problem is worsened further. Consider this a slightly modified version of the Feed example. tsx // This lazy loads the GraphFeedItem component, meaning // it wont start loading until something renders it const GraphFeedItem = React.lazy(() => import('./GraphFeedItem')) function Feed() { const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: getFeed, }) if (isPending) { return 'Loading feed...' } return ( <> {data.map((feedItem) => { if (feedItem.type === 'GRAPH') { return <GraphFeedItem key={feedItem.id} feedItem={feedItem} /> } return <StandardFeedItem key={feedItem.id} feedItem={feedItem} /> })} </> ) } // GraphFeedItem.tsx function GraphFeedItem({ feedItem }) { const { data, isPending } = useQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) ... } // This lazy loads the GraphFeedItem component, meaning // it wont start loading until something renders it const GraphFeedItem = React.lazy(() => import('./GraphFeedItem')) function Feed() { const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: getFeed, }) if (isPending) { return 'Loading feed...' } return ( <> {data.map((feedItem) => { if (feedItem.type === 'GRAPH') { return <GraphFeedItem key={feedItem.id} feedItem={feedItem} /> } return <StandardFeedItem key={feedItem.id} feedItem={feedItem} /> })} </> ) } // GraphFeedItem.tsx function GraphFeedItem({ feedItem }) { const { data, isPending } = useQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) ... } This example has a double waterfall, looking like this: 1. |> getFeed() 2. |> JS for <GraphFeedItem> 3. |> getGraphDataById() 1. |> getFeed() 2. |> JS for <GraphFeedItem> 3. |> getGraphDataById() But that's just looking at the code from the example, if we consider what the first page load of this page looks like, we actually have to complete 5 round trips to the server before we can render the graph! 1. |> Markup 2. |> JS for <Feed> 3. |> getFeed() 4. |> JS for <GraphFeedItem> 5. |> getGraphDataById() 1. |> Markup 2. |> JS for <Feed> 3. |> getFeed() 4. |> JS for <GraphFeedItem> 5. |> getGraphDataById() Note that this looks a bit different when server rendering, we will explore that further in the Server Rendering & Hydration guide. Also note that it's not uncommon for the route that contains <Feed> to also be code split, which could add yet another hop. In the code split case, it might actually help to hoist the getGraphDataById query to the <Feed> component and make it conditional, or add a conditional prefetch. That query could then be fetched in parallel with the code, turning the example part into this: 1. |> getFeed() 2. |> getGraphDataById() 2. |> JS for <GraphFeedItem> 1. |> getFeed() 2. |> getGraphDataById() 2. |> JS for <GraphFeedItem> This is very much a tradeoff however. You are now including the data fetching code for getGraphDataById in the same bundle as <Feed>, so evaluate what is best for your case. Read more about how to do this in the Prefetching & Router Integration guide. > The tradeoff between: > > * Include all data fetching code in the main bundle, even if we seldom use it > * Put the data fetching code in the code split bundle, but with a request waterfall > > is not great and has been one of the motivations for Server Components. With Server Components, it's possible to avoid both, read more about how this applies to React Query in the Advanced Server Rendering guide. ## Summary and takeaways Request Waterfalls are a very common and complex performance concern with many tradeoffs. There are many ways to accidentally introduce them into your application: * Adding a query to a child, not realizing a parent already has a query * Adding a query to a parent, not realizing a child already has a query * Moving a component with descendants that has a query to a new parent with an ancestor that has a query * Etc.. Because of this accidental complexity, it pays off to be mindful of waterfalls and regularly examine your application looking for them (a good way is to examine the Network tab every now and then!). You don't necessarily have to flatten them all to have good performance, but keep an eye out for the high impact ones. In the next guide, we'll look at more ways to flatten waterfalls, by leveraging Prefetching & Router Integration. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/prefetching When you know or suspect that a certain piece of data will be needed, you can use prefetching to populate the cache with that data ahead of time, leading to a faster experience. There are a few different prefetching patterns: 1. In event handlers 2. In components 3. Via router integration 4. During Server Rendering (another form of router integration) In this guide, we'll take a look at the first three, while the fourth will be covered in depth in the Server Rendering & Hydration guide and the Advanced Server Rendering guide. One specific use of prefetching is to avoid Request Waterfalls, for an in-depth background and explanation of those, see the Performance & Request Waterfalls guide. ## prefetchQuery & prefetchInfiniteQuery Before jumping into the different specific prefetch patterns, let's look at the prefetchQuery and prefetchInfiniteQuery functions. First a few basics: * Out of the box, these functions use the default staleTime configured for the queryClient to determine whether existing data in the cache is fresh or needs to be fetched again * You can also pass a specific staleTime like this: prefetchQuery({ queryKey: \['todos'\], queryFn: fn, staleTime: 5000 }) * This staleTime is only used for the prefetch, you still need to set it for any useQuery call as well * If you want to ignore staleTime and instead always return data if it's available in the cache, you can use the ensureQueryData function. * Tip: If you are prefetching on the server, set a default staleTime higher than 0 for that queryClient to avoid having to pass in a specific staleTime to each prefetch call * If no instances of useQuery appear for a prefetched query, it will be deleted and garbage collected after the time specified in gcTime * These functions returns Promise<void> and thus never return query data. If that's something you need, use fetchQuery/fetchInfiniteQuery instead. * The prefetch functions never throws errors because they usually try to fetch again in a useQuery which is a nice graceful fallback. If you need to catch errors, use fetchQuery/fetchInfiniteQuery instead. This is how you use prefetchQuery: tsx const prefetchTodos = async () => { // The results of this query will be cached like a normal query await queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos, }) } const prefetchTodos = async () => { // The results of this query will be cached like a normal query await queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos, }) } Infinite Queries can be prefetched like regular Queries. Per default, only the first page of the Query will be prefetched and will be stored under the given QueryKey. If you want to prefetch more than one page, you can use the pages option, in which case you also have to provide a getNextPageParam function: tsx const prefetchProjects = async () => { // The results of this query will be cached like a normal query await queryClient.prefetchInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, pages: 3, // prefetch the first 3 pages }) } const prefetchProjects = async () => { // The results of this query will be cached like a normal query await queryClient.prefetchInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, pages: 3, // prefetch the first 3 pages }) } Next, let's look at how you can use these and other ways to prefetch in different situations. ## Prefetch in event handlers A straightforward form of prefetching is doing it when the user interacts with something. In this example we'll use queryClient.prefetchQuery to start a prefetch on onMouseEnter or onFocus. tsx function ShowDetailsButton() { const queryClient = useQueryClient() const prefetch = () => { queryClient.prefetchQuery({ queryKey: ['details'], queryFn: getDetailsData, // Prefetch only fires when data is older than the staleTime, // so in a case like this you definitely want to set one staleTime: 60000, }) } return ( <button onMouseEnter={prefetch} onFocus={prefetch} onClick={...}> Show Details </button> ) } function ShowDetailsButton() { const queryClient = useQueryClient() const prefetch = () => { queryClient.prefetchQuery({ queryKey: ['details'], queryFn: getDetailsData, // Prefetch only fires when data is older than the staleTime, // so in a case like this you definitely want to set one staleTime: 60000, }) } return ( <button onMouseEnter={prefetch} onFocus={prefetch} onClick={...}> Show Details </button> ) } ## Prefetch in components Prefetching during the component lifecycle is useful when we know some child or descendant will need a particular piece of data, but we can't render that until some other query has finished loading. Let's borrow an example from the Request Waterfall guide to explain: tsx function Article({ id }) { const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) if (isPending) { return 'Loading article...' } return ( <> <ArticleHeader articleData={articleData} /> <ArticleBody articleData={articleData} /> <Comments id={id} /> </> ) } function Comments({ id }) { const { data, isPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) ... } function Article({ id }) { const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) if (isPending) { return 'Loading article...' } return ( <> <ArticleHeader articleData={articleData} /> <ArticleBody articleData={articleData} /> <Comments id={id} /> </> ) } function Comments({ id }) { const { data, isPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) ... } This results in a request waterfall looking like this: 1. |> getArticleById() 2. |> getArticleCommentsById() 1. |> getArticleById() 2. |> getArticleCommentsById() As mentioned in that guide, one way to flatten this waterfall and improve performance is to hoist the getArticleCommentsById query to the parent and pass down the result as a prop, but what if this is not feasible or desirable, for example when the components are unrelated and have multiple levels between them? In that case, we can instead prefetch the query in the parent. The simplest way to do this is to use a query but ignore the result: tsx function Article({ id }) { const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) // Prefetch useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, // Optional optimization to avoid rerenders when this query changes: notifyOnChangeProps: [], }) if (isPending) { return 'Loading article...' } return ( <> <ArticleHeader articleData={articleData} /> <ArticleBody articleData={articleData} /> <Comments id={id} /> </> ) } function Comments({ id }) { const { data, isPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) ... } function Article({ id }) { const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) // Prefetch useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, // Optional optimization to avoid rerenders when this query changes: notifyOnChangeProps: [], }) if (isPending) { return 'Loading article...' } return ( <> <ArticleHeader articleData={articleData} /> <ArticleBody articleData={articleData} /> <Comments id={id} /> </> ) } function Comments({ id }) { const { data, isPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) ... } This starts fetching 'article-comments' immediately and flattens the waterfall: 1. |> getArticleById() 1. |> getArticleCommentsById() 1. |> getArticleById() 1. |> getArticleCommentsById() If you want to prefetch together with Suspense, you will have to do things a bit differently. You can't use useSuspenseQueries to prefetch, since the prefetch would block the component from rendering. You also can not use useQuery for the prefetch, because that wouldn't start the prefetch until after suspenseful query had resolved. For this scenario, you can use the usePrefetchQuery or the usePrefetchInfiniteQuery hooks available in the library. You can now use useSuspenseQuery in the component that actually needs the data. You _might_ want to wrap this later component in its own <Suspense> boundary so the "secondary" query we are prefetching does not block rendering of the "primary" data. tsx function ArticleLayout({ id }) { usePrefetchQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) return ( <Suspense fallback="Loading article"> <Article id={id} /> </Suspense> ) } function Article({ id }) { const { data: articleData, isPending } = useSuspenseQuery({ queryKey: ['article', id], queryFn: getArticleById, }) ... } function ArticleLayout({ id }) { usePrefetchQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) return ( <Suspense fallback="Loading article"> <Article id={id} /> </Suspense> ) } function Article({ id }) { const { data: articleData, isPending } = useSuspenseQuery({ queryKey: ['article', id], queryFn: getArticleById, }) ... } Another way is to prefetch inside of the query function. This makes sense if you know that every time an article is fetched it's very likely comments will also be needed. For this, we'll use queryClient.prefetchQuery: tsx const queryClient = useQueryClient() const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: (...args) => { queryClient.prefetchQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) return getArticleById(...args) }, }) const queryClient = useQueryClient() const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: (...args) => { queryClient.prefetchQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) return getArticleById(...args) }, }) Prefetching in an effect also works, but note that if you are using useSuspenseQuery in the same component, this effect wont run until _after_ the query finishes which might not be what you want. tsx const queryClient = useQueryClient() useEffect(() => { queryClient.prefetchQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) }, [queryClient, id]) const queryClient = useQueryClient() useEffect(() => { queryClient.prefetchQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) }, [queryClient, id]) To recap, if you want to prefetch a query during the component lifecycle, there are a few different ways to do it, pick the one that suits your situation best: * Prefetch before a suspense boundary using usePrefetchQuery or usePrefetchInfiniteQuery hooks * Use useQuery or useSuspenseQueries and ignore the result * Prefetch inside the query function * Prefetch in an effect Let's look at a slightly more advanced case next. ### Dependent Queries & Code Splitting Sometimes we want to prefetch conditionally, based on the result of another fetch. Consider this example borrowed from the Performance & Request Waterfalls guide: tsx // This lazy loads the GraphFeedItem component, meaning // it wont start loading until something renders it const GraphFeedItem = React.lazy(() => import('./GraphFeedItem')) function Feed() { const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: getFeed, }) if (isPending) { return 'Loading feed...' } return ( <> {data.map((feedItem) => { if (feedItem.type === 'GRAPH') { return <GraphFeedItem key={feedItem.id} feedItem={feedItem} /> } return <StandardFeedItem key={feedItem.id} feedItem={feedItem} /> })} </> ) } // GraphFeedItem.tsx function GraphFeedItem({ feedItem }) { const { data, isPending } = useQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) ... } // This lazy loads the GraphFeedItem component, meaning // it wont start loading until something renders it const GraphFeedItem = React.lazy(() => import('./GraphFeedItem')) function Feed() { const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: getFeed, }) if (isPending) { return 'Loading feed...' } return ( <> {data.map((feedItem) => { if (feedItem.type === 'GRAPH') { return <GraphFeedItem key={feedItem.id} feedItem={feedItem} /> } return <StandardFeedItem key={feedItem.id} feedItem={feedItem} /> })} </> ) } // GraphFeedItem.tsx function GraphFeedItem({ feedItem }) { const { data, isPending } = useQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) ... } As noted over in that guide, this example leads to the following double request waterfall: 1. |> getFeed() 2. |> JS for <GraphFeedItem> 3. |> getGraphDataById() 1. |> getFeed() 2. |> JS for <GraphFeedItem> 3. |> getGraphDataById() If we can not restructure our API so getFeed() also returns the getGraphDataById() data when necessary, there is no way to get rid of the getFeed->getGraphDataById waterfall, but by leveraging conditional prefetching, we can at least load the code and data in parallel. Just like described above, there are multiple ways to do this, but for this example, we'll do it in the query function: tsx function Feed() { const queryClient = useQueryClient() const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: async (...args) => { const feed = await getFeed(...args) for (const feedItem of feed) { if (feedItem.type === 'GRAPH') { queryClient.prefetchQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) } } return feed } }) ... } function Feed() { const queryClient = useQueryClient() const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: async (...args) => { const feed = await getFeed(...args) for (const feedItem of feed) { if (feedItem.type === 'GRAPH') { queryClient.prefetchQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) } } return feed } }) ... } This would load the code and data in parallel: 1. |> getFeed() 2. |> JS for <GraphFeedItem> 2. |> getGraphDataById() 1. |> getFeed() 2. |> JS for <GraphFeedItem> 2. |> getGraphDataById() There is a tradeoff however, in that the code for getGraphDataById is now included in the parent bundle instead of in JS for <GraphFeedItem> so you'll need to determine what's the best performance tradeoff on a case by case basis. If GraphFeedItem are likely, it's probably worth to include the code in the parent. If they are exceedingly rare, it's probably not. ## Router Integration Because data fetching in the component tree itself can easily lead to request waterfalls and the different fixes for that can be cumbersome as they accumulate throughout the application, an attractive way to do prefetching is integrating it at the router level. In this approach, you explicitly declare for each _route_ what data is going to be needed for that component tree, ahead of time. Because Server Rendering has traditionally needed all data to be loaded before rendering starts, this has been the dominating approach for SSR'd apps for a long time. This is still a common approach and you can read more about it in the Server Rendering & Hydration guide. For now, let's focus on the client side case and look at an example of how you can make this work with Tanstack Router. These examples leave out a lot of setup and boilerplate to stay concise, you can check out a full React Query example over in the Tanstack Router docs. When integrating at the router level, you can choose to either _block_ rendering of that route until all data is present, or you can start a prefetch but not await the result. That way, you can start rendering the route as soon as possible. You can also mix these two approaches and await some critical data, but start rendering before all the secondary data has finished loading. In this example, we'll configure an /article route to not render until the article data has finished loading, as well as start prefetching comments as soon as possible, but not block rendering the route if comments haven't finished loading yet. tsx const queryClient = new QueryClient() const routerContext = new RouterContext() const rootRoute = routerContext.createRootRoute({ component: () => { ... } }) const articleRoute = new Route({ getParentRoute: () => rootRoute, path: 'article', beforeLoad: () => { return { articleQueryOptions: { queryKey: ['article'], queryFn: fetchArticle }, commentsQueryOptions: { queryKey: ['comments'], queryFn: fetchComments }, } }, loader: async ({ context: { queryClient }, routeContext: { articleQueryOptions, commentsQueryOptions }, }) => { // Fetch comments asap, but don't block queryClient.prefetchQuery(commentsQueryOptions) // Don't render the route at all until article has been fetched await queryClient.prefetchQuery(articleQueryOptions) }, component: ({ useRouteContext }) => { const { articleQueryOptions, commentsQueryOptions } = useRouteContext() const articleQuery = useQuery(articleQueryOptions) const commentsQuery = useQuery(commentsQueryOptions) return ( ... ) }, errorComponent: () => 'Oh crap!', }) const queryClient = new QueryClient() const routerContext = new RouterContext() const rootRoute = routerContext.createRootRoute({ component: () => { ... } }) const articleRoute = new Route({ getParentRoute: () => rootRoute, path: 'article', beforeLoad: () => { return { articleQueryOptions: { queryKey: ['article'], queryFn: fetchArticle }, commentsQueryOptions: { queryKey: ['comments'], queryFn: fetchComments }, } }, loader: async ({ context: { queryClient }, routeContext: { articleQueryOptions, commentsQueryOptions }, }) => { // Fetch comments asap, but don't block queryClient.prefetchQuery(commentsQueryOptions) // Don't render the route at all until article has been fetched await queryClient.prefetchQuery(articleQueryOptions) }, component: ({ useRouteContext }) => { const { articleQueryOptions, commentsQueryOptions } = useRouteContext() const articleQuery = useQuery(articleQueryOptions) const commentsQuery = useQuery(commentsQueryOptions) return ( ... ) }, errorComponent: () => 'Oh crap!', }) Integration with other routers is also possible, see the React Router example for another demonstration. ## Manually Priming a Query If you already have the data for your query synchronously available, you don't need to prefetch it. You can just use the Query Client's setQueryData method to directly add or update a query's cached result by key. tsx queryClient.setQueryData(['todos'], todos) queryClient.setQueryData(['todos'], todos) ## Further reading For a deep-dive on how to get data into your Query Cache before you fetch, have a look at #17: Seeding the Query Cache from the Community Resources. Integrating with Server Side routers and frameworks is very similar to what we just saw, with the addition that the data has to passed from the server to the client to be hydrated into the cache there. To learn how, continue on to the Server Rendering & Hydration guide. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/ssr In this guide you'll learn how to use React Query with server rendering. See the guide on Prefetching & Router Integration for some background. You might also want to check out the Performance & Request Waterfalls guide before that. For advanced server rendering patterns, such as streaming, Server Components and the new Next.js app router, see the Advanced Server Rendering guide. If you just want to see some code, you can skip ahead to the Full Next.js pages router example or the Full Remix example below. ## Server Rendering & React Query So what is server rendering anyway? The rest of this guide will assume you are familiar with the concept, but let's spend some time to look at how it relates to React Query. Server rendering is the act of generating the initial html on the server, so that the user has some content to look at as soon as the page loads. This can happen on demand when a page is requested (SSR). It can also happen ahead of time either because a previous request was cached, or at build time (SSG). If you've read the Request Waterfalls guide, you might remember this: 1. |-> Markup (without content) 2. |-> JS 3. |-> Query 1. |-> Markup (without content) 2. |-> JS 3. |-> Query With a client rendered application, these are the minimum 3 server roundtrips you will need to make before getting any content on the screen for the user. One way of viewing server rendering is that it turns the above into this: 1. |-> Markup (with content AND initial data) 2. |-> JS 1. |-> Markup (with content AND initial data) 2. |-> JS As soon as **1.** is complete, the user can see the content and when **2.** finishes, the page is interactive and clickable. Because the markup also contains the initial data we need, step **3.** does not need to run on the client at all, at least until you want to revalidate the data for some reason. This is all from the clients perspective. On the server, we need to **prefetch** that data before we generate/render the markup, we need to **dehydrate** that data into a serializable format we can embed in the markup, and on the client we need to **hydrate** that data into a React Query cache so we can avoid doing a new fetch on the client. Read on to learn how to implement these three steps with React Query. ## A quick note on Suspense This guide uses the regular useQuery API. While we don't necessarily recommend it, it is possible to replace this with useSuspenseQuery instead **as long as you always prefetch all your queries**. The upside is that you get to use <Suspense> for loading states on the client. If you do forget to prefetch a query when you are using useSuspenseQuery, the consequences will depend on the framework you are using. In some cases, the data will Suspend and get fetched on the server but never be hydrated to the client, where it will fetch again. In these cases you will get a markup hydration mismatch, because the server and the client tried to render different things. ## Initial setup The first steps of using React Query is always to create a queryClient and wrap the application in a <QueryClientProvider>. When doing server rendering, it's important to create the queryClient instance **inside of your app**, in React state (an instance ref works fine too). **This ensures that data is not shared between different users and requests**, while still only creating the queryClient once per component lifecycle. Next.js pages router: tsx // _app.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' // NEVER DO THIS: // const queryClient = new QueryClient() // // Creating the queryClient at the file root level makes the cache shared // between all requests and means _all_ data gets passed to _all_ users. // Besides being bad for performance, this also leaks any sensitive data. export default function MyApp({ Component, pageProps }) { // Instead do this, which ensures each request has its own cache: const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> ) } // _app.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' // NEVER DO THIS: // const queryClient = new QueryClient() // // Creating the queryClient at the file root level makes the cache shared // between all requests and means _all_ data gets passed to _all_ users. // Besides being bad for performance, this also leaks any sensitive data. export default function MyApp({ Component, pageProps }) { // Instead do this, which ensures each request has its own cache: const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> ) } Remix: tsx // app/root.tsx import { Outlet } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp() { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Outlet /> </QueryClientProvider> ) } // app/root.tsx import { Outlet } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp() { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Outlet /> </QueryClientProvider> ) } ## Get started fast with initialData The quickest way to get started is to not involve React Query at all when it comes to prefetching and not use the dehydrate/hydrate APIs. What you do instead is passing the raw data in as the initialData option to useQuery. Let's look at an example using Next.js pages router, using getServerSideProps. tsx export async function getServerSideProps() { const posts = await getPosts() return { props: { posts } } } function Posts(props) { const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: props.posts, }) // ... } export async function getServerSideProps() { const posts = await getPosts() return { props: { posts } } } function Posts(props) { const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: props.posts, }) // ... } This also works with getStaticProps or even the older getInitialProps and the same pattern can be applied in any other framework that has equivalent functions. This is what the same example looks like with Remix: tsx export async function loader() { const posts = await getPosts() return json({ posts }) } function Posts() { const { posts } = useLoaderData<typeof loader>() const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: posts, }) // ... } export async function loader() { const posts = await getPosts() return json({ posts }) } function Posts() { const { posts } = useLoaderData<typeof loader>() const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: posts, }) // ... } The setup is minimal and this can be a quick solution for some cases, but there are a **few tradeoffs to consider** when compared to the full approach: * If you are calling useQuery in a component deeper down in the tree you need to pass the initialData down to that point * If you are calling useQuery with the same query in multiple locations, passing initialData to only one of them can be brittle and break when your app changes since. If you remove or move the component that has the useQuery with initialData, the more deeply nested useQuery might no longer have any data. Passing initialData to **all** queries that needs it can also be cumbersome. * There is no way to know at what time the query was fetched on the server, so dataUpdatedAt and determining if the query needs refetching is based on when the page loaded instead * If there is already data in the cache for a query, initialData will never overwrite this data, **even if the new data is fresher than the old one**. * To understand why this is especially bad, consider the getServerSideProps example above. If you navigate back and forth to a page several times, getServerSideProps would get called each time and fetch new data, but because we are using the initialData option, the client cache and data would never be updated. Setting up the full hydration solution is straightforward and does not have these drawbacks, this will be the focus for the rest of the documentation. ## Using the Hydration APIs With just a little more setup, you can use a queryClient to prefetch queries during a preload phase, pass a serialized version of that queryClient to the rendering part of the app and reuse it there. This avoids the drawbacks above. Feel free to skip ahead for full Next.js pages router and Remix examples, but at a general level these are the extra steps: * In the framework loader function, create a const queryClient = new QueryClient(options) * In the loader function, do await queryClient.prefetchQuery(...) for each query you want to prefetch * You want to use await Promise.all(...) to fetch the queries in parallel when possible * It's fine to have queries that aren't prefetched. These wont be server rendered, instead they will be fetched on the client after the application is interactive. This can be great for content that are shown only after user interaction, or is far down on the page to avoid blocking more critical content. * From the loader, return dehydrate(queryClient), note that the exact syntax to return this differs between frameworks * Wrap your tree with <HydrationBoundary state={dehydratedState}> where dehydratedState comes from the framework loader. How you get dehydratedState also differs between frameworks. * This can be done for each route, or at the top of the application to avoid boilerplate, see examples > An interesting detail is that there are actually _three_ queryClients involved. The framework loaders are a form of "preloading" phase that happens before rendering, and this phase has its own queryClient that does the prefetching. The dehydrated result of this phase gets passed to **both** the server rendering process **and** the client rendering process which each has its own queryClient. This ensures they both start with the same data so they can return the same markup. > Server Components are another form of "preloading" phase, that can also "preload" (pre-render) parts of a React component tree. Read more in the Advanced Server Rendering guide. ### Full Next.js pages router example > For app router documentation, see the Advanced Server Rendering guide. Initial setup: tsx // _app.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> ) } // _app.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> ) } In each route: tsx // pages/posts.tsx import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' // This could also be getServerSideProps export async function getStaticProps() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return { props: { dehydratedState: dehydrate(queryClient), }, } } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } // pages/posts.tsx import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' // This could also be getServerSideProps export async function getStaticProps() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return { props: { dehydratedState: dehydrate(queryClient), }, } } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } ### Full Remix example Initial setup: tsx // app/root.tsx import { Outlet } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp() { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Outlet /> </QueryClientProvider> ) } // app/root.tsx import { Outlet } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp() { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Outlet /> </QueryClientProvider> ) } In each route, note that it's fine to do this in nested routes too: tsx // app/routes/posts.tsx import { json } from '@remix-run/node' import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' export async function loader() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return json({ dehydratedState: dehydrate(queryClient) }) } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute() { const { dehydratedState } = useLoaderData<typeof loader>() return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } // app/routes/posts.tsx import { json } from '@remix-run/node' import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' export async function loader() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return json({ dehydratedState: dehydrate(queryClient) }) } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute() { const { dehydratedState } = useLoaderData<typeof loader>() return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } ## Optional - Remove boilerplate Having this part in every route might seem like a lot of boilerplate: tsx export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } While there is nothing wrong with this approach, if you want to get rid of this boilerplate, here's how you can modify your setup in Next.js: tsx // _app.tsx import { HydrationBoundary, QueryClient, QueryClientProvider, } from '@tanstack/react-query' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState(() => new QueryClient()) return ( <QueryClientProvider client={queryClient}> <HydrationBoundary state={pageProps.dehydratedState}> <Component {...pageProps} /> </HydrationBoundary> </QueryClientProvider> ) } // pages/posts.tsx // Remove PostsRoute with the HydrationBoundary and instead export Posts directly: export default function Posts() { ... } // _app.tsx import { HydrationBoundary, QueryClient, QueryClientProvider, } from '@tanstack/react-query' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState(() => new QueryClient()) return ( <QueryClientProvider client={queryClient}> <HydrationBoundary state={pageProps.dehydratedState}> <Component {...pageProps} /> </HydrationBoundary> </QueryClientProvider> ) } // pages/posts.tsx // Remove PostsRoute with the HydrationBoundary and instead export Posts directly: export default function Posts() { ... } With Remix, this is a little bit more involved, we recommend checking out the use-dehydrated-state package. ## Prefetching dependent queries Over in the Prefetching guide we learned how to prefetch dependent queries, but how do we do this in framework loaders? Consider the following code, taken from the Dependent Queries guide: tsx // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) How would we prefetch this so it can be server rendered? Here's an example: tsx // For Remix, rename this to loader instead export async function getServerSideProps() { const queryClient = new QueryClient() const user = await queryClient.fetchQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) if (user?.userId) { await queryClient.prefetchQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, }) } // For Remix: // return json({ dehydratedState: dehydrate(queryClient) }) return { props: { dehydratedState: dehydrate(queryClient) } } } // For Remix, rename this to loader instead export async function getServerSideProps() { const queryClient = new QueryClient() const user = await queryClient.fetchQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) if (user?.userId) { await queryClient.prefetchQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, }) } // For Remix: // return json({ dehydratedState: dehydrate(queryClient) }) return { props: { dehydratedState: dehydrate(queryClient) } } } This can get more complex of course, but since these loader functions are just JavaScript, you can use the full power of the language to build your logic. Make sure you prefetch all queries that you want to be server rendered. ## Error handling React Query defaults to a graceful degradation strategy. This means: * queryClient.prefetchQuery(...) never throws errors * dehydrate(...) only includes successful queries, not failed ones This will lead to any failed queries being retried on the client and that the server rendered output will include loading states instead of the full content. While a good default, sometimes this is not what you want. When critical content is missing, you might want to respond with a 404 or 500 status code depending on the situation. For these cases, use queryClient.fetchQuery(...) instead, which will throw errors when it fails, letting you handle things in a suitable way. tsx let result try { result = await queryClient.fetchQuery(...) } catch (error) { // Handle the error, refer to your framework documentation } // You might also want to check and handle any invalid `result` here let result try { result = await queryClient.fetchQuery(...) } catch (error) { // Handle the error, refer to your framework documentation } // You might also want to check and handle any invalid `result` here If you for some reason want to include failed queries in the dehydrated state to avoid retries, you can use the option shouldDehydrateQuery to override the default function and implement your own logic: tsx dehydrate(queryClient, { shouldDehydrateQuery: (query) => { // This will include all queries, including failed ones, // but you can also implement your own logic by inspecting `query` return true }, }) dehydrate(queryClient, { shouldDehydrateQuery: (query) => { // This will include all queries, including failed ones, // but you can also implement your own logic by inspecting `query` return true }, }) ## Serialization When doing return { props: { dehydratedState: dehydrate(queryClient) } } in Next.js, or return json({ dehydratedState: dehydrate(queryClient) }) in Remix, what happens is that the dehydratedState representation of the queryClient is serialized by the framework so it can be embedded into the markup and transported to the client. By default, these frameworks only supports returning things that are safely serializable/parsable, and therefore does not support undefined, Error, Date, Map, Set, BigInt, Infinity, NaN, \-0, regular expressions etc. This also means that you can not return any of these things from your queries. If returning these values is something you want, check out superjson or similar packages. If you are using a custom SSR setup, you need to take care of this step yourself. Your first instinct might be to use JSON.stringify(dehydratedState), but because this doesn't escape things like <script>alert('Oh no..')</script> by default, this can easily lead to **XSS-vulnerabilities** in your application. superjson also **does not** escape values and is unsafe to use by itself in a custom SSR setup (unless you add an extra step for escaping the output). Instead we recommend using a library like Serialize JavaScript or devalue which are both safe against XSS injections out of the box. ## A note about request waterfalls In the Performance & Request Waterfalls guide we mentioned we would revisit how server rendering changes one of the more complex nested waterfalls. Check back for the specific code example, but as a refresher, we have a code split <GraphFeedItem> component inside a <Feed> component. This only renders if the feed contains a graph item and both of these components fetches their own data. With client rendering, this leads to the following request waterfall: 1. |> Markup (without content) 2. |> JS for <Feed> 3. |> getFeed() 4. |> JS for <GraphFeedItem> 5. |> getGraphDataById() 1. |> Markup (without content) 2. |> JS for <Feed> 3. |> getFeed() 4. |> JS for <GraphFeedItem> 5. |> getGraphDataById() The nice thing about server rendering is that we can turn the above into: 1. |> Markup (with content AND initial data) 2. |> JS for <Feed> 2. |> JS for <GraphFeedItem> 1. |> Markup (with content AND initial data) 2. |> JS for <Feed> 2. |> JS for <GraphFeedItem> Note that the queries are no longer fetched on the client, instead their data was included in the markup. The reason we can now load the JS in parallel is that since <GraphFeedItem> was rendered on the server we know that we are going to need this JS on the client as well and can insert a script-tag for this chunk in the markup. On the server, we would still have this request waterfall: 1. |> getFeed() 2. |> getGraphDataById() 1. |> getFeed() 2. |> getGraphDataById() We simply can not know before we have fetched the feed if we also need to fetch graph data, they are dependent queries. Because this happens on the server where latency is generally both lower and more stable, this often isn't such a big deal. Amazing, we've mostly flattened our waterfalls! There's a catch though. Let's call this page the /feed page, and let's pretend we also have another page like /posts. If we type in www.example.com/feed directly in the url bar and hit enter, we get all these great server rendering benefits, BUT, if we instead type in www.example.com/posts and then **click a link** to /feed, we're back to to this: 1. |> JS for <Feed> 2. |> getFeed() 3. |> JS for <GraphFeedItem> 4. |> getGraphDataById() 1. |> JS for <Feed> 2. |> getFeed() 3. |> JS for <GraphFeedItem> 4. |> getGraphDataById() This is because with SPA's, server rendering only works for the initial page load, not for any subsequent navigation. Modern frameworks often try to solve this by fetching the initial code and data in parallel, so if you were using Next.js or Remix with the prefetching patterns we outlined in this guide, including how to prefetch dependent queries, it would actually look like this instead: 1. |> JS for <Feed> 1. |> getFeed() + getGraphDataById() 2. |> JS for <GraphFeedItem> 1. |> JS for <Feed> 1. |> getFeed() + getGraphDataById() 2. |> JS for <GraphFeedItem> This is much better, but if we want to improve this further we can flatten this to a single roundtrip with Server Components. Learn how in the Advanced Server Rendering guide. ## Tips, Tricks and Caveats ### Staleness is measured from when the query was fetched on the server A query is considered stale depending on when it was dataUpdatedAt. A caveat here is that the server needs to have the correct time for this to work properly, but UTC time is used, so timezones do not factor into this. Because staleTime defaults to 0, queries will be refetched in the background on page load by default. You might want to use a higher staleTime to avoid this double fetching, especially if you don't cache your markup. This refetching of stale queries is a perfect match when caching markup in a CDN! You can set the cache time of the page itself decently high to avoid having to re-render pages on the server, but configure the staleTime of the queries lower to make sure data is refetched in the background as soon as a user visits the page. Maybe you want to cache the pages for a week, but refetch the data automatically on page load if it's older than a day? ### High memory consumption on server In case you are creating the QueryClient for every request, React Query creates the isolated cache for this client, which is preserved in memory for the gcTime period. That may lead to high memory consumption on server in case of high number of requests during that period. On the server, gcTime defaults to Infinity which disables manual garbage collection and will automatically clear memory once a request has finished. If you are explicitly setting a non-Infinity gcTime then you will be responsible for clearing the cache early. Avoid setting gcTime to 0 as it may result in a hydration error. This occurs because the Hydration Boundary places necessary data into the cache for rendering, but if the garbage collector removes the data before the rendering completes, issues may arise. If you require a shorter gcTime, we recommend setting it to 2 \* 1000 to allow sufficient time for the app to reference the data. To clear the cache after it is not needed and to lower memory consumption, you can add a call to queryClient.clear() after the request is handled and dehydrated state has been sent to the client. Alternatively, you can set a smaller gcTime. ### Caveat for Next.js rewrites There's a catch if you're using Next.js' rewrites feature together with Automatic Static Optimization or getStaticProps: It will cause a second hydration by React Query. That's because Next.js needs to ensure that they parse the rewrites on the client and collect any params after hydration so that they can be provided in router.query. The result is missing referential equality for all the hydration data, which for example triggers wherever your data is used as props of components or in the dependency array of useEffects/useMemos. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr Welcome to the Advanced Server Rendering guide, where you will learn all about using React Query with streaming, Server Components and the Next.js app router. You might want to read the Server Rendering & Hydration guide before this one as it teaches the basics for using React Query with SSR, and Performance & Request Waterfalls as well as Prefetching & Router Integration also contains valuable background. Before we start, let's note that while the initialData approach outlined in the SSR guide also works with Server Components, we'll focus this guide on the hydration APIs. ## Server Components & Next.js app router We won't cover Server Components in depth here, but the short version is that they are components that are guaranteed to _only_ run on the server, both for the initial page view and **also on page transitions**. This is similar to how Next.js getServerSideProps/getStaticProps and Remix loader works, as these also always run on the server but while those can only return data, Server Components can do a lot more. The data part is central to React Query however, so let's focus on that. How do we take what we learned in the Server Rendering guide about passing data prefetched in framework loaders to the app and apply that to Server Components and the Next.js app router? The best way to start thinking about this is to consider Server Components as "just" another framework loader. ### A quick note on terminology So far in these guides, we've been talking about the _server_ and the _client_. It's important to note that confusingly enough this does not match 1-1 with _Server Components_ and _Client Components_. Server Components are guaranteed to only run on the server, but Client Components can actually run in both places. The reason for this is that they can also render during the initial _server rendering_ pass. One way to think of this is that even though Server Components also _render_, they happen during a "loader phase" (always happens on the server), while Client Components run during the "application phase". That application can run both on the server during SSR, and in for example a browser. Where exactly that application runs and if it runs during SSR or not might differ between frameworks. ### Initial setup The first step of any React Query setup is always to create a queryClient and wrap your application in a QueryClientProvider. With Server Components, this looks mostly the same across frameworks, one difference being the filename conventions: tsx // In Next.js, this file would be called: app/providers.tsx 'use client' // Since QueryClientProvider relies on useContext under the hood, we have to put 'use client' on top import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export default function Providers({ children }: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> ) } // In Next.js, this file would be called: app/providers.tsx 'use client' // Since QueryClientProvider relies on useContext under the hood, we have to put 'use client' on top import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export default function Providers({ children }: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> ) } tsx // In Next.js, this file would be called: app/layout.tsx import Providers from './providers' export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <head /> <body> <Providers>{children}</Providers> </body> </html> ) } // In Next.js, this file would be called: app/layout.tsx import Providers from './providers' export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <head /> <body> <Providers>{children}</Providers> </body> </html> ) } This part is pretty similar to what we did in the SSR guide, we just need to split things up into two different files. ### Prefetching and de/hydrating data Let's next look at how to actually prefetch data and dehydrate and hydrate it. This is what it looked like using the **Next.js pages router**: tsx // pages/posts.tsx import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' // This could also be getServerSideProps export async function getStaticProps() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return { props: { dehydratedState: dehydrate(queryClient), }, } } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way // // Note that we are using useQuery here instead of useSuspenseQuery. // Because this data has already been prefetched, there is no need to // ever suspend in the component itself. If we forget or remove the // prefetch, this will instead fetch the data on the client, while // using useSuspenseQuery would have had worse side effects. const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } // pages/posts.tsx import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' // This could also be getServerSideProps export async function getStaticProps() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return { props: { dehydratedState: dehydrate(queryClient), }, } } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way // // Note that we are using useQuery here instead of useSuspenseQuery. // Because this data has already been prefetched, there is no need to // ever suspend in the component itself. If we forget or remove the // prefetch, this will instead fetch the data on the client, while // using useSuspenseQuery would have had worse side effects. const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } Converting this to the app router actually looks pretty similar, we just need to move things around a bit. First, we'll create a Server Component to do the prefetching part: tsx // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Posts from './posts' export default async function PostsPage() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( // Neat! Serialization is now as easy as passing props. // HydrationBoundary is a Client Component, so hydration will happen there. <HydrationBoundary state={dehydrate(queryClient)}> <Posts /> </HydrationBoundary> ) } // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Posts from './posts' export default async function PostsPage() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( // Neat! Serialization is now as easy as passing props. // HydrationBoundary is a Client Component, so hydration will happen there. <HydrationBoundary state={dehydrate(queryClient)}> <Posts /> </HydrationBoundary> ) } Next, we'll look at what the Client Component part looks like: tsx // app/posts/posts.tsx 'use client' export default function Posts() { // This useQuery could just as well happen in some deeper // child to <Posts>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: () => getPosts(), }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix. const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } // app/posts/posts.tsx 'use client' export default function Posts() { // This useQuery could just as well happen in some deeper // child to <Posts>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: () => getPosts(), }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix. const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } One neat thing about the examples above is that the only thing that is Next.js-specific here are the file names, everything else would look the same in any other framework that supports Server Components. In the SSR guide, we noted that you could get rid of the boilerplate of having <HydrationBoundary> in every route. This is not possible with Server Components. > NOTE: If you encounter a type error while using async Server Components with TypeScript versions lower than 5.1.3 and @types/react versions lower than 18.2.8, it is recommended to update to the latest versions of both. Alternatively, you can use the temporary workaround of adding {/\* @ts-expect-error Server Component \*/} when calling this component inside another. For more information, see Async Server Component TypeScript Error in the Next.js 13 docs. > NOTE: If you encounter an error Only plain objects, and a few built-ins, can be passed to Server Actions. Classes or null prototypes are not supported. make sure that you're **not** passing to queryFn a function reference, instead call the function because queryFn args has a bunch of properties and not all of it would be serializable. see Server Action only works when queryFn isn't a reference. ### Nesting Server Components A nice thing about Server Components is that they can be nested and exist on many levels in the React tree, making it possible to prefetch data closer to where it's actually used instead of only at the top of the application (just like Remix loaders). This can be as simple as a Server Component rendering another Server Component (we'll leave the Client Components out in this example for brevity): tsx // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Posts from './posts' import CommentsServerComponent from './comments-server' export default async function PostsPage() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( <HydrationBoundary state={dehydrate(queryClient)}> <Posts /> <CommentsServerComponent /> </HydrationBoundary> ) } // app/posts/comments-server.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Comments from './comments' export default async function CommentsServerComponent() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) return ( <HydrationBoundary state={dehydrate(queryClient)}> <Comments /> </HydrationBoundary> ) } // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Posts from './posts' import CommentsServerComponent from './comments-server' export default async function PostsPage() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( <HydrationBoundary state={dehydrate(queryClient)}> <Posts /> <CommentsServerComponent /> </HydrationBoundary> ) } // app/posts/comments-server.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Comments from './comments' export default async function CommentsServerComponent() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) return ( <HydrationBoundary state={dehydrate(queryClient)}> <Comments /> </HydrationBoundary> ) } As you can see, it's perfectly fine to use <HydrationBoundary> in multiple places, and create and dehydrate multiple queryClient for prefetching. Note that because we are awaiting getPosts before rendering CommentsServerComponent this would lead to a server side waterfall: 1. |> getPosts() 2. |> getComments() 1. |> getPosts() 2. |> getComments() If the server latency to the data is low, this might not be a huge issue, but is still worth pointing out. In Next.js, besides prefetching data in page.tsx, you can also do it in layout.tsx, and in parallel routes. Because these are all part of the routing, Next.js knows how to fetch them all in parallel. So if CommentsServerComponent above was instead expressed as a parallel route, the waterfall would be flattened automatically. As more frameworks start supporting Server Components, they might have other routing conventions. Read your framework docs for details. ### Alternative: Use a single queryClient for prefetching In the example above, we create a new queryClient for each Server Component that fetches data. This is the recommended approach, but if you want to, you can alternatively create a single one that is reused across all Server Components: tsx // app/getQueryClient.tsx import { QueryClient } from '@tanstack/react-query' import { cache } from 'react' // cache() is scoped per request, so we don't leak data between requests const getQueryClient = cache(() => new QueryClient()) export default getQueryClient // app/getQueryClient.tsx import { QueryClient } from '@tanstack/react-query' import { cache } from 'react' // cache() is scoped per request, so we don't leak data between requests const getQueryClient = cache(() => new QueryClient()) export default getQueryClient The benefit of this is that you can call getQueryClient() to get a hold of this client anywhere that gets called from a Server Component, including utility functions. The downside is that every time you call dehydrate(getQueryClient()), you serialize _the entire_ queryClient, including queries that have already been serialized before and are unrelated to the current Server Component which is unnecessary overhead. Next.js already dedupes requests that utilize fetch(), but if you are using something else in your queryFn, or if you use a framework that does _not_ dedupe these requests automatically, using a single queryClient as described above might make sense, despite the duplicated serialization. > As a future improvement, we might look into creating a dehydrateNew() function (name pending) that only dehydrate queries that are _new_ since the last call to dehydrateNew(). Feel free to get in touch if this sounds interesting and like something you want to help out with! ### Data ownership and revalidation With Server Components, it's important to think about data ownership and revalidation. To explain why, let's look at a modified example from above: tsx // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Posts from './posts' export default async function PostsPage() { const queryClient = new QueryClient() // Note we are now using fetchQuery() const posts = await queryClient.fetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( <HydrationBoundary state={dehydrate(queryClient)}> {/* This is the new part */} <div>Nr of posts: {posts.length}</div> <Posts /> </HydrationBoundary> ) } // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Posts from './posts' export default async function PostsPage() { const queryClient = new QueryClient() // Note we are now using fetchQuery() const posts = await queryClient.fetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( <HydrationBoundary state={dehydrate(queryClient)}> {/* This is the new part */} <div>Nr of posts: {posts.length}</div> <Posts /> </HydrationBoundary> ) } We are now rendering data from the getPosts query both in a Server Component and in a Client Component. This will be fine for the initial page render, but what happens when the query revalidates on the client for some reason when staleTime has been passed? React Query has no idea of how to _revalidate the Server Component_, so if it refetches the data on the client, causing React to rerender the list of posts, the Nr of posts: {posts.length} will end up out of sync. This is fine if you set staleTime: Infinity, so that React Query never revalidates, but this is probably not what you want if you are using React Query in the first place. Using React Query with Server Components makes most sense if: * You have an app using React Query and want to migrate to Server Components without rewriting all the data fetching * You want a familiar programming paradigm, but want to still sprinkle in the benefits of Server Components where it makes most sense * You have some use case that React Query covers, but that your framework of choice does not cover It's hard to give general advice on when it makes sense to pair React Query with Server Components and not. **If you are just starting out with a new Server Components app, we suggest you start out with any tools for data fetching your framework provides you with and avoid bringing in React Query until you actually need it.** This might be never, and that's fine, use the right tool for the job! If you do use it, a good rule of thumb is to avoid queryClient.fetchQuery unless you need to catch errors. If you do use it, don't render its result on the server or pass the result to another component, even a Client Component one. From the React Query perspective, treat Server Components as a place to prefetch data, nothing more. Of course, it's fine to have Server Components own some data, and Client Components own other, just make sure those two realities don't get out of sync. ## Streaming with Server Components The Next.js app router automatically streams any part of the application that is ready to be displayed to the browser as soon as possible, so finished content can be displayed immediately without waiting for still pending content. It does this along <Suspense> boundary lines. Note that if you create a file loading.tsx, this automatically creates a <Suspense> boundary behind the scenes. With the prefetching patterns described above, React Query is perfectly compatible with this form of streaming. As the data for each Suspense boundary resolves, Next.js can render and stream the finished content to the browser. This works even if you are using useQuery as outlined above because the suspending actually happens when you await the prefetch. As of React Query v5.40.0, you don't have to await all prefetches for this to work, as pending Queries can also be dehydrated and sent to the client. This lets you kick off prefetches as early as possible without letting them block an entire Suspense boundary, and streams the _data_ to the client as the query finishes. This can be useful for example if you want to prefetch some content that is only visible after some user interaction, or say if you want to await and render the first page of an infinite query, but start prefetching page 2 without blocking rendering. To make this work, we have to instruct the queryClient to also dehydrate pending Queries. We can do this globally, or by passing that option directly to dehydrate. We will also need to move the getQueryClient() function out of our app/providers.tsx file as we want to use it in our server component and our client provider. tsx // app/get-query-client.ts import { isServer, QueryClient, defaultShouldDehydrateQuery, } from '@tanstack/react-query' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { staleTime: 60 * 1000, }, dehydrate: { // include pending queries in dehydration shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === 'pending', shouldRedactErrors: (error) => { // We should not catch Next.js server errors // as that's how Next.js detects dynamic pages // so we cannot redact them. // Next.js also automatically redacts errors for us // with better digests. return false }, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined export function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } // app/get-query-client.ts import { isServer, QueryClient, defaultShouldDehydrateQuery, } from '@tanstack/react-query' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { staleTime: 60 * 1000, }, dehydrate: { // include pending queries in dehydration shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === 'pending', shouldRedactErrors: (error) => { // We should not catch Next.js server errors // as that's how Next.js detects dynamic pages // so we cannot redact them. // Next.js also automatically redacts errors for us // with better digests. return false }, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined export function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } > Note: This works in NextJs and Server Components because React can serialize Promises over the wire when you pass them down to Client Components. Then, all we need to do is provide a HydrationBoundary, but we don't need to await prefetches anymore: tsx // app/posts/page.tsx import { dehydrate, HydrationBoundary } from '@tanstack/react-query' import { getQueryClient } from './get-query-client' import Posts from './posts' // the function doesn't need to be `async` because we don't `await` anything export default function PostsPage() { const queryClient = getQueryClient() // look ma, no await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( <HydrationBoundary state={dehydrate(queryClient)}> <Posts /> </HydrationBoundary> ) } // app/posts/page.tsx import { dehydrate, HydrationBoundary } from '@tanstack/react-query' import { getQueryClient } from './get-query-client' import Posts from './posts' // the function doesn't need to be `async` because we don't `await` anything export default function PostsPage() { const queryClient = getQueryClient() // look ma, no await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( <HydrationBoundary state={dehydrate(queryClient)}> <Posts /> </HydrationBoundary> ) } On the client, the Promise will be put into the QueryCache for us. That means we can now call useSuspenseQuery inside the Posts component to "use" that Promise (which was created on the Server): tsx // app/posts/posts.tsx 'use client' export default function Posts() { const { data } = useSuspenseQuery({ queryKey: ['posts'], queryFn: getPosts }) // ... } // app/posts/posts.tsx 'use client' export default function Posts() { const { data } = useSuspenseQuery({ queryKey: ['posts'], queryFn: getPosts }) // ... } > Note that you could also useQuery instead of useSuspenseQuery, and the Promise would still be picked up correctly. However, NextJs won't suspend in that case and the component will render in the pending status, which also opts out of server rendering the content. If you're using non-JSON data types and serialize the query results on the server, you can specify the dehydrate.serializeData and hydrate.deserializeData options to serialize and deserialize the data on each side of the boundary to ensure the data in the cache is the same format both on the server and the client: tsx // app/get-query-client.ts import { QueryClient, defaultShouldDehydrateQuery } from '@tanstack/react-query' import { deserialize, serialize } from './transformer' function makeQueryClient() { return new QueryClient({ defaultOptions: { // ... hydrate: { deserializeData: deserialize, }, dehydrate: { serializeData: serialize, }, }, }) } // ... // app/get-query-client.ts import { QueryClient, defaultShouldDehydrateQuery } from '@tanstack/react-query' import { deserialize, serialize } from './transformer' function makeQueryClient() { return new QueryClient({ defaultOptions: { // ... hydrate: { deserializeData: deserialize, }, dehydrate: { serializeData: serialize, }, }, }) } // ... tsx // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import { getQueryClient } from './get-query-client' import { serialize } from './transformer' import Posts from './posts' export default function PostsPage() { const queryClient = getQueryClient() // look ma, no await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: () => getPosts().then(serialize), // <-- serialize the data on the server }) return ( <HydrationBoundary state={dehydrate(queryClient)}> <Posts /> </HydrationBoundary> ) } // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import { getQueryClient } from './get-query-client' import { serialize } from './transformer' import Posts from './posts' export default function PostsPage() { const queryClient = getQueryClient() // look ma, no await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: () => getPosts().then(serialize), // <-- serialize the data on the server }) return ( <HydrationBoundary state={dehydrate(queryClient)}> <Posts /> </HydrationBoundary> ) } tsx // app/posts/posts.tsx 'use client' export default function Posts() { const { data } = useSuspenseQuery({ queryKey: ['posts'], queryFn: getPosts }) // ... } // app/posts/posts.tsx 'use client' export default function Posts() { const { data } = useSuspenseQuery({ queryKey: ['posts'], queryFn: getPosts }) // ... } Now, your getPosts function can return e.g. Temporal datetime objects and the data will be serialized and deserialized on the client, assuming your transformer can serialize and deserialize those data types. For more information, check out the Next.js App with Prefetching Example. ## Experimental streaming without prefetching in Next.js While we recommend the prefetching solution detailed above because it flattens request waterfalls both on the initial page load **and** any subsequent page navigation, there is an experimental way to skip prefetching altogether and still have streaming SSR work: @tanstack/react-query-next-experimental This package will allow you to fetch data on the server (in a Client Component) by just calling useSuspenseQuery in your component. Results will then be streamed from the server to the client as SuspenseBoundaries resolve. If you call useSuspenseQuery without wrapping it in a <Suspense> boundary, the HTML response won't start until the fetch resolves. This can be when you want depending on the situation, but keep in mind that this will hurt your TTFB. To achieve this, wrap your app in the ReactQueryStreamedHydration component: tsx // app/providers.tsx 'use client' import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import * as React from 'react' import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export function Providers(props: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}> <ReactQueryStreamedHydration> {props.children} </ReactQueryStreamedHydration> </QueryClientProvider> ) } // app/providers.tsx 'use client' import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import * as React from 'react' import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export function Providers(props: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}> <ReactQueryStreamedHydration> {props.children} </ReactQueryStreamedHydration> </QueryClientProvider> ) } For more information, check out the NextJs Suspense Streaming Example. The big upside is that you no longer need to prefetch queries manually to have SSR work, and it even still streams in the result! This gives you phenomenal DX and lower code complexity. The downside is easiest to explain if we look back at the complex request waterfall example in the Performance & Request Waterfalls guide. Server Components with prefetching effectively eliminates the request waterfalls both for the initial page load **and** any subsequent navigation. This prefetch-less approach however will only flatten the waterfalls on the initial page load but ends up the same deep waterfall as the original example on page navigations: 1. |> JS for <Feed> 2. |> getFeed() 3. |> JS for <GraphFeedItem> 4. |> getGraphDataById() 1. |> JS for <Feed> 2. |> getFeed() 3. |> JS for <GraphFeedItem> 4. |> getGraphDataById() This is even worse than with getServerSideProps/getStaticProps, since with those we could at least parallelize data- and code-fetching. If you value DX/iteration/shipping speed with low code complexity over performance, don't have deeply nested queries, or are on top of your request waterfalls with parallel fetching using tools like useSuspenseQueries, this can be a good tradeoff. > It might be possible to combine the two approaches, but even we haven't tried that out yet. If you do try this, please report back your findings, or even update these docs with some tips! ## Final words Server Components and streaming are still fairly new concepts and we are still figuring out how React Query fits in and what improvements we can make to the API. We welcome suggestions, feedback and bug reports! Similarly, it would be impossible to teach all the intricacies of this new paradigm all in one guide, on the first try. If you are missing some piece of information here or have suggestions on how to improve this content, also get in touch, or even better, click the "Edit on GitHub" button below and help us out. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/caching > Please thoroughly read the Important Defaults before reading this guide ## Basic Example This caching example illustrates the story and lifecycle of: * Query Instances with and without cache data * Background Refetching * Inactive Queries * Garbage Collection Let's assume we are using the default gcTime of **5 minutes** and the default staleTime of 0. * A new instance of useQuery({ queryKey: \['todos'\], queryFn: fetchTodos }) mounts. * Since no other queries have been made with the \['todos'\] query key, this query will show a hard loading state and make a network request to fetch the data. * When the network request has completed, the returned data will be cached under the \['todos'\] key. * The hook will mark the data as stale after the configured staleTime (defaults to 0, or immediately). * A second instance of useQuery({ queryKey: \['todos'\], queryFn: fetchTodos }) mounts elsewhere. * Since the cache already has data for the \['todos'\] key from the first query, that data is immediately returned from the cache. * The new instance triggers a new network request using its query function. * Note that regardless of whether both fetchTodos query functions are identical or not, both queries' status are updated (including isFetching, isPending, and other related values) because they have the same query key. * When the request completes successfully, the cache's data under the \['todos'\] key is updated with the new data, and both instances are updated with the new data. * Both instances of the useQuery({ queryKey: \['todos'\], queryFn: fetchTodos }) query are unmounted and no longer in use. * Since there are no more active instances of this query, a garbage collection timeout is set using gcTime to delete and garbage collect the query (defaults to **5 minutes**). * Before the cache timeout has completed, another instance of useQuery({ queryKey: \['todos'\], queryFn: fetchTodos }) mounts. The query immediately returns the available cached data while the fetchTodos function is being run in the background. When it completes successfully, it will populate the cache with fresh data. * The final instance of useQuery({ queryKey: \['todos'\], queryFn: fetchTodos }) unmounts. * No more instances of useQuery({ queryKey: \['todos'\], queryFn: fetchTodos }) appear within **5 minutes**. * The cached data under the \['todos'\] key is deleted and garbage collected. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/render-optimizations React Query applies a couple of optimizations automatically to ensure that your components only re-render when they actually need to. This is done by the following means: ## structural sharing React Query uses a technique called "structural sharing" to ensure that as many references as possible will be kept intact between re-renders. If data is fetched over the network, usually, you'll get a completely new reference by json parsing the response. However, React Query will keep the original reference if _nothing_ changed in the data. If a subset changed, React Query will keep the unchanged parts and only replace the changed parts. > Note: This optimization only works if the queryFn returns JSON compatible data. You can turn it off by setting structuralSharing: false globally or on a per-query basis, or you can implement your own structural sharing by passing a function to it. ### referential identity The top level object returned from useQuery, useInfiniteQuery, useMutation and the Array returned from useQueries is **not referentially stable**. It will be a new reference on every render. However, the data properties returned from these hooks will be as stable as possible. ## tracked properties React Query will only trigger a re-render if one of the properties returned from useQuery is actually "used". This is done by using custom getters. This avoids a lot of unnecessary re-renders, e.g. because properties like isFetching or isStale might change often, but are not used in the component. You can customize this feature by setting notifyOnChangeProps manually globally or on a per-query basis. If you want to turn that feature off, you can set notifyOnChangeProps: 'all'. > Note: Custom getters are invoked by accessing a property, either via destructuring or by accessing it directly. If you use object rest destructuring, you will disable this optimization. We have a lint rule to guard against this pitfall. ## select You can use the select option to select a subset of the data that your component should subscribe to. This is useful for highly optimized data transformations or to avoid unnecessary re-renders. js export const useTodos = (select) => { return useQuery({ queryKey: ['todos'], queryFn: fetchTodos, select, }) } export const useTodoCount = () => { return useTodos((data) => data.length) } export const useTodos = (select) => { return useQuery({ queryKey: ['todos'], queryFn: fetchTodos, select, }) } export const useTodoCount = () => { return useTodos((data) => data.length) } A component using the useTodoCount custom hook will only re-render if the length of the todos changes. It will **not** re-render if e.g. the name of a todo changed. > Note: select operates on successfully cached data and is not the appropriate place to throw errors. The source of truth for errors is the queryFn, and a select function that returns an error results in data being undefined and isSuccess being true. We recommend handling errors in the queryFn if you wish to have a query fail on incorrect data, or outside of the query hook if you have a error case not related to caching. ### memoization The select function will only re-run if: * the select function itself changed referentially * data changed This means that an inlined select function, as shown above, will run on every render. To avoid this, you can wrap the select function in useCallback, or extract it to a stable function reference if it doesn't have any dependencies: js // wrapped in useCallback export const useTodoCount = () => { return useTodos(useCallback((data) => data.length, [])) } // wrapped in useCallback export const useTodoCount = () => { return useTodos(useCallback((data) => data.length, [])) } js // extracted to a stable function reference const selectTodoCount = (data) => data.length export const useTodoCount = () => { return useTodos(selectTodoCount) } // extracted to a stable function reference const selectTodoCount = (data) => data.length export const useTodoCount = () => { return useTodos(selectTodoCount) } ## Further Reading For an in-depth guide about these topics, read React Query Render Optimizations from the Community Resources. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/default-query-function If you find yourself wishing for whatever reason that you could just share the same query function for your entire app and just use query keys to identify what it should fetch, you can do that by providing a **default query function** to TanStack Query: tsx // Define a default query function that will receive the query key const defaultQueryFn = async ({ queryKey }) => { const { data } = await axios.get( `https://jsonplaceholder.typicode.com${queryKey[0]}`, ) return data } // provide the default query function to your app with defaultOptions const queryClient = new QueryClient({ defaultOptions: { queries: { queryFn: defaultQueryFn, }, }, }) function App() { return ( <QueryClientProvider client={queryClient}> <YourApp /> </QueryClientProvider> ) } // All you have to do now is pass a key! function Posts() { const { status, data, error, isFetching } = useQuery({ queryKey: ['/posts'] }) // ... } // You can even leave out the queryFn and just go straight into options function Post({ postId }) { const { status, data, error, isFetching } = useQuery({ queryKey: [`/posts/${postId}`], enabled: !!postId, }) // ... } // Define a default query function that will receive the query key const defaultQueryFn = async ({ queryKey }) => { const { data } = await axios.get( `https://jsonplaceholder.typicode.com${queryKey[0]}`, ) return data } // provide the default query function to your app with defaultOptions const queryClient = new QueryClient({ defaultOptions: { queries: { queryFn: defaultQueryFn, }, }, }) function App() { return ( <QueryClientProvider client={queryClient}> <YourApp /> </QueryClientProvider> ) } // All you have to do now is pass a key! function Posts() { const { status, data, error, isFetching } = useQuery({ queryKey: ['/posts'] }) // ... } // You can even leave out the queryFn and just go straight into options function Post({ postId }) { const { status, data, error, isFetching } = useQuery({ queryKey: [`/posts/${postId}`], enabled: !!postId, }) // ... } If you ever want to override the default queryFn, you can just provide your own like you normally would. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/suspense React Query can also be used with React's Suspense for Data Fetching APIs. For this, we have dedicated hooks: * useSuspenseQuery * useSuspenseInfiniteQuery * useSuspenseQueries * Additionally, you can use the useQuery().promise and React.use() (Experimental) When using suspense mode, status states and error objects are not needed and are then replaced by usage of the React.Suspense component (including the use of the fallback prop and React error boundaries for catching errors). Please read the Resetting Error Boundaries and look at the Suspense Example for more information on how to set up suspense mode. If you want mutations to propagate errors to the nearest error boundary (similar to queries), you can set the throwOnError option to true as well. Enabling suspense mode for a query: tsx import { useSuspenseQuery } from '@tanstack/react-query' const { data } = useSuspenseQuery({ queryKey, queryFn }) import { useSuspenseQuery } from '@tanstack/react-query' const { data } = useSuspenseQuery({ queryKey, queryFn }) This works nicely in TypeScript, because data is guaranteed to be defined (as errors and loading states are handled by Suspense- and ErrorBoundaries). On the flip side, you therefore can't conditionally enable / disable the Query. This generally shouldn't be necessary for dependent Queries because with suspense, all your Queries inside one component are fetched in serial. placeholderData also doesn't exist for this Query. To prevent the UI from being replaced by a fallback during an update, wrap your updates that change the QueryKey into startTransition. ### throwOnError default Not all errors are thrown to the nearest Error Boundary per default - we're only throwing errors if there is no other data to show. That means if a Query ever successfully got data in the cache, the component will render, even if data is stale. Thus, the default for throwOnError is: throwOnError: (error, query) => typeof query.state.data === 'undefined' throwOnError: (error, query) => typeof query.state.data === 'undefined' Since you can't change throwOnError (because it would allow for data to become potentially undefined), you have to throw errors manually if you want all errors to be handled by Error Boundaries: tsx import { useSuspenseQuery } from '@tanstack/react-query' const { data, error, isFetching } = useSuspenseQuery({ queryKey, queryFn }) if (error && !isFetching) { throw error } // continue rendering data import { useSuspenseQuery } from '@tanstack/react-query' const { data, error, isFetching } = useSuspenseQuery({ queryKey, queryFn }) if (error && !isFetching) { throw error } // continue rendering data ## Resetting Error Boundaries Whether you are using **suspense** or **throwOnError** in your queries, you will need a way to let queries know that you want to try again when re-rendering after some error occurred. Query errors can be reset with the QueryErrorResetBoundary component or with the useQueryErrorResetBoundary hook. When using the component it will reset any query errors within the boundaries of the component: tsx import { QueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => ( <QueryErrorResetBoundary> {({ reset }) => ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> There was an error! <Button onClick={() => resetErrorBoundary()}>Try again</Button> </div> )} > <Page /> </ErrorBoundary> )} </QueryErrorResetBoundary> ) import { QueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => ( <QueryErrorResetBoundary> {({ reset }) => ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> There was an error! <Button onClick={() => resetErrorBoundary()}>Try again</Button> </div> )} > <Page /> </ErrorBoundary> )} </QueryErrorResetBoundary> ) When using the hook it will reset any query errors within the closest QueryErrorResetBoundary. If there is no boundary defined it will reset them globally: tsx import { useQueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => { const { reset } = useQueryErrorResetBoundary() return ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> There was an error! <Button onClick={() => resetErrorBoundary()}>Try again</Button> </div> )} > <Page /> </ErrorBoundary> ) } import { useQueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => { const { reset } = useQueryErrorResetBoundary() return ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> There was an error! <Button onClick={() => resetErrorBoundary()}>Try again</Button> </div> )} > <Page /> </ErrorBoundary> ) } ## Fetch-on-render vs Render-as-you-fetch Out of the box, React Query in suspense mode works really well as a **Fetch-on-render** solution with no additional configuration. This means that when your components attempt to mount, they will trigger query fetching and suspend, but only once you have imported them and mounted them. If you want to take it to the next level and implement a **Render-as-you-fetch** model, we recommend implementing Prefetching on routing callbacks and/or user interactions events to start loading queries before they are mounted and hopefully even before you start importing or mounting their parent components. ## Suspense on the Server with streaming If you are using NextJs, you can use our **experimental** integration for Suspense on the Server: @tanstack/react-query-next-experimental. This package will allow you to fetch data on the server (in a client component) by just calling useSuspenseQuery in your component. Results will then be streamed from the server to the client as SuspenseBoundaries resolve. To achieve this, wrap your app in the ReactQueryStreamedHydration component: tsx // app/providers.tsx 'use client' import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import * as React from 'react' import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export function Providers(props: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}> <ReactQueryStreamedHydration> {props.children} </ReactQueryStreamedHydration> </QueryClientProvider> ) } // app/providers.tsx 'use client' import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import * as React from 'react' import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export function Providers(props: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}> <ReactQueryStreamedHydration> {props.children} </ReactQueryStreamedHydration> </QueryClientProvider> ) } For more information, check out the NextJs Suspense Streaming Example and the Advanced Rendering & Hydration guide. ## Using useQuery().promise and React.use() (Experimental) > To enable this feature, you need to set the experimental\_prefetchInRender option to true when creating your QueryClient **Example code:** tsx const queryClient = new QueryClient({ defaultOptions: { queries: { experimental_prefetchInRender: true, }, }, }) const queryClient = new QueryClient({ defaultOptions: { queries: { experimental_prefetchInRender: true, }, }, }) **Usage:** tsx import React from 'react' import { useQuery } from '@tanstack/react-query' import { fetchTodos, type Todo } from './api' function TodoList({ query }: { query: UseQueryResult<Todo[]> }) { const data = React.use(query.promise) return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) } export function App() { const query = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) return ( <> <h1>Todos</h1> <React.Suspense fallback={<div>Loading...</div>}> <TodoList query={query} /> </React.Suspense> </> ) } import React from 'react' import { useQuery } from '@tanstack/react-query' import { fetchTodos, type Todo } from './api' function TodoList({ query }: { query: UseQueryResult<Todo[]> }) { const data = React.use(query.promise) return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) } export function App() { const query = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) return ( <> <h1>Todos</h1> <React.Suspense fallback={<div>Loading...</div>}> <TodoList query={query} /> </React.Suspense> </> ) } --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/network-mode/#network-mode-online TanStack Query provides three different network modes to distinguish how Queries and Mutations should behave if you have no network connection. This mode can be set for each Query / Mutation individually, or globally via the query / mutation defaults. Since TanStack Query is most often used for data fetching in combination with data fetching libraries, the default network mode is online. ## Network Mode: online In this mode, Queries and Mutations will not fire unless you have network connection. This is the default mode. If a fetch is initiated for a query, it will always stay in the state (pending, error, success) it is in if the fetch cannot be made because there is no network connection. However, a fetchStatus is exposed additionally. This can be either: * fetching: The queryFn is really executing - a request is in-flight. * paused: The query is not executing - it is paused until you have connection again * idle: The query is not fetching and not paused The flags isFetching and isPaused are derived from this state and exposed for convenience. > Keep in mind that it might not be enough to check for pending state to show a loading spinner. Queries can be in state: 'pending', but fetchStatus: 'paused' if they are mounting for the first time, and you have no network connection. If a query runs because you are online, but you go offline while the fetch is still happening, TanStack Query will also pause the retry mechanism. Paused queries will then continue to run once you re-gain network connection. This is independent of refetchOnReconnect (which also defaults to true in this mode), because it is not a refetch, but rather a continue. If the query has been cancelled in the meantime, it will not continue. ## Network Mode: always In this mode, TanStack Query will always fetch and ignore the online / offline state. This is likely the mode you want to choose if you use TanStack Query in an environment where you don't need an active network connection for your Queries to work - e.g. if you just read from AsyncStorage, or if you just want to return Promise.resolve(5) from your queryFn. * Queries will never be paused because you have no network connection. * Retries will also not pause - your Query will go to error state if it fails. * refetchOnReconnect defaults to false in this mode, because reconnecting to the network is not a good indicator anymore that stale queries should be refetched. You can still turn it on if you want. ## Network Mode: offlineFirst This mode is the middle ground between the first two options, where TanStack Query will run the queryFn once, but then pause retries. This is very handy if you have a serviceWorker that intercepts a request for caching like in an offline-first PWA, or if you use HTTP caching via the Cache-Control header. In those situations, the first fetch might succeed because it comes from an offline storage / cache. However, if there is a cache miss, the network request will go out and fail, in which case this mode behaves like an online query - pausing retries. ## Devtools The TanStack Query Devtools will show Queries in a paused state if they would be fetching, but there is no network connection. There is also a toggle button to _Mock offline behavior_. Please note that this button will _not_ actually mess with your network connection (you can do that in the browser devtools), but it will set the OnlineManager in an offline state. ## Signature * networkMode: 'online' | 'always' | 'offlineFirst' * optional * defaults to 'online' --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/disabling-queries/#typesafe-disabling-of-queries-using-skiptoken If you ever want to disable a query from automatically running, you can use the enabled = false option. The enabled option also accepts a callback that returns a boolean. When enabled is false: * If the query has cached data, then the query will be initialized in the status === 'success' or isSuccess state. * If the query does not have cached data, then the query will start in the status === 'pending' and fetchStatus === 'idle' state. * The query will not automatically fetch on mount. * The query will not automatically refetch in the background. * The query will ignore query client invalidateQueries and refetchQueries calls that would normally result in the query refetching. * refetch returned from useQuery can be used to manually trigger the query to fetch. However, it will not work with skipToken. > TypeScript users may prefer to use skipToken as an alternative to enabled = false. tsx function Todos() { const { isLoading, isError, data, error, refetch, isFetching } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, enabled: false, }) return ( <div> <button onClick={() => refetch()}>Fetch Todos</button> {data ? ( <> <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> </> ) : isError ? ( <span>Error: {error.message}</span> ) : isLoading ? ( <span>Loading...</span> ) : ( <span>Not ready ...</span> )} <div>{isFetching ? 'Fetching...' : null}</div> </div> ) } function Todos() { const { isLoading, isError, data, error, refetch, isFetching } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, enabled: false, }) return ( <div> <button onClick={() => refetch()}>Fetch Todos</button> {data ? ( <> <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> </> ) : isError ? ( <span>Error: {error.message}</span> ) : isLoading ? ( <span>Loading...</span> ) : ( <span>Not ready ...</span> )} <div>{isFetching ? 'Fetching...' : null}</div> </div> ) } Permanently disabling a query opts out of many great features that TanStack Query has to offer (like background refetches), and it's also not the idiomatic way. It takes you from the declarative approach (defining dependencies when your query should run) into an imperative mode (fetch whenever I click here). It is also not possible to pass parameters to refetch. Oftentimes, all you want is a lazy query that defers the initial fetch: ## Lazy Queries The enabled option can not only be used to permanently disable a query, but also to enable / disable it at a later time. A good example would be a filter form where you only want to fire off the first request once the user has entered a filter value: tsx function Todos() { const [filter, setFilter] = React.useState('') const { data } = useQuery({ queryKey: ['todos', filter], queryFn: () => fetchTodos(filter), // ⬇️ disabled as long as the filter is empty enabled: !!filter, }) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data} />} </div> ) } function Todos() { const [filter, setFilter] = React.useState('') const { data } = useQuery({ queryKey: ['todos', filter], queryFn: () => fetchTodos(filter), // ⬇️ disabled as long as the filter is empty enabled: !!filter, }) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data} />} </div> ) } ### isLoading (Previously: isInitialLoading) Lazy queries will be in status: 'pending' right from the start because pending means that there is no data yet. This is technically true, however, since we are not currently fetching any data (as the query is not _enabled_), it also means you likely cannot use this flag to show a loading spinner. If you are using disabled or lazy queries, you can use the isLoading flag instead. It's a derived flag that is computed from: isPending && isFetching so it will only be true if the query is currently fetching for the first time. ## Typesafe disabling of queries using skipToken If you are using TypeScript, you can use the skipToken to disable a query. This is useful when you want to disable a query based on a condition, but you still want to keep the query to be type safe. > IMPORTANT: refetch from useQuery will not work with skipToken. Other than that, skipToken works the same as enabled: false. tsx import { skipToken, useQuery } from '@tanstack/react-query' function Todos() { const [filter, setFilter] = React.useState<string | undefined>() const { data } = useQuery({ queryKey: ['todos', filter], // ⬇️ disabled as long as the filter is undefined or empty queryFn: filter ? () => fetchTodos(filter) : skipToken, }) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data} />} </div> ) } import { skipToken, useQuery } from '@tanstack/react-query' function Todos() { const [filter, setFilter] = React.useState<string | undefined>() const { data } = useQuery({ queryKey: ['todos', filter], // ⬇️ disabled as long as the filter is undefined or empty queryFn: filter ? () => fetchTodos(filter) : skipToken, }) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data} />} </div> ) } --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/ssr/#full-nextjs-pages-router-example In this guide you'll learn how to use React Query with server rendering. See the guide on Prefetching & Router Integration for some background. You might also want to check out the Performance & Request Waterfalls guide before that. For advanced server rendering patterns, such as streaming, Server Components and the new Next.js app router, see the Advanced Server Rendering guide. If you just want to see some code, you can skip ahead to the Full Next.js pages router example or the Full Remix example below. ## Server Rendering & React Query So what is server rendering anyway? The rest of this guide will assume you are familiar with the concept, but let's spend some time to look at how it relates to React Query. Server rendering is the act of generating the initial html on the server, so that the user has some content to look at as soon as the page loads. This can happen on demand when a page is requested (SSR). It can also happen ahead of time either because a previous request was cached, or at build time (SSG). If you've read the Request Waterfalls guide, you might remember this: 1. |-> Markup (without content) 2. |-> JS 3. |-> Query 1. |-> Markup (without content) 2. |-> JS 3. |-> Query With a client rendered application, these are the minimum 3 server roundtrips you will need to make before getting any content on the screen for the user. One way of viewing server rendering is that it turns the above into this: 1. |-> Markup (with content AND initial data) 2. |-> JS 1. |-> Markup (with content AND initial data) 2. |-> JS As soon as **1.** is complete, the user can see the content and when **2.** finishes, the page is interactive and clickable. Because the markup also contains the initial data we need, step **3.** does not need to run on the client at all, at least until you want to revalidate the data for some reason. This is all from the clients perspective. On the server, we need to **prefetch** that data before we generate/render the markup, we need to **dehydrate** that data into a serializable format we can embed in the markup, and on the client we need to **hydrate** that data into a React Query cache so we can avoid doing a new fetch on the client. Read on to learn how to implement these three steps with React Query. ## A quick note on Suspense This guide uses the regular useQuery API. While we don't necessarily recommend it, it is possible to replace this with useSuspenseQuery instead **as long as you always prefetch all your queries**. The upside is that you get to use <Suspense> for loading states on the client. If you do forget to prefetch a query when you are using useSuspenseQuery, the consequences will depend on the framework you are using. In some cases, the data will Suspend and get fetched on the server but never be hydrated to the client, where it will fetch again. In these cases you will get a markup hydration mismatch, because the server and the client tried to render different things. ## Initial setup The first steps of using React Query is always to create a queryClient and wrap the application in a <QueryClientProvider>. When doing server rendering, it's important to create the queryClient instance **inside of your app**, in React state (an instance ref works fine too). **This ensures that data is not shared between different users and requests**, while still only creating the queryClient once per component lifecycle. Next.js pages router: tsx // _app.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' // NEVER DO THIS: // const queryClient = new QueryClient() // // Creating the queryClient at the file root level makes the cache shared // between all requests and means _all_ data gets passed to _all_ users. // Besides being bad for performance, this also leaks any sensitive data. export default function MyApp({ Component, pageProps }) { // Instead do this, which ensures each request has its own cache: const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> ) } // _app.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' // NEVER DO THIS: // const queryClient = new QueryClient() // // Creating the queryClient at the file root level makes the cache shared // between all requests and means _all_ data gets passed to _all_ users. // Besides being bad for performance, this also leaks any sensitive data. export default function MyApp({ Component, pageProps }) { // Instead do this, which ensures each request has its own cache: const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> ) } Remix: tsx // app/root.tsx import { Outlet } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp() { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Outlet /> </QueryClientProvider> ) } // app/root.tsx import { Outlet } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp() { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Outlet /> </QueryClientProvider> ) } ## Get started fast with initialData The quickest way to get started is to not involve React Query at all when it comes to prefetching and not use the dehydrate/hydrate APIs. What you do instead is passing the raw data in as the initialData option to useQuery. Let's look at an example using Next.js pages router, using getServerSideProps. tsx export async function getServerSideProps() { const posts = await getPosts() return { props: { posts } } } function Posts(props) { const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: props.posts, }) // ... } export async function getServerSideProps() { const posts = await getPosts() return { props: { posts } } } function Posts(props) { const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: props.posts, }) // ... } This also works with getStaticProps or even the older getInitialProps and the same pattern can be applied in any other framework that has equivalent functions. This is what the same example looks like with Remix: tsx export async function loader() { const posts = await getPosts() return json({ posts }) } function Posts() { const { posts } = useLoaderData<typeof loader>() const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: posts, }) // ... } export async function loader() { const posts = await getPosts() return json({ posts }) } function Posts() { const { posts } = useLoaderData<typeof loader>() const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: posts, }) // ... } The setup is minimal and this can be a quick solution for some cases, but there are a **few tradeoffs to consider** when compared to the full approach: * If you are calling useQuery in a component deeper down in the tree you need to pass the initialData down to that point * If you are calling useQuery with the same query in multiple locations, passing initialData to only one of them can be brittle and break when your app changes since. If you remove or move the component that has the useQuery with initialData, the more deeply nested useQuery might no longer have any data. Passing initialData to **all** queries that needs it can also be cumbersome. * There is no way to know at what time the query was fetched on the server, so dataUpdatedAt and determining if the query needs refetching is based on when the page loaded instead * If there is already data in the cache for a query, initialData will never overwrite this data, **even if the new data is fresher than the old one**. * To understand why this is especially bad, consider the getServerSideProps example above. If you navigate back and forth to a page several times, getServerSideProps would get called each time and fetch new data, but because we are using the initialData option, the client cache and data would never be updated. Setting up the full hydration solution is straightforward and does not have these drawbacks, this will be the focus for the rest of the documentation. ## Using the Hydration APIs With just a little more setup, you can use a queryClient to prefetch queries during a preload phase, pass a serialized version of that queryClient to the rendering part of the app and reuse it there. This avoids the drawbacks above. Feel free to skip ahead for full Next.js pages router and Remix examples, but at a general level these are the extra steps: * In the framework loader function, create a const queryClient = new QueryClient(options) * In the loader function, do await queryClient.prefetchQuery(...) for each query you want to prefetch * You want to use await Promise.all(...) to fetch the queries in parallel when possible * It's fine to have queries that aren't prefetched. These wont be server rendered, instead they will be fetched on the client after the application is interactive. This can be great for content that are shown only after user interaction, or is far down on the page to avoid blocking more critical content. * From the loader, return dehydrate(queryClient), note that the exact syntax to return this differs between frameworks * Wrap your tree with <HydrationBoundary state={dehydratedState}> where dehydratedState comes from the framework loader. How you get dehydratedState also differs between frameworks. * This can be done for each route, or at the top of the application to avoid boilerplate, see examples > An interesting detail is that there are actually _three_ queryClients involved. The framework loaders are a form of "preloading" phase that happens before rendering, and this phase has its own queryClient that does the prefetching. The dehydrated result of this phase gets passed to **both** the server rendering process **and** the client rendering process which each has its own queryClient. This ensures they both start with the same data so they can return the same markup. > Server Components are another form of "preloading" phase, that can also "preload" (pre-render) parts of a React component tree. Read more in the Advanced Server Rendering guide. ### Full Next.js pages router example > For app router documentation, see the Advanced Server Rendering guide. Initial setup: tsx // _app.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> ) } // _app.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> ) } In each route: tsx // pages/posts.tsx import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' // This could also be getServerSideProps export async function getStaticProps() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return { props: { dehydratedState: dehydrate(queryClient), }, } } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } // pages/posts.tsx import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' // This could also be getServerSideProps export async function getStaticProps() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return { props: { dehydratedState: dehydrate(queryClient), }, } } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } ### Full Remix example Initial setup: tsx // app/root.tsx import { Outlet } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp() { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Outlet /> </QueryClientProvider> ) } // app/root.tsx import { Outlet } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp() { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Outlet /> </QueryClientProvider> ) } In each route, note that it's fine to do this in nested routes too: tsx // app/routes/posts.tsx import { json } from '@remix-run/node' import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' export async function loader() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return json({ dehydratedState: dehydrate(queryClient) }) } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute() { const { dehydratedState } = useLoaderData<typeof loader>() return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } // app/routes/posts.tsx import { json } from '@remix-run/node' import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' export async function loader() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return json({ dehydratedState: dehydrate(queryClient) }) } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute() { const { dehydratedState } = useLoaderData<typeof loader>() return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } ## Optional - Remove boilerplate Having this part in every route might seem like a lot of boilerplate: tsx export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } While there is nothing wrong with this approach, if you want to get rid of this boilerplate, here's how you can modify your setup in Next.js: tsx // _app.tsx import { HydrationBoundary, QueryClient, QueryClientProvider, } from '@tanstack/react-query' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState(() => new QueryClient()) return ( <QueryClientProvider client={queryClient}> <HydrationBoundary state={pageProps.dehydratedState}> <Component {...pageProps} /> </HydrationBoundary> </QueryClientProvider> ) } // pages/posts.tsx // Remove PostsRoute with the HydrationBoundary and instead export Posts directly: export default function Posts() { ... } // _app.tsx import { HydrationBoundary, QueryClient, QueryClientProvider, } from '@tanstack/react-query' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState(() => new QueryClient()) return ( <QueryClientProvider client={queryClient}> <HydrationBoundary state={pageProps.dehydratedState}> <Component {...pageProps} /> </HydrationBoundary> </QueryClientProvider> ) } // pages/posts.tsx // Remove PostsRoute with the HydrationBoundary and instead export Posts directly: export default function Posts() { ... } With Remix, this is a little bit more involved, we recommend checking out the use-dehydrated-state package. ## Prefetching dependent queries Over in the Prefetching guide we learned how to prefetch dependent queries, but how do we do this in framework loaders? Consider the following code, taken from the Dependent Queries guide: tsx // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) How would we prefetch this so it can be server rendered? Here's an example: tsx // For Remix, rename this to loader instead export async function getServerSideProps() { const queryClient = new QueryClient() const user = await queryClient.fetchQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) if (user?.userId) { await queryClient.prefetchQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, }) } // For Remix: // return json({ dehydratedState: dehydrate(queryClient) }) return { props: { dehydratedState: dehydrate(queryClient) } } } // For Remix, rename this to loader instead export async function getServerSideProps() { const queryClient = new QueryClient() const user = await queryClient.fetchQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) if (user?.userId) { await queryClient.prefetchQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, }) } // For Remix: // return json({ dehydratedState: dehydrate(queryClient) }) return { props: { dehydratedState: dehydrate(queryClient) } } } This can get more complex of course, but since these loader functions are just JavaScript, you can use the full power of the language to build your logic. Make sure you prefetch all queries that you want to be server rendered. ## Error handling React Query defaults to a graceful degradation strategy. This means: * queryClient.prefetchQuery(...) never throws errors * dehydrate(...) only includes successful queries, not failed ones This will lead to any failed queries being retried on the client and that the server rendered output will include loading states instead of the full content. While a good default, sometimes this is not what you want. When critical content is missing, you might want to respond with a 404 or 500 status code depending on the situation. For these cases, use queryClient.fetchQuery(...) instead, which will throw errors when it fails, letting you handle things in a suitable way. tsx let result try { result = await queryClient.fetchQuery(...) } catch (error) { // Handle the error, refer to your framework documentation } // You might also want to check and handle any invalid `result` here let result try { result = await queryClient.fetchQuery(...) } catch (error) { // Handle the error, refer to your framework documentation } // You might also want to check and handle any invalid `result` here If you for some reason want to include failed queries in the dehydrated state to avoid retries, you can use the option shouldDehydrateQuery to override the default function and implement your own logic: tsx dehydrate(queryClient, { shouldDehydrateQuery: (query) => { // This will include all queries, including failed ones, // but you can also implement your own logic by inspecting `query` return true }, }) dehydrate(queryClient, { shouldDehydrateQuery: (query) => { // This will include all queries, including failed ones, // but you can also implement your own logic by inspecting `query` return true }, }) ## Serialization When doing return { props: { dehydratedState: dehydrate(queryClient) } } in Next.js, or return json({ dehydratedState: dehydrate(queryClient) }) in Remix, what happens is that the dehydratedState representation of the queryClient is serialized by the framework so it can be embedded into the markup and transported to the client. By default, these frameworks only supports returning things that are safely serializable/parsable, and therefore does not support undefined, Error, Date, Map, Set, BigInt, Infinity, NaN, \-0, regular expressions etc. This also means that you can not return any of these things from your queries. If returning these values is something you want, check out superjson or similar packages. If you are using a custom SSR setup, you need to take care of this step yourself. Your first instinct might be to use JSON.stringify(dehydratedState), but because this doesn't escape things like <script>alert('Oh no..')</script> by default, this can easily lead to **XSS-vulnerabilities** in your application. superjson also **does not** escape values and is unsafe to use by itself in a custom SSR setup (unless you add an extra step for escaping the output). Instead we recommend using a library like Serialize JavaScript or devalue which are both safe against XSS injections out of the box. ## A note about request waterfalls In the Performance & Request Waterfalls guide we mentioned we would revisit how server rendering changes one of the more complex nested waterfalls. Check back for the specific code example, but as a refresher, we have a code split <GraphFeedItem> component inside a <Feed> component. This only renders if the feed contains a graph item and both of these components fetches their own data. With client rendering, this leads to the following request waterfall: 1. |> Markup (without content) 2. |> JS for <Feed> 3. |> getFeed() 4. |> JS for <GraphFeedItem> 5. |> getGraphDataById() 1. |> Markup (without content) 2. |> JS for <Feed> 3. |> getFeed() 4. |> JS for <GraphFeedItem> 5. |> getGraphDataById() The nice thing about server rendering is that we can turn the above into: 1. |> Markup (with content AND initial data) 2. |> JS for <Feed> 2. |> JS for <GraphFeedItem> 1. |> Markup (with content AND initial data) 2. |> JS for <Feed> 2. |> JS for <GraphFeedItem> Note that the queries are no longer fetched on the client, instead their data was included in the markup. The reason we can now load the JS in parallel is that since <GraphFeedItem> was rendered on the server we know that we are going to need this JS on the client as well and can insert a script-tag for this chunk in the markup. On the server, we would still have this request waterfall: 1. |> getFeed() 2. |> getGraphDataById() 1. |> getFeed() 2. |> getGraphDataById() We simply can not know before we have fetched the feed if we also need to fetch graph data, they are dependent queries. Because this happens on the server where latency is generally both lower and more stable, this often isn't such a big deal. Amazing, we've mostly flattened our waterfalls! There's a catch though. Let's call this page the /feed page, and let's pretend we also have another page like /posts. If we type in www.example.com/feed directly in the url bar and hit enter, we get all these great server rendering benefits, BUT, if we instead type in www.example.com/posts and then **click a link** to /feed, we're back to to this: 1. |> JS for <Feed> 2. |> getFeed() 3. |> JS for <GraphFeedItem> 4. |> getGraphDataById() 1. |> JS for <Feed> 2. |> getFeed() 3. |> JS for <GraphFeedItem> 4. |> getGraphDataById() This is because with SPA's, server rendering only works for the initial page load, not for any subsequent navigation. Modern frameworks often try to solve this by fetching the initial code and data in parallel, so if you were using Next.js or Remix with the prefetching patterns we outlined in this guide, including how to prefetch dependent queries, it would actually look like this instead: 1. |> JS for <Feed> 1. |> getFeed() + getGraphDataById() 2. |> JS for <GraphFeedItem> 1. |> JS for <Feed> 1. |> getFeed() + getGraphDataById() 2. |> JS for <GraphFeedItem> This is much better, but if we want to improve this further we can flatten this to a single roundtrip with Server Components. Learn how in the Advanced Server Rendering guide. ## Tips, Tricks and Caveats ### Staleness is measured from when the query was fetched on the server A query is considered stale depending on when it was dataUpdatedAt. A caveat here is that the server needs to have the correct time for this to work properly, but UTC time is used, so timezones do not factor into this. Because staleTime defaults to 0, queries will be refetched in the background on page load by default. You might want to use a higher staleTime to avoid this double fetching, especially if you don't cache your markup. This refetching of stale queries is a perfect match when caching markup in a CDN! You can set the cache time of the page itself decently high to avoid having to re-render pages on the server, but configure the staleTime of the queries lower to make sure data is refetched in the background as soon as a user visits the page. Maybe you want to cache the pages for a week, but refetch the data automatically on page load if it's older than a day? ### High memory consumption on server In case you are creating the QueryClient for every request, React Query creates the isolated cache for this client, which is preserved in memory for the gcTime period. That may lead to high memory consumption on server in case of high number of requests during that period. On the server, gcTime defaults to Infinity which disables manual garbage collection and will automatically clear memory once a request has finished. If you are explicitly setting a non-Infinity gcTime then you will be responsible for clearing the cache early. Avoid setting gcTime to 0 as it may result in a hydration error. This occurs because the Hydration Boundary places necessary data into the cache for rendering, but if the garbage collector removes the data before the rendering completes, issues may arise. If you require a shorter gcTime, we recommend setting it to 2 \* 1000 to allow sufficient time for the app to reference the data. To clear the cache after it is not needed and to lower memory consumption, you can add a call to queryClient.clear() after the request is handled and dehydrated state has been sent to the client. Alternatively, you can set a smaller gcTime. ### Caveat for Next.js rewrites There's a catch if you're using Next.js' rewrites feature together with Automatic Static Optimization or getStaticProps: It will cause a second hydration by React Query. That's because Next.js needs to ensure that they parse the rewrites on the client and collect any params after hydration so that they can be provided in router.query. The result is missing referential equality for all the hydration data, which for example triggers wherever your data is used as props of components or in the dependency array of useEffects/useMemos. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/suspense/#resetting-error-boundaries React Query can also be used with React's Suspense for Data Fetching APIs. For this, we have dedicated hooks: * useSuspenseQuery * useSuspenseInfiniteQuery * useSuspenseQueries * Additionally, you can use the useQuery().promise and React.use() (Experimental) When using suspense mode, status states and error objects are not needed and are then replaced by usage of the React.Suspense component (including the use of the fallback prop and React error boundaries for catching errors). Please read the Resetting Error Boundaries and look at the Suspense Example for more information on how to set up suspense mode. If you want mutations to propagate errors to the nearest error boundary (similar to queries), you can set the throwOnError option to true as well. Enabling suspense mode for a query: tsx import { useSuspenseQuery } from '@tanstack/react-query' const { data } = useSuspenseQuery({ queryKey, queryFn }) import { useSuspenseQuery } from '@tanstack/react-query' const { data } = useSuspenseQuery({ queryKey, queryFn }) This works nicely in TypeScript, because data is guaranteed to be defined (as errors and loading states are handled by Suspense- and ErrorBoundaries). On the flip side, you therefore can't conditionally enable / disable the Query. This generally shouldn't be necessary for dependent Queries because with suspense, all your Queries inside one component are fetched in serial. placeholderData also doesn't exist for this Query. To prevent the UI from being replaced by a fallback during an update, wrap your updates that change the QueryKey into startTransition. ### throwOnError default Not all errors are thrown to the nearest Error Boundary per default - we're only throwing errors if there is no other data to show. That means if a Query ever successfully got data in the cache, the component will render, even if data is stale. Thus, the default for throwOnError is: throwOnError: (error, query) => typeof query.state.data === 'undefined' throwOnError: (error, query) => typeof query.state.data === 'undefined' Since you can't change throwOnError (because it would allow for data to become potentially undefined), you have to throw errors manually if you want all errors to be handled by Error Boundaries: tsx import { useSuspenseQuery } from '@tanstack/react-query' const { data, error, isFetching } = useSuspenseQuery({ queryKey, queryFn }) if (error && !isFetching) { throw error } // continue rendering data import { useSuspenseQuery } from '@tanstack/react-query' const { data, error, isFetching } = useSuspenseQuery({ queryKey, queryFn }) if (error && !isFetching) { throw error } // continue rendering data ## Resetting Error Boundaries Whether you are using **suspense** or **throwOnError** in your queries, you will need a way to let queries know that you want to try again when re-rendering after some error occurred. Query errors can be reset with the QueryErrorResetBoundary component or with the useQueryErrorResetBoundary hook. When using the component it will reset any query errors within the boundaries of the component: tsx import { QueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => ( <QueryErrorResetBoundary> {({ reset }) => ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> There was an error! <Button onClick={() => resetErrorBoundary()}>Try again</Button> </div> )} > <Page /> </ErrorBoundary> )} </QueryErrorResetBoundary> ) import { QueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => ( <QueryErrorResetBoundary> {({ reset }) => ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> There was an error! <Button onClick={() => resetErrorBoundary()}>Try again</Button> </div> )} > <Page /> </ErrorBoundary> )} </QueryErrorResetBoundary> ) When using the hook it will reset any query errors within the closest QueryErrorResetBoundary. If there is no boundary defined it will reset them globally: tsx import { useQueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => { const { reset } = useQueryErrorResetBoundary() return ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> There was an error! <Button onClick={() => resetErrorBoundary()}>Try again</Button> </div> )} > <Page /> </ErrorBoundary> ) } import { useQueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => { const { reset } = useQueryErrorResetBoundary() return ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> There was an error! <Button onClick={() => resetErrorBoundary()}>Try again</Button> </div> )} > <Page /> </ErrorBoundary> ) } ## Fetch-on-render vs Render-as-you-fetch Out of the box, React Query in suspense mode works really well as a **Fetch-on-render** solution with no additional configuration. This means that when your components attempt to mount, they will trigger query fetching and suspend, but only once you have imported them and mounted them. If you want to take it to the next level and implement a **Render-as-you-fetch** model, we recommend implementing Prefetching on routing callbacks and/or user interactions events to start loading queries before they are mounted and hopefully even before you start importing or mounting their parent components. ## Suspense on the Server with streaming If you are using NextJs, you can use our **experimental** integration for Suspense on the Server: @tanstack/react-query-next-experimental. This package will allow you to fetch data on the server (in a client component) by just calling useSuspenseQuery in your component. Results will then be streamed from the server to the client as SuspenseBoundaries resolve. To achieve this, wrap your app in the ReactQueryStreamedHydration component: tsx // app/providers.tsx 'use client' import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import * as React from 'react' import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export function Providers(props: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}> <ReactQueryStreamedHydration> {props.children} </ReactQueryStreamedHydration> </QueryClientProvider> ) } // app/providers.tsx 'use client' import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import * as React from 'react' import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export function Providers(props: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}> <ReactQueryStreamedHydration> {props.children} </ReactQueryStreamedHydration> </QueryClientProvider> ) } For more information, check out the NextJs Suspense Streaming Example and the Advanced Rendering & Hydration guide. ## Using useQuery().promise and React.use() (Experimental) > To enable this feature, you need to set the experimental\_prefetchInRender option to true when creating your QueryClient **Example code:** tsx const queryClient = new QueryClient({ defaultOptions: { queries: { experimental_prefetchInRender: true, }, }, }) const queryClient = new QueryClient({ defaultOptions: { queries: { experimental_prefetchInRender: true, }, }, }) **Usage:** tsx import React from 'react' import { useQuery } from '@tanstack/react-query' import { fetchTodos, type Todo } from './api' function TodoList({ query }: { query: UseQueryResult<Todo[]> }) { const data = React.use(query.promise) return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) } export function App() { const query = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) return ( <> <h1>Todos</h1> <React.Suspense fallback={<div>Loading...</div>}> <TodoList query={query} /> </React.Suspense> </> ) } import React from 'react' import { useQuery } from '@tanstack/react-query' import { fetchTodos, type Todo } from './api' function TodoList({ query }: { query: UseQueryResult<Todo[]> }) { const data = React.use(query.promise) return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) } export function App() { const query = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) return ( <> <h1>Todos</h1> <React.Suspense fallback={<div>Loading...</div>}> <TodoList query={query} /> </React.Suspense> </> ) } --- ## Page: https://tanstack.com/query/latest/docs/framework/react/installation You can install React Query via NPM, or a good ol' <script> via ESM.sh. ### NPM bash npm i @tanstack/react-query npm i @tanstack/react-query or bash pnpm add @tanstack/react-query pnpm add @tanstack/react-query or bash yarn add @tanstack/react-query yarn add @tanstack/react-query or bash bun add @tanstack/react-query bun add @tanstack/react-query React Query is compatible with React v18+ and works with ReactDOM and React Native. > Wanna give it a spin before you download? Try out the simple or basic examples! ### CDN If you're not using a module bundler or package manager, you can also use this library via an ESM-compatible CDN such as ESM.sh. Simply add a <script type="module"> tag to the bottom of your HTML file: html <script type="module"> import React from 'https://esm.sh/react@18.2.0' import ReactDOM from 'https://esm.sh/react-dom@18.2.0' import { QueryClient } from 'https://esm.sh/@tanstack/react-query' </script> <script type="module"> import React from 'https://esm.sh/react@18.2.0' import ReactDOM from 'https://esm.sh/react-dom@18.2.0' import { QueryClient } from 'https://esm.sh/@tanstack/react-query' </script> > You can find instructions on how to use React without JSX here. ### Requirements React Query is optimized for modern browsers. It is compatible with the following browsers config Chrome >= 91 Firefox >= 90 Edge >= 91 Safari >= 15 iOS >= 15 Opera >= 77 Chrome >= 91 Firefox >= 90 Edge >= 91 Safari >= 15 iOS >= 15 Opera >= 77 > Depending on your environment, you might need to add polyfills. If you want to support older browsers, you need to transpile the library from node\_modules yourselves. ### Recommendations It is recommended to also use our ESLint Plugin Query to help you catch bugs and inconsistencies while you code. You can install it via: bash npm i -D @tanstack/eslint-plugin-query npm i -D @tanstack/eslint-plugin-query or bash pnpm add -D @tanstack/eslint-plugin-query pnpm add -D @tanstack/eslint-plugin-query or bash yarn add -D @tanstack/eslint-plugin-query yarn add -D @tanstack/eslint-plugin-query or bash bun add -D @tanstack/eslint-plugin-query bun add -D @tanstack/eslint-plugin-query --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/important-defaults Out of the box, TanStack Query is configured with **aggressive but sane** defaults. **Sometimes these defaults can catch new users off guard or make learning/debugging difficult if they are unknown by the user.** Keep them in mind as you continue to learn and use TanStack Query: * Query instances via useQuery or useInfiniteQuery by default **consider cached data as stale**. > To change this behavior, you can configure your queries both globally and per-query using the staleTime option. Specifying a longer staleTime means queries will not refetch their data as often * Stale queries are refetched automatically in the background when: * New instances of the query mount * The window is refocused * The network is reconnected * The query is optionally configured with a refetch interval > To change this functionality, you can use options like refetchOnMount, refetchOnWindowFocus, refetchOnReconnect and refetchInterval. * Query results that have no more active instances of useQuery, useInfiniteQuery or query observers are labeled as "inactive" and remain in the cache in case they are used again at a later time. * By default, "inactive" queries are garbage collected after **5 minutes**. > To change this, you can alter the default gcTime for queries to something other than 1000 \* 60 \* 5 milliseconds. * Queries that fail are **silently retried 3 times, with exponential backoff delay** before capturing and displaying an error to the UI. > To change this, you can alter the default retry and retryDelay options for queries to something other than 3 and the default exponential backoff function. * Query results by default are **structurally shared to detect if data has actually changed** and if not, **the data reference remains unchanged** to better help with value stabilization with regards to useMemo and useCallback. If this concept sounds foreign, then don't worry about it! 99.9% of the time you will not need to disable this and it makes your app more performant at zero cost to you. > Structural sharing only works with JSON-compatible values, any other value types will always be considered as changed. If you are seeing performance issues because of large responses for example, you can disable this feature with the config.structuralSharing flag. If you are dealing with non-JSON compatible values in your query responses and still want to detect if data has changed or not, you can provide your own custom function as config.structuralSharing to compute a value from the old and new responses, retaining references as required. ## Further Reading Have a look at the following articles from our Community Resources for further explanations of the defaults: * Practical React Query * React Query as a State Manager --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/queries ## Query Basics A query is a declarative dependency on an asynchronous source of data that is tied to a **unique key**. A query can be used with any Promise based method (including GET and POST methods) to fetch data from a server. If your method modifies data on the server, we recommend using Mutations instead. To subscribe to a query in your components or custom hooks, call the useQuery hook with at least: * A **unique key for the query** * A function that returns a promise that: * Resolves the data, or * Throws an error tsx import { useQuery } from '@tanstack/react-query' function App() { const info = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList }) } import { useQuery } from '@tanstack/react-query' function App() { const info = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList }) } The **unique key** you provide is used internally for refetching, caching, and sharing your queries throughout your application. The query result returned by useQuery contains all of the information about the query that you'll need for templating and any other usage of the data: tsx const result = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList }) const result = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList }) The result object contains a few very important states you'll need to be aware of to be productive. A query can only be in one of the following states at any given moment: * isPending or status === 'pending' - The query has no data yet * isError or status === 'error' - The query encountered an error * isSuccess or status === 'success' - The query was successful and data is available Beyond those primary states, more information is available depending on the state of the query: * error - If the query is in an isError state, the error is available via the error property. * data - If the query is in an isSuccess state, the data is available via the data property. * isFetching - In any state, if the query is fetching at any time (including background refetching) isFetching will be true. For **most** queries, it's usually sufficient to check for the isPending state, then the isError state, then finally, assume that the data is available and render the successful state: tsx function Todos() { const { isPending, isError, data, error } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) if (isPending) { return <span>Loading...</span> } if (isError) { return <span>Error: {error.message}</span> } // We can assume by this point that `isSuccess === true` return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) } function Todos() { const { isPending, isError, data, error } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) if (isPending) { return <span>Loading...</span> } if (isError) { return <span>Error: {error.message}</span> } // We can assume by this point that `isSuccess === true` return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) } If booleans aren't your thing, you can always use the status state as well: tsx function Todos() { const { status, data, error } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) if (status === 'pending') { return <span>Loading...</span> } if (status === 'error') { return <span>Error: {error.message}</span> } // also status === 'success', but "else" logic works, too return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) } function Todos() { const { status, data, error } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) if (status === 'pending') { return <span>Loading...</span> } if (status === 'error') { return <span>Error: {error.message}</span> } // also status === 'success', but "else" logic works, too return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) } TypeScript will also narrow the type of data correctly if you've checked for pending and error before accessing it. ### FetchStatus In addition to the status field, you will also get an additional fetchStatus property with the following options: * fetchStatus === 'fetching' - The query is currently fetching. * fetchStatus === 'paused' - The query wanted to fetch, but it is paused. Read more about this in the Network Mode guide. * fetchStatus === 'idle' - The query is not doing anything at the moment. ### Why two different states? Background refetches and stale-while-revalidate logic make all combinations for status and fetchStatus possible. For example: * a query in success status will usually be in idle fetchStatus, but it could also be in fetching if a background refetch is happening. * a query that mounts and has no data will usually be in pending status and fetching fetchStatus, but it could also be paused if there is no network connection. So keep in mind that a query can be in pending state without actually fetching data. As a rule of thumb: * The status gives information about the data: Do we have any or not? * The fetchStatus gives information about the queryFn: Is it running or not? ## Further Reading For an alternative way of performing status checks, have a look at the Community Resources. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/query-keys At its core, TanStack Query manages query caching for you based on query keys. Query keys have to be an Array at the top level, and 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, and **unique to the query's data**, you can use it! ## Simple Query Keys The simplest form of a key is an array with constants values. This format is useful for: * Generic List/Index resources * Non-hierarchical resources tsx // A list of todos useQuery({ queryKey: ['todos'], ... }) // Something else, whatever! useQuery({ queryKey: ['something', 'special'], ... }) // A list of todos useQuery({ queryKey: ['todos'], ... }) // Something else, whatever! useQuery({ queryKey: ['something', 'special'], ... }) ## Array Keys with variables When a query needs more information to uniquely describe its data, you can use an array with a string and any number of serializable objects to describe it. This is useful for: * Hierarchical or nested resources * It's common to pass an ID, index, or other primitive to uniquely identify the item * Queries with additional parameters * It's common to pass an object of additional options tsx // An individual todo useQuery({ queryKey: ['todo', 5], ... }) // An individual todo in a "preview" format useQuery({ queryKey: ['todo', 5, { preview: true }], ...}) // A list of todos that are "done" useQuery({ queryKey: ['todos', { type: 'done' }], ... }) // An individual todo useQuery({ queryKey: ['todo', 5], ... }) // An individual todo in a "preview" format useQuery({ queryKey: ['todo', 5, { preview: true }], ...}) // A list of todos that are "done" useQuery({ queryKey: ['todos', { type: 'done' }], ... }) ## Query Keys are hashed deterministically! This means that no matter the order of keys in objects, all of the following queries are considered equal: tsx useQuery({ queryKey: ['todos', { status, page }], ... }) useQuery({ queryKey: ['todos', { page, status }], ...}) useQuery({ queryKey: ['todos', { page, status, other: undefined }], ... }) useQuery({ queryKey: ['todos', { status, page }], ... }) useQuery({ queryKey: ['todos', { page, status }], ...}) useQuery({ queryKey: ['todos', { page, status, other: undefined }], ... }) The following query keys, however, are not equal. Array item order matters! tsx useQuery({ queryKey: ['todos', status, page], ... }) useQuery({ queryKey: ['todos', page, status], ...}) useQuery({ queryKey: ['todos', undefined, page, status], ...}) useQuery({ queryKey: ['todos', status, page], ... }) useQuery({ queryKey: ['todos', page, status], ...}) useQuery({ queryKey: ['todos', undefined, page, status], ...}) ## If your query function depends on a variable, include it in your query key Since query keys uniquely describe the data they are fetching, they should include any variables you use in your query function that **change**. For example: tsx function Todos({ todoId }) { const result = useQuery({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId), }) } function Todos({ todoId }) { const result = useQuery({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId), }) } Note that 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 any time a variable changes, _queries will be refetched automatically_ (depending on your staleTime settings). See the exhaustive-deps section for more information and examples. ## Further reading For tips on organizing Query Keys in larger applications, have a look at Effective React Query Keys and check the Query Key Factory Package from the Community Resources. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/query-functions A query function can be literally any function that **returns a promise**. The promise that is returned should either **resolve the data** or **throw an error**. All of the following are valid query function configurations: tsx useQuery({ queryKey: ['todos'], queryFn: fetchAllTodos }) useQuery({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId) }) useQuery({ queryKey: ['todos', todoId], queryFn: async () => { const data = await fetchTodoById(todoId) return data }, }) useQuery({ queryKey: ['todos', todoId], queryFn: ({ queryKey }) => fetchTodoById(queryKey[1]), }) useQuery({ queryKey: ['todos'], queryFn: fetchAllTodos }) useQuery({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId) }) useQuery({ queryKey: ['todos', todoId], queryFn: async () => { const data = await fetchTodoById(todoId) return data }, }) useQuery({ queryKey: ['todos', todoId], queryFn: ({ queryKey }) => fetchTodoById(queryKey[1]), }) ## Handling and Throwing Errors For TanStack Query to determine a query has errored, the query function **must throw** or return a **rejected Promise**. Any error that is thrown in the query function will be persisted on the error state of the query. tsx const { error } = useQuery({ queryKey: ['todos', todoId], queryFn: async () => { if (somethingGoesWrong) { throw new Error('Oh no!') } if (somethingElseGoesWrong) { return Promise.reject(new Error('Oh no!')) } return data }, }) const { error } = useQuery({ queryKey: ['todos', todoId], queryFn: async () => { if (somethingGoesWrong) { throw new Error('Oh no!') } if (somethingElseGoesWrong) { return Promise.reject(new Error('Oh no!')) } return data }, }) ## Usage with fetch and other clients that do not throw by default While most utilities like axios or graphql-request automatically throw errors for unsuccessful HTTP calls, some utilities like fetch do not throw errors by default. If that's the case, you'll need to throw them on your own. Here is a simple way to do that with the popular fetch API: tsx useQuery({ queryKey: ['todos', todoId], queryFn: async () => { const response = await fetch('/todos/' + todoId) if (!response.ok) { throw new Error('Network response was not ok') } return response.json() }, }) useQuery({ queryKey: ['todos', todoId], queryFn: async () => { const response = await fetch('/todos/' + todoId) if (!response.ok) { throw new Error('Network response was not ok') } return response.json() }, }) ## Query Function Variables Query keys are not just for uniquely identifying the data you are fetching, but are also conveniently passed into your query function as part of the QueryFunctionContext. While not always necessary, this makes it possible to extract your query functions if needed: tsx function Todos({ status, page }) { const result = useQuery({ queryKey: ['todos', { status, page }], queryFn: fetchTodoList, }) } // Access the key, status and page variables in your query function! function fetchTodoList({ queryKey }) { const [_key, { status, page }] = queryKey return new Promise() } function Todos({ status, page }) { const result = useQuery({ queryKey: ['todos', { status, page }], queryFn: fetchTodoList, }) } // Access the key, status and page variables in your query function! function fetchTodoList({ queryKey }) { const [_key, { status, page }] = queryKey return new Promise() } ### QueryFunctionContext The QueryFunctionContext is the object passed to each query function. It consists of: * queryKey: QueryKey: Query Keys * client: QueryClient: QueryClient * signal?: AbortSignal * AbortSignal instance provided by TanStack Query * Can be used for Query Cancellation * meta: Record<string, unknown> | undefined * an optional field you can fill with additional information about your query Additionally, Infinite Queries get the following options passed: * pageParam: TPageParam * the page parameter used to fetch the current page * direction: 'forward' | 'backward' * **deprecated** * the direction of the current page fetch * To get access to the direction of the current page fetch, please add a direction to pageParam from getNextPageParam and getPreviousPageParam. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/query-options One of the best ways to share queryKey and queryFn between multiple places, yet keep them co-located to one another, is to use the queryOptions helper. At runtime, this helper just returns whatever you pass into it, but it has a lot of advantages when using it with TypeScript. You can define all possible options for a query in one place, and you'll also get type inference and type safety for all of them. ts import { queryOptions } from '@tanstack/react-query' function groupOptions(id: number) { return queryOptions({ queryKey: ['groups', id], queryFn: () => fetchGroups(id), staleTime: 5 * 1000, }) } // usage: useQuery(groupOptions(1)) useSuspenseQuery(groupOptions(5)) useQueries({ queries: [groupOptions(1), groupOptions(2)], }) queryClient.prefetchQuery(groupOptions(23)) queryClient.setQueryData(groupOptions(42).queryKey, newGroups) import { queryOptions } from '@tanstack/react-query' function groupOptions(id: number) { return queryOptions({ queryKey: ['groups', id], queryFn: () => fetchGroups(id), staleTime: 5 * 1000, }) } // usage: useQuery(groupOptions(1)) useSuspenseQuery(groupOptions(5)) useQueries({ queries: [groupOptions(1), groupOptions(2)], }) queryClient.prefetchQuery(groupOptions(23)) queryClient.setQueryData(groupOptions(42).queryKey, newGroups) For Infinite Queries, a separate infiniteQueryOptions helper is available. You can still override some options at the component level. A very common and useful pattern is to create per-component select functions: ts // Type inference still works, so query.data will be the return type of select instead of queryFn const query = useQuery({ ...groupOptions(1), select: (data) => data.groupName, }) // Type inference still works, so query.data will be the return type of select instead of queryFn const query = useQuery({ ...groupOptions(1), select: (data) => data.groupName, }) --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/network-mode TanStack Query provides three different network modes to distinguish how Queries and Mutations should behave if you have no network connection. This mode can be set for each Query / Mutation individually, or globally via the query / mutation defaults. Since TanStack Query is most often used for data fetching in combination with data fetching libraries, the default network mode is online. ## Network Mode: online In this mode, Queries and Mutations will not fire unless you have network connection. This is the default mode. If a fetch is initiated for a query, it will always stay in the state (pending, error, success) it is in if the fetch cannot be made because there is no network connection. However, a fetchStatus is exposed additionally. This can be either: * fetching: The queryFn is really executing - a request is in-flight. * paused: The query is not executing - it is paused until you have connection again * idle: The query is not fetching and not paused The flags isFetching and isPaused are derived from this state and exposed for convenience. > Keep in mind that it might not be enough to check for pending state to show a loading spinner. Queries can be in state: 'pending', but fetchStatus: 'paused' if they are mounting for the first time, and you have no network connection. If a query runs because you are online, but you go offline while the fetch is still happening, TanStack Query will also pause the retry mechanism. Paused queries will then continue to run once you re-gain network connection. This is independent of refetchOnReconnect (which also defaults to true in this mode), because it is not a refetch, but rather a continue. If the query has been cancelled in the meantime, it will not continue. ## Network Mode: always In this mode, TanStack Query will always fetch and ignore the online / offline state. This is likely the mode you want to choose if you use TanStack Query in an environment where you don't need an active network connection for your Queries to work - e.g. if you just read from AsyncStorage, or if you just want to return Promise.resolve(5) from your queryFn. * Queries will never be paused because you have no network connection. * Retries will also not pause - your Query will go to error state if it fails. * refetchOnReconnect defaults to false in this mode, because reconnecting to the network is not a good indicator anymore that stale queries should be refetched. You can still turn it on if you want. ## Network Mode: offlineFirst This mode is the middle ground between the first two options, where TanStack Query will run the queryFn once, but then pause retries. This is very handy if you have a serviceWorker that intercepts a request for caching like in an offline-first PWA, or if you use HTTP caching via the Cache-Control header. In those situations, the first fetch might succeed because it comes from an offline storage / cache. However, if there is a cache miss, the network request will go out and fail, in which case this mode behaves like an online query - pausing retries. ## Devtools The TanStack Query Devtools will show Queries in a paused state if they would be fetching, but there is no network connection. There is also a toggle button to _Mock offline behavior_. Please note that this button will _not_ actually mess with your network connection (you can do that in the browser devtools), but it will set the OnlineManager in an offline state. ## Signature * networkMode: 'online' | 'always' | 'offlineFirst' * optional * defaults to 'online' --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/parallel-queries "Parallel" queries are queries that are executed in parallel, or at the same time so as to maximize fetching concurrency. ## Manual Parallel Queries When the number of parallel queries does not change, there is **no extra effort** to use parallel queries. Just use any number of TanStack Query's useQuery and useInfiniteQuery hooks side-by-side! tsx function App () { // The following queries will execute in parallel const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers }) const teamsQuery = useQuery({ queryKey: ['teams'], queryFn: fetchTeams }) const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: fetchProjects }) ... } function App () { // The following queries will execute in parallel const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers }) const teamsQuery = useQuery({ queryKey: ['teams'], queryFn: fetchTeams }) const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: fetchProjects }) ... } > When using React Query in suspense mode, this pattern of parallelism does not work, since the first query would throw a promise internally and would suspend the component before the other queries run. To get around this, you'll either need to use the useSuspenseQueries hook (which is suggested) or orchestrate your own parallelism with separate components for each useSuspenseQuery instance. ## Dynamic Parallel Queries with useQueries If the number of queries you need to execute is changing from render to render, you cannot use manual querying since that would violate the rules of hooks. Instead, TanStack Query provides a useQueries hook, which you can use to dynamically execute as many queries in parallel as you'd like. useQueries accepts an **options object** with a **queries key** whose value is an **array of query objects**. It returns an **array of query results**: tsx function App({ users }) { const userQueries = useQueries({ queries: users.map((user) => { return { queryKey: ['user', user.id], queryFn: () => fetchUserById(user.id), } }), }) } function App({ users }) { const userQueries = useQueries({ queries: users.map((user) => { return { queryKey: ['user', user.id], queryFn: () => fetchUserById(user.id), } }), }) } --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/dependent-queries ## useQuery dependent Query Dependent (or serial) queries depend on previous ones to finish before they can execute. To achieve this, it's as easy as using the enabled option to tell a query when it is ready to run: tsx // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) The projects query will start in: tsx status: 'pending' isPending: true fetchStatus: 'idle' status: 'pending' isPending: true fetchStatus: 'idle' As soon as the user is available, the projects query will be enabled and will then transition to: tsx status: 'pending' isPending: true fetchStatus: 'fetching' status: 'pending' isPending: true fetchStatus: 'fetching' Once we have the projects, it will go to: tsx status: 'success' isPending: false fetchStatus: 'idle' status: 'success' isPending: false fetchStatus: 'idle' ## useQueries dependent Query Dynamic parallel query - useQueries can depend on a previous query also, here's how to achieve this: tsx // Get the users ids const { data: userIds } = useQuery({ queryKey: ['users'], queryFn: getUsersData, select: (users) => users.map((user) => user.id), }) // Then get the users messages const usersMessages = useQueries({ queries: userIds ? userIds.map((id) => { return { queryKey: ['messages', id], queryFn: () => getMessagesByUsers(id), } }) : [], // if users is undefined, an empty array will be returned }) // Get the users ids const { data: userIds } = useQuery({ queryKey: ['users'], queryFn: getUsersData, select: (users) => users.map((user) => user.id), }) // Then get the users messages const usersMessages = useQueries({ queries: userIds ? userIds.map((id) => { return { queryKey: ['messages', id], queryFn: () => getMessagesByUsers(id), } }) : [], // if users is undefined, an empty array will be returned }) **Note** that useQueries return an **array of query results** ## A note about performance Dependent queries by definition constitutes a form of request waterfall, which hurts performance. If we pretend both queries take the same amount of time, doing them serially instead of in parallel always takes twice as much time, which is especially hurtful when it happens on a client that has high latency. If you can, it's always better to restructure the backend APIs so that both queries can be fetched in parallel, though that might not always be practically feasible. In the example above, instead of first fetching getUserByEmail to be able to getProjectsByUser, introducing a new getProjectsByUserEmail query would flatten the waterfall. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/background-fetching-indicators A query's status === 'pending' state is sufficient enough to show the initial hard-loading state for a query, but sometimes you may want to display an additional indicator that a query is refetching in the background. To do this, queries also supply you with an isFetching boolean that you can use to show that it's in a fetching state, regardless of the state of the status variable: tsx function Todos() { const { status, data: todos, error, isFetching, } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, }) return status === 'pending' ? ( <span>Loading...</span> ) : status === 'error' ? ( <span>Error: {error.message}</span> ) : ( <> {isFetching ? <div>Refreshing...</div> : null} <div> {todos.map((todo) => ( <Todo todo={todo} /> ))} </div> </> ) } function Todos() { const { status, data: todos, error, isFetching, } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, }) return status === 'pending' ? ( <span>Loading...</span> ) : status === 'error' ? ( <span>Error: {error.message}</span> ) : ( <> {isFetching ? <div>Refreshing...</div> : null} <div> {todos.map((todo) => ( <Todo todo={todo} /> ))} </div> </> ) } ## Displaying Global Background Fetching Loading State In addition to individual query loading states, if you would like to show a global loading indicator when **any** queries are fetching (including in the background), you can use the useIsFetching hook: tsx import { useIsFetching } from '@tanstack/react-query' function GlobalLoadingIndicator() { const isFetching = useIsFetching() return isFetching ? ( <div>Queries are fetching in the background...</div> ) : null } import { useIsFetching } from '@tanstack/react-query' function GlobalLoadingIndicator() { const isFetching = useIsFetching() return isFetching ? ( <div>Queries are fetching in the background...</div> ) : null } --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/window-focus-refetching If a user leaves your application and returns and the query data is stale, **TanStack Query automatically requests fresh data for you in the background**. You can disable this globally or per-query using the refetchOnWindowFocus option: #### Disabling Globally tsx // const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, // default: true }, }, }) function App() { return <QueryClientProvider client={queryClient}>...</QueryClientProvider> } // const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, // default: true }, }, }) function App() { return <QueryClientProvider client={queryClient}>...</QueryClientProvider> } #### Disabling Per-Query tsx useQuery({ queryKey: ['todos'], queryFn: fetchTodos, refetchOnWindowFocus: false, }) useQuery({ queryKey: ['todos'], queryFn: fetchTodos, refetchOnWindowFocus: false, }) ## Custom Window Focus Event In rare circumstances, you may want to manage your own window focus events that trigger TanStack Query to revalidate. To do this, TanStack Query provides a focusManager.setEventListener function that supplies you the callback that should be fired when the window is focused and allows you to set up your own events. When calling focusManager.setEventListener, the previously set handler is removed (which in most cases will be the default handler) and your new handler is used instead. For example, this is the default handler: tsx focusManager.setEventListener((handleFocus) => { // Listen to visibilitychange if (typeof window !== 'undefined' && window.addEventListener) { const visibilitychangeHandler = () => { handleFocus(document.visibilityState === 'visible') } window.addEventListener('visibilitychange', visibilitychangeHandler, false) return () => { // Be sure to unsubscribe if a new handler is set window.removeEventListener('visibilitychange', visibilitychangeHandler) } } }) focusManager.setEventListener((handleFocus) => { // Listen to visibilitychange if (typeof window !== 'undefined' && window.addEventListener) { const visibilitychangeHandler = () => { handleFocus(document.visibilityState === 'visible') } window.addEventListener('visibilitychange', visibilitychangeHandler, false) return () => { // Be sure to unsubscribe if a new handler is set window.removeEventListener('visibilitychange', visibilitychangeHandler) } } }) ## Managing Focus in React Native Instead of event listeners on window, React Native provides focus information through the AppState module. You can use the AppState "change" event to trigger an update when the app state changes to "active": tsx import { AppState } from 'react-native' import { focusManager } from '@tanstack/react-query' function onAppStateChange(status: AppStateStatus) { if (Platform.OS !== 'web') { focusManager.setFocused(status === 'active') } } useEffect(() => { const subscription = AppState.addEventListener('change', onAppStateChange) return () => subscription.remove() }, []) import { AppState } from 'react-native' import { focusManager } from '@tanstack/react-query' function onAppStateChange(status: AppStateStatus) { if (Platform.OS !== 'web') { focusManager.setFocused(status === 'active') } } useEffect(() => { const subscription = AppState.addEventListener('change', onAppStateChange) return () => subscription.remove() }, []) ## Managing focus state tsx import { focusManager } from '@tanstack/react-query' // Override the default focus state focusManager.setFocused(true) // Fallback to the default focus check focusManager.setFocused(undefined) import { focusManager } from '@tanstack/react-query' // Override the default focus state focusManager.setFocused(true) // Fallback to the default focus check focusManager.setFocused(undefined) --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/disabling-queries If you ever want to disable a query from automatically running, you can use the enabled = false option. The enabled option also accepts a callback that returns a boolean. When enabled is false: * If the query has cached data, then the query will be initialized in the status === 'success' or isSuccess state. * If the query does not have cached data, then the query will start in the status === 'pending' and fetchStatus === 'idle' state. * The query will not automatically fetch on mount. * The query will not automatically refetch in the background. * The query will ignore query client invalidateQueries and refetchQueries calls that would normally result in the query refetching. * refetch returned from useQuery can be used to manually trigger the query to fetch. However, it will not work with skipToken. > TypeScript users may prefer to use skipToken as an alternative to enabled = false. tsx function Todos() { const { isLoading, isError, data, error, refetch, isFetching } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, enabled: false, }) return ( <div> <button onClick={() => refetch()}>Fetch Todos</button> {data ? ( <> <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> </> ) : isError ? ( <span>Error: {error.message}</span> ) : isLoading ? ( <span>Loading...</span> ) : ( <span>Not ready ...</span> )} <div>{isFetching ? 'Fetching...' : null}</div> </div> ) } function Todos() { const { isLoading, isError, data, error, refetch, isFetching } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, enabled: false, }) return ( <div> <button onClick={() => refetch()}>Fetch Todos</button> {data ? ( <> <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> </> ) : isError ? ( <span>Error: {error.message}</span> ) : isLoading ? ( <span>Loading...</span> ) : ( <span>Not ready ...</span> )} <div>{isFetching ? 'Fetching...' : null}</div> </div> ) } Permanently disabling a query opts out of many great features that TanStack Query has to offer (like background refetches), and it's also not the idiomatic way. It takes you from the declarative approach (defining dependencies when your query should run) into an imperative mode (fetch whenever I click here). It is also not possible to pass parameters to refetch. Oftentimes, all you want is a lazy query that defers the initial fetch: ## Lazy Queries The enabled option can not only be used to permanently disable a query, but also to enable / disable it at a later time. A good example would be a filter form where you only want to fire off the first request once the user has entered a filter value: tsx function Todos() { const [filter, setFilter] = React.useState('') const { data } = useQuery({ queryKey: ['todos', filter], queryFn: () => fetchTodos(filter), // ⬇️ disabled as long as the filter is empty enabled: !!filter, }) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data} />} </div> ) } function Todos() { const [filter, setFilter] = React.useState('') const { data } = useQuery({ queryKey: ['todos', filter], queryFn: () => fetchTodos(filter), // ⬇️ disabled as long as the filter is empty enabled: !!filter, }) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data} />} </div> ) } ### isLoading (Previously: isInitialLoading) Lazy queries will be in status: 'pending' right from the start because pending means that there is no data yet. This is technically true, however, since we are not currently fetching any data (as the query is not _enabled_), it also means you likely cannot use this flag to show a loading spinner. If you are using disabled or lazy queries, you can use the isLoading flag instead. It's a derived flag that is computed from: isPending && isFetching so it will only be true if the query is currently fetching for the first time. ## Typesafe disabling of queries using skipToken If you are using TypeScript, you can use the skipToken to disable a query. This is useful when you want to disable a query based on a condition, but you still want to keep the query to be type safe. > IMPORTANT: refetch from useQuery will not work with skipToken. Other than that, skipToken works the same as enabled: false. tsx import { skipToken, useQuery } from '@tanstack/react-query' function Todos() { const [filter, setFilter] = React.useState<string | undefined>() const { data } = useQuery({ queryKey: ['todos', filter], // ⬇️ disabled as long as the filter is undefined or empty queryFn: filter ? () => fetchTodos(filter) : skipToken, }) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data} />} </div> ) } import { skipToken, useQuery } from '@tanstack/react-query' function Todos() { const [filter, setFilter] = React.useState<string | undefined>() const { data } = useQuery({ queryKey: ['todos', filter], // ⬇️ disabled as long as the filter is undefined or empty queryFn: filter ? () => fetchTodos(filter) : skipToken, }) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data} />} </div> ) } --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/query-retries When a useQuery query fails (the query function throws an error), TanStack Query will automatically retry the query if that query's request has not reached the max number of consecutive retries (defaults to 3) or a function is provided to determine if a retry is allowed. You can configure retries both on a global level and an individual query level. * Setting retry = false will disable retries. * Setting retry = 6 will retry failing requests 6 times before showing the final error thrown by the function. * Setting retry = true will infinitely retry failing requests. * Setting retry = (failureCount, error) => ... allows for custom logic based on why the request failed. > On the server, retries default to 0 to make server rendering as fast as possible. tsx import { useQuery } from '@tanstack/react-query' // Make a specific query retry a certain number of times const result = useQuery({ queryKey: ['todos', 1], queryFn: fetchTodoListPage, retry: 10, // Will retry failed requests 10 times before displaying an error }) import { useQuery } from '@tanstack/react-query' // Make a specific query retry a certain number of times const result = useQuery({ queryKey: ['todos', 1], queryFn: fetchTodoListPage, retry: 10, // Will retry failed requests 10 times before displaying an error }) > Info: Contents of the error property will be part of failureReason response property of useQuery until the last retry attempt. So in above example any error contents will be part of failureReason property for first 9 retry attempts (Overall 10 attempts) and finally they will be part of error after last attempt if error persists after all retry attempts. ## Retry Delay By default, retries in TanStack Query do not happen immediately after a request fails. As is standard, a back-off delay is gradually applied to each retry attempt. The default retryDelay is set to double (starting at 1000ms) with each attempt, but not exceed 30 seconds: tsx // Configure for all queries 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> } // Configure for all queries 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> } Though it is not recommended, you can obviously override the retryDelay function/integer in both the Provider and individual query options. If set to an integer instead of a function the delay will always be the same amount of time: tsx const result = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, retryDelay: 1000, // Will always wait 1000ms to retry, regardless of how many retries }) const result = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, retryDelay: 1000, // Will always wait 1000ms to retry, regardless of how many retries }) --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/paginated-queries Rendering paginated data is a very common UI pattern and in TanStack Query, it "just works" by including the page information in the query key: tsx const result = useQuery({ queryKey: ['projects', page], queryFn: fetchProjects, }) const result = useQuery({ queryKey: ['projects', page], queryFn: fetchProjects, }) However, if you run this simple example, you might notice something strange: **The UI jumps in and out of the success and pending states because each new page is treated like a brand new query.** This experience is not optimal and unfortunately is how many tools today insist on working. But not TanStack Query! As you may have guessed, TanStack Query comes with an awesome feature called placeholderData that allows us to get around this. ## Better Paginated Queries with placeholderData Consider the following example where we would ideally want to increment a pageIndex (or cursor) for a query. If we were to use useQuery, **it would still technically work fine**, but the UI would jump in and out of the success and pending states as different queries are created and destroyed for each page or cursor. By setting placeholderData to (previousData) => previousData or keepPreviousData function exported from TanStack Query, we get a few new things: * **The data from the last successful fetch is available while new data is being requested, even though the query key has changed**. * When the new data arrives, the previous data is seamlessly swapped to show the new data. * isPlaceholderData is made available to know what data the query is currently providing you tsx import { keepPreviousData, useQuery } from '@tanstack/react-query' import React from 'react' function Todos() { const [page, setPage] = React.useState(0) const fetchProjects = (page = 0) => fetch('/api/projects?page=' + page).then((res) => res.json()) const { isPending, isError, error, data, isFetching, isPlaceholderData } = useQuery({ queryKey: ['projects', page], queryFn: () => fetchProjects(page), placeholderData: keepPreviousData, }) return ( <div> {isPending ? ( <div>Loading...</div> ) : isError ? ( <div>Error: {error.message}</div> ) : ( <div> {data.projects.map((project) => ( <p key={project.id}>{project.name}</p> ))} </div> )} <span>Current Page: {page + 1}</span> <button onClick={() => setPage((old) => Math.max(old - 1, 0))} disabled={page === 0} > Previous Page </button> <button onClick={() => { if (!isPlaceholderData && data.hasMore) { setPage((old) => old + 1) } }} // Disable the Next Page button until we know a next page is available disabled={isPlaceholderData || !data?.hasMore} > Next Page </button> {isFetching ? <span> Loading...</span> : null} </div> ) } import { keepPreviousData, useQuery } from '@tanstack/react-query' import React from 'react' function Todos() { const [page, setPage] = React.useState(0) const fetchProjects = (page = 0) => fetch('/api/projects?page=' + page).then((res) => res.json()) const { isPending, isError, error, data, isFetching, isPlaceholderData } = useQuery({ queryKey: ['projects', page], queryFn: () => fetchProjects(page), placeholderData: keepPreviousData, }) return ( <div> {isPending ? ( <div>Loading...</div> ) : isError ? ( <div>Error: {error.message}</div> ) : ( <div> {data.projects.map((project) => ( <p key={project.id}>{project.name}</p> ))} </div> )} <span>Current Page: {page + 1}</span> <button onClick={() => setPage((old) => Math.max(old - 1, 0))} disabled={page === 0} > Previous Page </button> <button onClick={() => { if (!isPlaceholderData && data.hasMore) { setPage((old) => old + 1) } }} // Disable the Next Page button until we know a next page is available disabled={isPlaceholderData || !data?.hasMore} > Next Page </button> {isFetching ? <span> Loading...</span> : null} </div> ) } ## Lagging Infinite Query results with placeholderData While not as common, the placeholderData option also works flawlessly with the useInfiniteQuery hook, so you can seamlessly allow your users to continue to see cached data while infinite query keys change over time. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries Rendering lists that can additively "load more" data onto an existing set of data or "infinite scroll" is also a very common UI pattern. TanStack Query supports a useful version of useQuery called useInfiniteQuery for querying these types of lists. When using useInfiniteQuery, you'll notice a few things are different: * data is now an object containing infinite query data: * data.pages array containing the fetched pages * data.pageParams array containing the page params used to fetch the pages * The fetchNextPage and fetchPreviousPage functions are now available (fetchNextPage is required) * The initialPageParam option is now available (and required) to specify the initial page param * The getNextPageParam and getPreviousPageParam options are available for both determining if there is more data to load and the information to fetch it. This information is supplied as an additional parameter in the query function * A hasNextPage boolean is now available and is true if getNextPageParam returns a value other than null or undefined * A hasPreviousPage boolean is now available and is true if getPreviousPageParam returns a value other than null or undefined * The isFetchingNextPage and isFetchingPreviousPage booleans are now available to distinguish between a background refresh state and a loading more state > Note: Options initialData or placeholderData need to conform to the same structure of an object with data.pages and data.pageParams properties. ## Example Let's assume we have an API that returns pages of projects 3 at a time based on a cursor index along with a cursor that can be used to fetch the next group of projects: tsx fetch('/api/projects?cursor=0') // { data: [...], nextCursor: 3} fetch('/api/projects?cursor=3') // { data: [...], nextCursor: 6} fetch('/api/projects?cursor=6') // { data: [...], nextCursor: 9} fetch('/api/projects?cursor=9') // { data: [...] } fetch('/api/projects?cursor=0') // { data: [...], nextCursor: 3} fetch('/api/projects?cursor=3') // { data: [...], nextCursor: 6} fetch('/api/projects?cursor=6') // { data: [...], nextCursor: 9} fetch('/api/projects?cursor=9') // { data: [...] } With this information, we can create a "Load More" UI by: * Waiting for useInfiniteQuery to request the first group of data by default * Returning the information for the next query in getNextPageParam * Calling fetchNextPage function tsx import { useInfiniteQuery } from '@tanstack/react-query' function Projects() { const fetchProjects = async ({ pageParam }) => { const res = await fetch('/api/projects?cursor=' + pageParam) return res.json() } const { data, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, status, } = useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, }) return status === 'pending' ? ( <p>Loading...</p> ) : status === 'error' ? ( <p>Error: {error.message}</p> ) : ( <> {data.pages.map((group, i) => ( <React.Fragment key={i}> {group.data.map((project) => ( <p key={project.id}>{project.name}</p> ))} </React.Fragment> ))} <div> <button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage} > {isFetchingNextPage ? 'Loading more...' : hasNextPage ? 'Load More' : 'Nothing more to load'} </button> </div> <div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div> </> ) } import { useInfiniteQuery } from '@tanstack/react-query' function Projects() { const fetchProjects = async ({ pageParam }) => { const res = await fetch('/api/projects?cursor=' + pageParam) return res.json() } const { data, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, status, } = useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, }) return status === 'pending' ? ( <p>Loading...</p> ) : status === 'error' ? ( <p>Error: {error.message}</p> ) : ( <> {data.pages.map((group, i) => ( <React.Fragment key={i}> {group.data.map((project) => ( <p key={project.id}>{project.name}</p> ))} </React.Fragment> ))} <div> <button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage} > {isFetchingNextPage ? 'Loading more...' : hasNextPage ? 'Load More' : 'Nothing more to load'} </button> </div> <div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div> </> ) } It's essential to understand that calling fetchNextPage while an ongoing fetch is in progress runs the risk of overwriting data refreshes happening in the background. This situation becomes particularly critical when rendering a list and triggering fetchNextPage simultaneously. Remember, there can only be a single ongoing fetch for an InfiniteQuery. A single cache entry is shared for all pages, attempting to fetch twice simultaneously might lead to data overwrites. If you intend to enable simultaneous fetching, you can utilize the { cancelRefetch: false } option (default: true) within fetchNextPage. To ensure a seamless querying process without conflicts, it's highly recommended to verify that the query is not in an isFetching state, especially if the user won't directly control that call. jsx <List onEndReached={() => !isFetchingNextPage && fetchNextPage()} /> <List onEndReached={() => !isFetchingNextPage && fetchNextPage()} /> ## What happens when an infinite query needs to be refetched? When an infinite query becomes stale and needs to be refetched, each group is fetched sequentially, starting from the first one. This ensures that even if the underlying data is mutated, we're not using stale cursors and potentially getting duplicates or skipping records. If an infinite query's results are ever removed from the queryCache, the pagination restarts at the initial state with only the initial group being requested. ## What if I want to implement a bi-directional infinite list? Bi-directional lists can be implemented by using the getPreviousPageParam, fetchPreviousPage, hasPreviousPage and isFetchingPreviousPage properties and functions. tsx useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor, }) useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor, }) ## What if I want to show the pages in reversed order? Sometimes you may want to show the pages in reversed order. If this is case, you can use the select option: tsx useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, select: (data) => ({ pages: [...data.pages].reverse(), pageParams: [...data.pageParams].reverse(), }), }) useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, select: (data) => ({ pages: [...data.pages].reverse(), pageParams: [...data.pageParams].reverse(), }), }) ## What if I want to manually update the infinite query? ### Manually removing first page: tsx queryClient.setQueryData(['projects'], (data) => ({ pages: data.pages.slice(1), pageParams: data.pageParams.slice(1), })) queryClient.setQueryData(['projects'], (data) => ({ pages: data.pages.slice(1), pageParams: data.pageParams.slice(1), })) ### Manually removing a single value from an individual page: tsx const newPagesArray = oldPagesArray?.pages.map((page) => page.filter((val) => val.id !== updatedId), ) ?? [] queryClient.setQueryData(['projects'], (data) => ({ pages: newPagesArray, pageParams: data.pageParams, })) const newPagesArray = oldPagesArray?.pages.map((page) => page.filter((val) => val.id !== updatedId), ) ?? [] queryClient.setQueryData(['projects'], (data) => ({ pages: newPagesArray, pageParams: data.pageParams, })) ### Keep only the first page: tsx queryClient.setQueryData(['projects'], (data) => ({ pages: data.pages.slice(0, 1), pageParams: data.pageParams.slice(0, 1), })) queryClient.setQueryData(['projects'], (data) => ({ pages: data.pages.slice(0, 1), pageParams: data.pageParams.slice(0, 1), })) Make sure to always keep the same data structure of pages and pageParams! ## What if I want to limit the number of pages? In some use cases you may want to limit the number of pages stored in the query data to improve the performance and UX: * when the user can load a large number of pages (memory usage) * when you have to refetch an infinite query that contains dozens of pages (network usage: all the pages are sequentially fetched) The solution is to use a "Limited Infinite Query". This is made possible by using the maxPages option in conjunction with getNextPageParam and getPreviousPageParam to allow fetching pages when needed in both directions. In the following example only 3 pages are kept in the query data pages array. If a refetch is needed, only 3 pages will be refetched sequentially. tsx useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor, maxPages: 3, }) useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor, maxPages: 3, }) ## What if my API doesn't return a cursor? If your API doesn't return a cursor, you can use the pageParam as a cursor. Because getNextPageParam and getPreviousPageParam also get the pageParamof the current page, you can use it to calculate the next / previous page param. tsx return useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, allPages, lastPageParam) => { if (lastPage.length === 0) { return undefined } return lastPageParam + 1 }, getPreviousPageParam: (firstPage, allPages, firstPageParam) => { if (firstPageParam <= 1) { return undefined } return firstPageParam - 1 }, }) return useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, allPages, lastPageParam) => { if (lastPage.length === 0) { return undefined } return lastPageParam + 1 }, getPreviousPageParam: (firstPage, allPages, firstPageParam) => { if (firstPageParam <= 1) { return undefined } return firstPageParam - 1 }, }) --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/initial-query-data There are many ways to supply initial data for a query to the cache before you need it: * Declaratively: * Provide initialData to a query to prepopulate its cache if empty * Imperatively: * Prefetch the data using queryClient.prefetchQuery * Manually place the data into the cache using queryClient.setQueryData ## Using initialData to prepopulate a query There may be times when you already have the initial data for a query available in your app and can simply provide it directly to your query. If and when this is the case, you can use the config.initialData option to set the initial data for a query and skip the initial loading state! > IMPORTANT: initialData is persisted to the cache, so it is not recommended to provide placeholder, partial or incomplete data to this option and instead use placeholderData tsx const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, }) const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, }) ### staleTime and initialDataUpdatedAt By default, initialData is treated as totally fresh, as if it were just fetched. This also means that it will affect how it is interpreted by the staleTime option. * If you configure your query observer with initialData, and no staleTime (the default staleTime: 0), the query will immediately refetch when it mounts: tsx // Will show initialTodos immediately, but also immediately refetch todos after mount const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, }) // Will show initialTodos immediately, but also immediately refetch todos after mount const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, }) * If you configure your query observer with initialData and a staleTime of 1000 ms, the data will be considered fresh for that same amount of time, as if it was just fetched from your query function. tsx // Show initialTodos immediately, but won't refetch until another interaction event is encountered after 1000 ms const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, staleTime: 1000, }) // Show initialTodos immediately, but won't refetch until another interaction event is encountered after 1000 ms const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, staleTime: 1000, }) * So what if your initialData isn't totally fresh? That leaves us with the last configuration that is actually the most accurate and uses an option called initialDataUpdatedAt. This option allows you to pass a numeric JS timestamp in milliseconds of when the initialData itself was last updated, e.g. what Date.now() provides. Take note that if you have a unix timestamp, you'll need to convert it to a JS timestamp by multiplying it by 1000. tsx // Show initialTodos immediately, but won't refetch until another interaction event is encountered after 1000 ms const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, staleTime: 60 * 1000, // 1 minute // This could be 10 seconds ago or 10 minutes ago initialDataUpdatedAt: initialTodosUpdatedTimestamp, // eg. 1608412420052 }) // Show initialTodos immediately, but won't refetch until another interaction event is encountered after 1000 ms const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, staleTime: 60 * 1000, // 1 minute // This could be 10 seconds ago or 10 minutes ago initialDataUpdatedAt: initialTodosUpdatedTimestamp, // eg. 1608412420052 }) This option allows the staleTime to be used for its original purpose, determining how fresh the data needs to be, while also allowing the data to be refetched on mount if the initialData is older than the staleTime. In the example above, our data needs to be fresh within 1 minute, and we can hint to the query when the initialData was last updated so the query can decide for itself whether the data needs to be refetched again or not. > If you would rather treat your data as **prefetched data**, we recommend that you use the prefetchQuery or fetchQuery APIs to populate the cache beforehand, thus letting you configure your staleTime independently from your initialData ### Initial Data Function If the process for accessing a query's initial data is intensive or just not something you want to perform on every render, you can pass a function as the initialData value. This function will be executed only once when the query is initialized, saving you precious memory and/or CPU: tsx const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: () => getExpensiveTodos(), }) const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: () => getExpensiveTodos(), }) ### Initial Data from Cache In some circumstances, you may be able to provide the initial data for a query from the cached result of another. A good example of this would be searching the cached data from a todos list query for an individual todo item, then using that as the initial data for your individual todo query: tsx const result = useQuery({ queryKey: ['todo', todoId], queryFn: () => fetch('/todos'), initialData: () => { // Use a todo from the 'todos' query as the initial data for this todo query return queryClient.getQueryData(['todos'])?.find((d) => d.id === todoId) }, }) const result = useQuery({ queryKey: ['todo', todoId], queryFn: () => fetch('/todos'), initialData: () => { // Use a todo from the 'todos' query as the initial data for this todo query return queryClient.getQueryData(['todos'])?.find((d) => d.id === todoId) }, }) ### Initial Data from the cache with initialDataUpdatedAt Getting initial data from the cache means the source query you're using to look up the initial data from is likely old. Instead of using an artificial staleTime to keep your query from refetching immediately, it's suggested that you pass the source query's dataUpdatedAt to initialDataUpdatedAt. This provides the query instance with all the information it needs to determine if and when the query needs to be refetched, regardless of initial data being provided. tsx const result = useQuery({ queryKey: ['todos', todoId], queryFn: () => fetch(`/todos/${todoId}`), initialData: () => queryClient.getQueryData(['todos'])?.find((d) => d.id === todoId), initialDataUpdatedAt: () => queryClient.getQueryState(['todos'])?.dataUpdatedAt, }) const result = useQuery({ queryKey: ['todos', todoId], queryFn: () => fetch(`/todos/${todoId}`), initialData: () => queryClient.getQueryData(['todos'])?.find((d) => d.id === todoId), initialDataUpdatedAt: () => queryClient.getQueryState(['todos'])?.dataUpdatedAt, }) ### Conditional Initial Data from Cache If the source query you're using to look up the initial data from is old, you may not want to use the cached data at all and just fetch from the server. To make this decision easier, you can use the queryClient.getQueryState method instead to get more information about the source query, including a state.dataUpdatedAt timestamp you can use to decide if the query is "fresh" enough for your needs: tsx const result = useQuery({ queryKey: ['todo', todoId], queryFn: () => fetch(`/todos/${todoId}`), initialData: () => { // Get the query state const state = queryClient.getQueryState(['todos']) // If the query exists and has data that is no older than 10 seconds... if (state && Date.now() - state.dataUpdatedAt <= 10 * 1000) { // return the individual todo return state.data.find((d) => d.id === todoId) } // Otherwise, return undefined and let it fetch from a hard loading state! }, }) const result = useQuery({ queryKey: ['todo', todoId], queryFn: () => fetch(`/todos/${todoId}`), initialData: () => { // Get the query state const state = queryClient.getQueryState(['todos']) // If the query exists and has data that is no older than 10 seconds... if (state && Date.now() - state.dataUpdatedAt <= 10 * 1000) { // return the individual todo return state.data.find((d) => d.id === todoId) } // Otherwise, return undefined and let it fetch from a hard loading state! }, }) ## Further reading For a comparison between Initial Data and Placeholder Data, have a look at the Community Resources. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/placeholder-query-data ## What is placeholder data? Placeholder data allows a query to behave as if it already has data, similar to the initialData option, but **the data is not persisted to the cache**. This comes in handy for situations where you have enough partial (or fake) data to render the query successfully while the actual data is fetched in the background. > Example: An individual blog post query could pull "preview" data from a parent list of blog posts that only include title and a small snippet of the post body. You would not want to persist this partial data to the query result of the individual query, but it is useful for showing the content layout as quickly as possible while the actual query finishes to fetch the entire object. There are a few ways to supply placeholder data for a query to the cache before you need it: * Declaratively: * Provide placeholderData to a query to prepopulate its cache if empty * Imperatively: * Prefetch or fetch the data using queryClient and the placeholderData option When we use placeholderData, our Query will not be in a pending state - it will start out as being in success state, because we have data to display - even if that data is just "placeholder" data. To distinguish it from "real" data, we will also have the isPlaceholderData flag set to true on the Query result. ## Placeholder Data as a Value tsx function Todos() { const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), placeholderData: placeholderTodos, }) } function Todos() { const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), placeholderData: placeholderTodos, }) } ### Placeholder Data Memoization If the process for accessing a query's placeholder data is intensive or just not something you want to perform on every render, you can memoize the value: tsx function Todos() { const placeholderData = useMemo(() => generateFakeTodos(), []) const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), placeholderData, }) } function Todos() { const placeholderData = useMemo(() => generateFakeTodos(), []) const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), placeholderData, }) } ## Placeholder Data as a Function placeholderData can also be a function, where you can get access to the data and Query meta information of a "previous" successful Query. This is useful for situations where you want to use the data from one query as the placeholder data for another query. When the QueryKey changes, e.g. from \['todos', 1\] to \['todos', 2\], we can keep displaying "old" data instead of having to show a loading spinner while data is _transitioning_ from one Query to the next. For more information, see Paginated Queries. tsx const result = useQuery({ queryKey: ['todos', id], queryFn: () => fetch(`/todos/${id}`), placeholderData: (previousData, previousQuery) => previousData, }) const result = useQuery({ queryKey: ['todos', id], queryFn: () => fetch(`/todos/${id}`), placeholderData: (previousData, previousQuery) => previousData, }) ### Placeholder Data from Cache In some circumstances, you may be able to provide the placeholder data for a query from the cached result of another. A good example of this would be searching the cached data from a blog post list query for a preview version of the post, then using that as the placeholder data for your individual post query: tsx function Todo({ blogPostId }) { const queryClient = useQueryClient() const result = useQuery({ queryKey: ['blogPost', blogPostId], queryFn: () => fetch(`/blogPosts/${blogPostId}`), placeholderData: () => { // Use the smaller/preview version of the blogPost from the 'blogPosts' // query as the placeholder data for this blogPost query return queryClient .getQueryData(['blogPosts']) ?.find((d) => d.id === blogPostId) }, }) } function Todo({ blogPostId }) { const queryClient = useQueryClient() const result = useQuery({ queryKey: ['blogPost', blogPostId], queryFn: () => fetch(`/blogPosts/${blogPostId}`), placeholderData: () => { // Use the smaller/preview version of the blogPost from the 'blogPosts' // query as the placeholder data for this blogPost query return queryClient .getQueryData(['blogPosts']) ?.find((d) => d.id === blogPostId) }, }) } ## Further reading For a comparison between Placeholder Data and Initial Data, have a look at the Community Resources. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/mutations Unlike queries, mutations are typically used to create/update/delete data or perform server side-effects. For this purpose, TanStack Query exports a useMutation hook. Here's an example of a mutation that adds a new todo to the server: tsx function App() { const mutation = useMutation({ mutationFn: (newTodo) => { return axios.post('/todos', newTodo) }, }) return ( <div> {mutation.isPending ? ( 'Adding todo...' ) : ( <> {mutation.isError ? ( <div>An error occurred: {mutation.error.message}</div> ) : null} {mutation.isSuccess ? <div>Todo added!</div> : null} <button onClick={() => { mutation.mutate({ id: new Date(), title: 'Do Laundry' }) }} > Create Todo </button> </> )} </div> ) } function App() { const mutation = useMutation({ mutationFn: (newTodo) => { return axios.post('/todos', newTodo) }, }) return ( <div> {mutation.isPending ? ( 'Adding todo...' ) : ( <> {mutation.isError ? ( <div>An error occurred: {mutation.error.message}</div> ) : null} {mutation.isSuccess ? <div>Todo added!</div> : null} <button onClick={() => { mutation.mutate({ id: new Date(), title: 'Do Laundry' }) }} > Create Todo </button> </> )} </div> ) } A mutation can only be in one of the following states at any given moment: * isIdle or status === 'idle' - The mutation is currently idle or in a fresh/reset state * isPending or status === 'pending' - The mutation is currently running * isError or status === 'error' - The mutation encountered an error * isSuccess or status === 'success' - The mutation was successful and mutation data is available Beyond those primary states, more information is available depending on the state of the mutation: * error - If the mutation is in an error state, the error is available via the error property. * data - If the mutation is in a success state, the data is available via the data property. In the example above, you also saw that you can pass variables to your mutations function by calling the mutate function with a **single variable or object**. Even with just variables, mutations aren't all that special, but when used with the onSuccess option, the Query Client's invalidateQueries method and the Query Client's setQueryData method, mutations become a very powerful tool. > IMPORTANT: The mutate function is an asynchronous function, which means you cannot use it directly in an event callback in **React 16 and earlier**. If you need to access the event in onSubmit you need to wrap mutate in another function. This is due to React event pooling. tsx // This will not work in React 16 and earlier const CreateTodo = () => { const mutation = useMutation({ mutationFn: (event) => { event.preventDefault() return fetch('/api', new FormData(event.target)) }, }) return <form onSubmit={mutation.mutate}>...</form> } // This will work const CreateTodo = () => { const mutation = useMutation({ mutationFn: (formData) => { return fetch('/api', formData) }, }) const onSubmit = (event) => { event.preventDefault() mutation.mutate(new FormData(event.target)) } return <form onSubmit={onSubmit}>...</form> } // This will not work in React 16 and earlier const CreateTodo = () => { const mutation = useMutation({ mutationFn: (event) => { event.preventDefault() return fetch('/api', new FormData(event.target)) }, }) return <form onSubmit={mutation.mutate}>...</form> } // This will work const CreateTodo = () => { const mutation = useMutation({ mutationFn: (formData) => { return fetch('/api', formData) }, }) const onSubmit = (event) => { event.preventDefault() mutation.mutate(new FormData(event.target)) } return <form onSubmit={onSubmit}>...</form> } ## Resetting Mutation State It's sometimes the case that you need to clear the error or data of a mutation request. To do this, you can use the reset function to handle this: tsx const CreateTodo = () => { const [title, setTitle] = useState('') const mutation = useMutation({ mutationFn: createTodo }) const onCreateTodo = (e) => { e.preventDefault() mutation.mutate({ title }) } return ( <form onSubmit={onCreateTodo}> {mutation.error && ( <h5 onClick={() => mutation.reset()}>{mutation.error}</h5> )} <input type="text" value={title} onChange={(e) => setTitle(e.target.value)} /> <br /> <button type="submit">Create Todo</button> </form> ) } const CreateTodo = () => { const [title, setTitle] = useState('') const mutation = useMutation({ mutationFn: createTodo }) const onCreateTodo = (e) => { e.preventDefault() mutation.mutate({ title }) } return ( <form onSubmit={onCreateTodo}> {mutation.error && ( <h5 onClick={() => mutation.reset()}>{mutation.error}</h5> )} <input type="text" value={title} onChange={(e) => setTitle(e.target.value)} /> <br /> <button type="submit">Create Todo</button> </form> ) } ## Mutation Side Effects useMutation comes with some helper options that allow quick and easy side-effects at any stage during the mutation lifecycle. These come in handy for both invalidating and refetching queries after mutations and even optimistic updates tsx useMutation({ mutationFn: addTodo, onMutate: (variables) => { // A mutation is about to happen! // Optionally return a context containing data to use when for example rolling back return { id: 1 } }, onError: (error, variables, context) => { // An error happened! console.log(`rolling back optimistic update with id ${context.id}`) }, onSuccess: (data, variables, context) => { // Boom baby! }, onSettled: (data, error, variables, context) => { // Error or success... doesn't matter! }, }) useMutation({ mutationFn: addTodo, onMutate: (variables) => { // A mutation is about to happen! // Optionally return a context containing data to use when for example rolling back return { id: 1 } }, onError: (error, variables, context) => { // An error happened! console.log(`rolling back optimistic update with id ${context.id}`) }, onSuccess: (data, variables, context) => { // Boom baby! }, onSettled: (data, error, variables, context) => { // Error or success... doesn't matter! }, }) When returning a promise in any of the callback functions it will first be awaited before the next callback is called: tsx useMutation({ mutationFn: addTodo, onSuccess: async () => { console.log("I'm first!") }, onSettled: async () => { console.log("I'm second!") }, }) useMutation({ mutationFn: addTodo, onSuccess: async () => { console.log("I'm first!") }, onSettled: async () => { console.log("I'm second!") }, }) You might find that you want to **trigger additional callbacks** beyond the ones defined on useMutation when calling mutate. This can be used to trigger component-specific side effects. To do that, you can provide any of the same callback options to the mutate function after your mutation variable. Supported options include: onSuccess, onError and onSettled. Please keep in mind that those additional callbacks won't run if your component unmounts _before_ the mutation finishes. tsx useMutation({ mutationFn: addTodo, onSuccess: (data, variables, context) => { // I will fire first }, onError: (error, variables, context) => { // I will fire first }, onSettled: (data, error, variables, context) => { // I will fire first }, }) mutate(todo, { onSuccess: (data, variables, context) => { // I will fire second! }, onError: (error, variables, context) => { // I will fire second! }, onSettled: (data, error, variables, context) => { // I will fire second! }, }) useMutation({ mutationFn: addTodo, onSuccess: (data, variables, context) => { // I will fire first }, onError: (error, variables, context) => { // I will fire first }, onSettled: (data, error, variables, context) => { // I will fire first }, }) mutate(todo, { onSuccess: (data, variables, context) => { // I will fire second! }, onError: (error, variables, context) => { // I will fire second! }, onSettled: (data, error, variables, context) => { // I will fire second! }, }) ### Consecutive mutations There is a slight difference in handling onSuccess, onError and onSettled callbacks when it comes to consecutive mutations. When passed to the mutate function, they will be fired up only _once_ and only if the component is still mounted. This is due to the fact that mutation observer is removed and resubscribed every time when the mutate function is called. On the contrary, useMutation handlers execute for each mutate call. > Be aware that most likely, mutationFn passed to useMutation is asynchronous. In that case, the order in which mutations are fulfilled may differ from the order of mutate function calls. tsx useMutation({ mutationFn: addTodo, onSuccess: (data, variables, context) => { // Will be called 3 times }, }) const todos = ['Todo 1', 'Todo 2', 'Todo 3'] todos.forEach((todo) => { mutate(todo, { onSuccess: (data, variables, context) => { // Will execute only once, for the last mutation (Todo 3), // regardless which mutation resolves first }, }) }) useMutation({ mutationFn: addTodo, onSuccess: (data, variables, context) => { // Will be called 3 times }, }) const todos = ['Todo 1', 'Todo 2', 'Todo 3'] todos.forEach((todo) => { mutate(todo, { onSuccess: (data, variables, context) => { // Will execute only once, for the last mutation (Todo 3), // regardless which mutation resolves first }, }) }) ## Promises Use mutateAsync instead of mutate to get a promise which will resolve on success or throw on an error. This can for example be used to compose side effects. tsx const mutation = useMutation({ mutationFn: addTodo }) try { const todo = await mutation.mutateAsync(todo) console.log(todo) } catch (error) { console.error(error) } finally { console.log('done') } const mutation = useMutation({ mutationFn: addTodo }) try { const todo = await mutation.mutateAsync(todo) console.log(todo) } catch (error) { console.error(error) } finally { console.log('done') } ## Retry By default, TanStack Query will not retry a mutation on error, but it is possible with the retry option: tsx const mutation = useMutation({ mutationFn: addTodo, retry: 3, }) const mutation = useMutation({ mutationFn: addTodo, retry: 3, }) If mutations fail because the device is offline, they will be retried in the same order when the device reconnects. ## Persist mutations Mutations can be persisted to storage if needed and resumed at a later point. This can be done with the hydration functions: tsx const queryClient = new QueryClient() // Define the "addTodo" mutation queryClient.setMutationDefaults(['addTodo'], { mutationFn: addTodo, onMutate: async (variables) => { // Cancel current queries for the todos list await queryClient.cancelQueries({ queryKey: ['todos'] }) // Create optimistic todo const optimisticTodo = { id: uuid(), title: variables.title } // Add optimistic todo to todos list queryClient.setQueryData(['todos'], (old) => [...old, optimisticTodo]) // Return context with the optimistic todo return { optimisticTodo } }, onSuccess: (result, variables, context) => { // Replace optimistic todo in the todos list with the result queryClient.setQueryData(['todos'], (old) => old.map((todo) => todo.id === context.optimisticTodo.id ? result : todo, ), ) }, onError: (error, variables, context) => { // Remove optimistic todo from the todos list queryClient.setQueryData(['todos'], (old) => old.filter((todo) => todo.id !== context.optimisticTodo.id), ) }, retry: 3, }) // Start mutation in some component: const mutation = useMutation({ mutationKey: ['addTodo'] }) mutation.mutate({ title: 'title' }) // If the mutation has been paused because the device is for example offline, // Then the paused mutation can be dehydrated when the application quits: const state = dehydrate(queryClient) // The mutation can then be hydrated again when the application is started: hydrate(queryClient, state) // Resume the paused mutations: queryClient.resumePausedMutations() const queryClient = new QueryClient() // Define the "addTodo" mutation queryClient.setMutationDefaults(['addTodo'], { mutationFn: addTodo, onMutate: async (variables) => { // Cancel current queries for the todos list await queryClient.cancelQueries({ queryKey: ['todos'] }) // Create optimistic todo const optimisticTodo = { id: uuid(), title: variables.title } // Add optimistic todo to todos list queryClient.setQueryData(['todos'], (old) => [...old, optimisticTodo]) // Return context with the optimistic todo return { optimisticTodo } }, onSuccess: (result, variables, context) => { // Replace optimistic todo in the todos list with the result queryClient.setQueryData(['todos'], (old) => old.map((todo) => todo.id === context.optimisticTodo.id ? result : todo, ), ) }, onError: (error, variables, context) => { // Remove optimistic todo from the todos list queryClient.setQueryData(['todos'], (old) => old.filter((todo) => todo.id !== context.optimisticTodo.id), ) }, retry: 3, }) // Start mutation in some component: const mutation = useMutation({ mutationKey: ['addTodo'] }) mutation.mutate({ title: 'title' }) // If the mutation has been paused because the device is for example offline, // Then the paused mutation can be dehydrated when the application quits: const state = dehydrate(queryClient) // The mutation can then be hydrated again when the application is started: hydrate(queryClient, state) // Resume the paused mutations: queryClient.resumePausedMutations() ### Persisting Offline mutations If you persist offline mutations with the persistQueryClient plugin, mutations cannot be resumed when the page is reloaded unless you provide a default mutation function. This is a technical limitation. When persisting to an external storage, only the state of mutations is persisted, as functions cannot be serialized. After hydration, the component that triggers the mutation might not be mounted, so calling resumePausedMutations might yield an error: No mutationFn found. tsx const persister = createSyncStoragePersister({ storage: window.localStorage, }) const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 24 hours }, }, }) // we need a default mutation function so that paused mutations can resume after a page reload queryClient.setMutationDefaults(['todos'], { mutationFn: ({ id, data }) => { return api.updateTodo(id, data) }, }) export default function App() { return ( <PersistQueryClientProvider client={queryClient} persistOptions={{ persister }} onSuccess={() => { // resume mutations after initial restore from localStorage was successful queryClient.resumePausedMutations() }} > <RestOfTheApp /> </PersistQueryClientProvider> ) } const persister = createSyncStoragePersister({ storage: window.localStorage, }) const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 24 hours }, }, }) // we need a default mutation function so that paused mutations can resume after a page reload queryClient.setMutationDefaults(['todos'], { mutationFn: ({ id, data }) => { return api.updateTodo(id, data) }, }) export default function App() { return ( <PersistQueryClientProvider client={queryClient} persistOptions={{ persister }} onSuccess={() => { // resume mutations after initial restore from localStorage was successful queryClient.resumePausedMutations() }} > <RestOfTheApp /> </PersistQueryClientProvider> ) } We also have an extensive offline example that covers both queries and mutations. ## Mutation Scopes Per default, all mutations run in parallel - even if you invoke .mutate() of the same mutation multiple times. Mutations can be given a scope with an id to avoid that. All mutations with the same scope.id will run in serial, which means when they are triggered, they will start in isPaused: true state if there is already a mutation for that scope in progress. They will be put into a queue and will automatically resume once their time in the queue has come. tsx const mutation = useMutation({ mutationFn: addTodo, scope: { id: 'todo', }, }) const mutation = useMutation({ mutationFn: addTodo, scope: { id: 'todo', }, }) ## Further reading For more information about mutations, have a look at #12: Mastering Mutations in React Query from the Community Resources. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation Waiting for queries to become stale before they are fetched again doesn't always work, especially when you know for a fact that a query's data is out of date because of something the user has done. For that purpose, the QueryClient has an invalidateQueries method that lets you intelligently mark queries as stale and potentially refetch them too! tsx // Invalidate every query in the cache queryClient.invalidateQueries() // Invalidate every query with a key that starts with `todos` queryClient.invalidateQueries({ queryKey: ['todos'] }) // Invalidate every query in the cache queryClient.invalidateQueries() // Invalidate every query with a key that starts with `todos` queryClient.invalidateQueries({ queryKey: ['todos'] }) > Note: Where other libraries that use normalized caches would attempt to update local queries with the new data either imperatively or via schema inference, TanStack Query gives you the tools to avoid the manual labor that comes with maintaining normalized caches and instead prescribes **targeted invalidation, background-refetching and ultimately atomic updates**. When a query is invalidated with invalidateQueries, two things happen: * It is marked as stale. This stale state overrides any staleTime configurations being used in useQuery or related hooks * If the query is currently being rendered via useQuery or related hooks, it will also be refetched in the background ## Query Matching with invalidateQueries When using APIs like invalidateQueries and removeQueries (and others that support partial query matching), you can match multiple queries by their prefix, or get really specific and match an exact query. For information on the types of filters you can use, please see Query Filters. In this example, we can use the todos prefix to invalidate any queries that start with todos in their query key: tsx import { useQuery, useQueryClient } from '@tanstack/react-query' // Get QueryClient from the context const queryClient = useQueryClient() queryClient.invalidateQueries({ queryKey: ['todos'] }) // Both queries below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) const todoListQuery = useQuery({ queryKey: ['todos', { page: 1 }], queryFn: fetchTodoList, }) import { useQuery, useQueryClient } from '@tanstack/react-query' // Get QueryClient from the context const queryClient = useQueryClient() queryClient.invalidateQueries({ queryKey: ['todos'] }) // Both queries below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) const todoListQuery = useQuery({ queryKey: ['todos', { page: 1 }], queryFn: fetchTodoList, }) You can even invalidate queries with specific variables by passing a more specific query key to the invalidateQueries method: tsx queryClient.invalidateQueries({ queryKey: ['todos', { type: 'done' }], }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { type: 'done' }], queryFn: fetchTodoList, }) // However, the following query below will NOT be invalidated const todoListQuery = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) queryClient.invalidateQueries({ queryKey: ['todos', { type: 'done' }], }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { type: 'done' }], queryFn: fetchTodoList, }) // However, the following query below will NOT be invalidated const todoListQuery = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) The invalidateQueries API is very flexible, so even if you want to **only** invalidate todos queries that don't have any more variables or subkeys, you can pass an exact: true option to the invalidateQueries method: tsx queryClient.invalidateQueries({ queryKey: ['todos'], exact: true, }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) // However, the following query below will NOT be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { type: 'done' }], queryFn: fetchTodoList, }) queryClient.invalidateQueries({ queryKey: ['todos'], exact: true, }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) // However, the following query below will NOT be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { type: 'done' }], queryFn: fetchTodoList, }) If you find yourself wanting **even more** granularity, you can pass a predicate function to the invalidateQueries method. This function will receive each Query instance from the query cache and allow you to return true or false for whether you want to invalidate that query: tsx queryClient.invalidateQueries({ predicate: (query) => query.queryKey[0] === 'todos' && query.queryKey[1]?.version >= 10, }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { version: 20 }], queryFn: fetchTodoList, }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { version: 10 }], queryFn: fetchTodoList, }) // However, the following query below will NOT be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { version: 5 }], queryFn: fetchTodoList, }) queryClient.invalidateQueries({ predicate: (query) => query.queryKey[0] === 'todos' && query.queryKey[1]?.version >= 10, }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { version: 20 }], queryFn: fetchTodoList, }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { version: 10 }], queryFn: fetchTodoList, }) // However, the following query below will NOT be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { version: 5 }], queryFn: fetchTodoList, }) --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/invalidations-from-mutations Invalidating queries is only half the battle. Knowing **when** to invalidate them is the other half. Usually when a mutation in your app succeeds, it's VERY likely that there are related queries in your application that need to be invalidated and possibly refetched to account for the new changes from your mutation. For example, assume we have a mutation to post a new todo: tsx const mutation = useMutation({ mutationFn: postTodo }) const mutation = useMutation({ mutationFn: postTodo }) When a successful postTodo mutation happens, we likely want all todos queries to get invalidated and possibly refetched to show the new todo item. To do this, you can use useMutation's onSuccess options and the client's invalidateQueries function: tsx import { useMutation, useQueryClient } from '@tanstack/react-query' const queryClient = useQueryClient() // When this mutation succeeds, invalidate any queries with the `todos` or `reminders` query key const mutation = useMutation({ mutationFn: addTodo, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }) queryClient.invalidateQueries({ queryKey: ['reminders'] }) }, }) import { useMutation, useQueryClient } from '@tanstack/react-query' const queryClient = useQueryClient() // When this mutation succeeds, invalidate any queries with the `todos` or `reminders` query key const mutation = useMutation({ mutationFn: addTodo, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }) queryClient.invalidateQueries({ queryKey: ['reminders'] }) }, }) You can wire up your invalidations to happen using any of the callbacks available in the useMutation hook --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/updates-from-mutation-responses When dealing with mutations that **update** objects on the server, it's common for the new object to be automatically returned in the response of the mutation. Instead of refetching any queries for that item and wasting a network call for data we already have, we can take advantage of the object returned by the mutation function and update the existing query with the new data immediately using the Query Client's setQueryData method: tsx const queryClient = useQueryClient() const mutation = useMutation({ mutationFn: editTodo, onSuccess: (data) => { queryClient.setQueryData(['todo', { id: 5 }], data) }, }) mutation.mutate({ id: 5, name: 'Do the laundry', }) // The query below will be updated with the response from the // successful mutation const { status, data, error } = useQuery({ queryKey: ['todo', { id: 5 }], queryFn: fetchTodoById, }) const queryClient = useQueryClient() const mutation = useMutation({ mutationFn: editTodo, onSuccess: (data) => { queryClient.setQueryData(['todo', { id: 5 }], data) }, }) mutation.mutate({ id: 5, name: 'Do the laundry', }) // The query below will be updated with the response from the // successful mutation const { status, data, error } = useQuery({ queryKey: ['todo', { id: 5 }], queryFn: fetchTodoById, }) You might want to tie the onSuccess logic into a reusable mutation, for that you can create a custom hook like this: tsx const useMutateTodo = () => { const queryClient = useQueryClient() return useMutation({ mutationFn: editTodo, // Notice the second argument is the variables object that the `mutate` function receives onSuccess: (data, variables) => { queryClient.setQueryData(['todo', { id: variables.id }], data) }, }) } const useMutateTodo = () => { const queryClient = useQueryClient() return useMutation({ mutationFn: editTodo, // Notice the second argument is the variables object that the `mutate` function receives onSuccess: (data, variables) => { queryClient.setQueryData(['todo', { id: variables.id }], data) }, }) } ## Immutability Updates via setQueryData must be performed in an _immutable_ way. **DO NOT** attempt to write directly to the cache by mutating data (that you retrieved from the cache) in place. It might work at first but can lead to subtle bugs along the way. tsx queryClient.setQueryData(['posts', { id }], (oldData) => { if (oldData) { // ❌ do not try this oldData.title = 'my new post title' } return oldData }) queryClient.setQueryData( ['posts', { id }], // ✅ this is the way (oldData) => oldData ? { ...oldData, title: 'my new post title', } : oldData, ) queryClient.setQueryData(['posts', { id }], (oldData) => { if (oldData) { // ❌ do not try this oldData.title = 'my new post title' } return oldData }) queryClient.setQueryData( ['posts', { id }], // ✅ this is the way (oldData) => oldData ? { ...oldData, title: 'my new post title', } : oldData, ) --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates React Query provides two ways to optimistically update your UI before a mutation has completed. You can either use the onMutate option to update your cache directly, or leverage the returned variables to update your UI from the useMutation result. ## Via the UI This is the simpler variant, as it doesn't interact with the cache directly. tsx const addTodoMutation = useMutation({ mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }), // make sure to _return_ the Promise from the query invalidation // so that the mutation stays in `pending` state until the refetch is finished onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), }) const { isPending, submittedAt, variables, mutate, isError } = addTodoMutation const addTodoMutation = useMutation({ mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }), // make sure to _return_ the Promise from the query invalidation // so that the mutation stays in `pending` state until the refetch is finished onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), }) const { isPending, submittedAt, variables, mutate, isError } = addTodoMutation you will then have access to addTodoMutation.variables, which contain the added todo. In your UI list, where the query is rendered, you can append another item to the list while the mutation isPending: tsx <ul> {todoQuery.items.map((todo) => ( <li key={todo.id}>{todo.text}</li> ))} {isPending && <li style={{ opacity: 0.5 }}>{variables}</li>} </ul> <ul> {todoQuery.items.map((todo) => ( <li key={todo.id}>{todo.text}</li> ))} {isPending && <li style={{ opacity: 0.5 }}>{variables}</li>} </ul> We're rendering a temporary item with a different opacity as long as the mutation is pending. Once it completes, the item will automatically no longer be rendered. Given that the refetch succeeded, we should see the item as a "normal item" in our list. If the mutation errors, the item will also disappear. But we could continue to show it, if we want, by checking for the isError state of the mutation. variables are _not_ cleared when the mutation errors, so we can still access them, maybe even show a retry button: tsx { isError && ( <li style={{ color: 'red' }}> {variables} <button onClick={() => mutate(variables)}>Retry</button> </li> ) } { isError && ( <li style={{ color: 'red' }}> {variables} <button onClick={() => mutate(variables)}>Retry</button> </li> ) } ### If the mutation and the query don't live in the same component This approach works very well if the mutation and the query live in the same component, However, you also get access to all mutations in other components via the dedicated useMutationState hook. It is best combined with a mutationKey: tsx // somewhere in your app const { mutate } = useMutation({ mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }), onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), mutationKey: ['addTodo'], }) // access variables somewhere else const variables = useMutationState<string>({ filters: { mutationKey: ['addTodo'], status: 'pending' }, select: (mutation) => mutation.state.variables, }) // somewhere in your app const { mutate } = useMutation({ mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }), onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), mutationKey: ['addTodo'], }) // access variables somewhere else const variables = useMutationState<string>({ filters: { mutationKey: ['addTodo'], status: 'pending' }, select: (mutation) => mutation.state.variables, }) variables will be an Array, because there might be multiple mutations running at the same time. If we need a unique key for the items, we can also select mutation.state.submittedAt. This will even make displaying concurrent optimistic updates a breeze. ## Via the cache When you optimistically update your state before performing a mutation, there is a chance that the mutation will fail. In most of these failure cases, you can just trigger a refetch for your optimistic queries to revert them to their true server state. In some circumstances though, refetching may not work correctly and the mutation error could represent some type of server issue that won't make it possible to refetch. In this event, you can instead choose to roll back your update. To do this, useMutation's onMutate handler option allows you to return a value that will later be passed to both onError and onSettled handlers as the last argument. In most cases, it is most useful to pass a rollback function. ### Updating a list of todos when adding a new todo tsx const queryClient = useQueryClient() useMutation({ mutationFn: updateTodo, // When mutate is called: onMutate: async (newTodo) => { // Cancel any outgoing refetches // (so they don't overwrite our optimistic update) await queryClient.cancelQueries({ queryKey: ['todos'] }) // Snapshot the previous value const previousTodos = queryClient.getQueryData(['todos']) // Optimistically update to the new value queryClient.setQueryData(['todos'], (old) => [...old, newTodo]) // Return a context object with the snapshotted value return { previousTodos } }, // If the mutation fails, // use the context returned from onMutate to roll back onError: (err, newTodo, context) => { queryClient.setQueryData(['todos'], context.previousTodos) }, // Always refetch after error or success: onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), }) const queryClient = useQueryClient() useMutation({ mutationFn: updateTodo, // When mutate is called: onMutate: async (newTodo) => { // Cancel any outgoing refetches // (so they don't overwrite our optimistic update) await queryClient.cancelQueries({ queryKey: ['todos'] }) // Snapshot the previous value const previousTodos = queryClient.getQueryData(['todos']) // Optimistically update to the new value queryClient.setQueryData(['todos'], (old) => [...old, newTodo]) // Return a context object with the snapshotted value return { previousTodos } }, // If the mutation fails, // use the context returned from onMutate to roll back onError: (err, newTodo, context) => { queryClient.setQueryData(['todos'], context.previousTodos) }, // Always refetch after error or success: onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), }) ### Updating a single todo tsx useMutation({ mutationFn: updateTodo, // When mutate is called: onMutate: async (newTodo) => { // Cancel any outgoing refetches // (so they don't overwrite our optimistic update) await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] }) // Snapshot the previous value const previousTodo = queryClient.getQueryData(['todos', newTodo.id]) // Optimistically update to the new value queryClient.setQueryData(['todos', newTodo.id], newTodo) // Return a context with the previous and new todo return { previousTodo, newTodo } }, // If the mutation fails, use the context we returned above onError: (err, newTodo, context) => { queryClient.setQueryData( ['todos', context.newTodo.id], context.previousTodo, ) }, // Always refetch after error or success: onSettled: (newTodo) => queryClient.invalidateQueries({ queryKey: ['todos', newTodo.id] }), }) useMutation({ mutationFn: updateTodo, // When mutate is called: onMutate: async (newTodo) => { // Cancel any outgoing refetches // (so they don't overwrite our optimistic update) await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] }) // Snapshot the previous value const previousTodo = queryClient.getQueryData(['todos', newTodo.id]) // Optimistically update to the new value queryClient.setQueryData(['todos', newTodo.id], newTodo) // Return a context with the previous and new todo return { previousTodo, newTodo } }, // If the mutation fails, use the context we returned above onError: (err, newTodo, context) => { queryClient.setQueryData( ['todos', context.newTodo.id], context.previousTodo, ) }, // Always refetch after error or success: onSettled: (newTodo) => queryClient.invalidateQueries({ queryKey: ['todos', newTodo.id] }), }) You can also use the onSettled function in place of the separate onError and onSuccess handlers if you wish: tsx useMutation({ mutationFn: updateTodo, // ... onSettled: async (newTodo, error, variables, context) => { if (error) { // do something } }, }) useMutation({ mutationFn: updateTodo, // ... onSettled: async (newTodo, error, variables, context) => { if (error) { // do something } }, }) ## When to use what If you only have one place where the optimistic result should be shown, using variables and updating the UI directly is the approach that requires less code and is generally easier to reason about. For example, you don't need to handle rollbacks at all. However, if you have multiple places on the screen that would require to know about the update, manipulating the cache directly will take care of this for you automatically. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/query-cancellation TanStack Query provides each query function with an AbortSignal instance. When a query becomes out-of-date or inactive, this signal will become aborted. This means that all queries are cancellable, and you can respond to the cancellation inside your query function if desired. The best part about this is that it allows you to continue to use normal async/await syntax while getting all the benefits of automatic cancellation. The AbortController API is available in most runtime environments, but if your runtime environment does not support it, you will need to provide a polyfill. There are several available. ## Default behavior By default, queries that unmount or become unused before their promises are resolved are _not_ cancelled. This means that after the promise has resolved, the resulting data will be available in the cache. This is helpful if you've started receiving a query, but then unmount the component before it finishes. If you mount the component again and the query has not been garbage collected yet, data will be available. However, if you consume the AbortSignal, the Promise will be cancelled (e.g. aborting the fetch) and therefore, also the Query must be cancelled. Cancelling the query will result in its state being _reverted_ to its previous state. ## Using fetch tsx const query = useQuery({ queryKey: ['todos'], queryFn: async ({ signal }) => { const todosResponse = await fetch('/todos', { // Pass the signal to one fetch signal, }) const todos = await todosResponse.json() const todoDetails = todos.map(async ({ details }) => { const response = await fetch(details, { // Or pass it to several signal, }) return response.json() }) return Promise.all(todoDetails) }, }) const query = useQuery({ queryKey: ['todos'], queryFn: async ({ signal }) => { const todosResponse = await fetch('/todos', { // Pass the signal to one fetch signal, }) const todos = await todosResponse.json() const todoDetails = todos.map(async ({ details }) => { const response = await fetch(details, { // Or pass it to several signal, }) return response.json() }) return Promise.all(todoDetails) }, }) ## Using axios v0.22.0+ tsx import axios from 'axios' const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => axios.get('/todos', { // Pass the signal to `axios` signal, }), }) import axios from 'axios' const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => axios.get('/todos', { // Pass the signal to `axios` signal, }), }) ### Using axios with version lower than v0.22.0 tsx import axios from 'axios' const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { // Create a new CancelToken source for this request const CancelToken = axios.CancelToken const source = CancelToken.source() const promise = axios.get('/todos', { // Pass the source token to your request cancelToken: source.token, }) // Cancel the request if TanStack Query signals to abort signal?.addEventListener('abort', () => { source.cancel('Query was cancelled by TanStack Query') }) return promise }, }) import axios from 'axios' const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { // Create a new CancelToken source for this request const CancelToken = axios.CancelToken const source = CancelToken.source() const promise = axios.get('/todos', { // Pass the source token to your request cancelToken: source.token, }) // Cancel the request if TanStack Query signals to abort signal?.addEventListener('abort', () => { source.cancel('Query was cancelled by TanStack Query') }) return promise }, }) ## Using XMLHttpRequest tsx const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { return new Promise((resolve, reject) => { var oReq = new XMLHttpRequest() oReq.addEventListener('load', () => { resolve(JSON.parse(oReq.responseText)) }) signal?.addEventListener('abort', () => { oReq.abort() reject() }) oReq.open('GET', '/todos') oReq.send() }) }, }) const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { return new Promise((resolve, reject) => { var oReq = new XMLHttpRequest() oReq.addEventListener('load', () => { resolve(JSON.parse(oReq.responseText)) }) signal?.addEventListener('abort', () => { oReq.abort() reject() }) oReq.open('GET', '/todos') oReq.send() }) }, }) ## Using graphql-request An AbortSignal can be set in the client request method. tsx const client = new GraphQLClient(endpoint) const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { client.request({ document: query, signal }) }, }) const client = new GraphQLClient(endpoint) const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { client.request({ document: query, signal }) }, }) ## Using graphql-request with version lower than v4.0.0 An AbortSignal can be set in the GraphQLClient constructor. tsx const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { const client = new GraphQLClient(endpoint, { signal, }) return client.request(query, variables) }, }) const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { const client = new GraphQLClient(endpoint, { signal, }) return client.request(query, variables) }, }) ## Manual Cancellation You might want to cancel a query manually. For example, if the request takes a long time to finish, you can allow the user to click a cancel button to stop the request. To do this, you just need to call queryClient.cancelQueries({ queryKey }), which will cancel the query and revert it back to its previous state. If you have consumed the signal passed to the query function, TanStack Query will additionally also cancel the Promise. tsx const query = useQuery({ queryKey: ['todos'], queryFn: async ({ signal }) => { const resp = await fetch('/todos', { signal }) return resp.json() }, }) const queryClient = useQueryClient() return ( <button onClick={(e) => { e.preventDefault() queryClient.cancelQueries({ queryKey: ['todos'] }) }} > Cancel </button> ) const query = useQuery({ queryKey: ['todos'], queryFn: async ({ signal }) => { const resp = await fetch('/todos', { signal }) return resp.json() }, }) const queryClient = useQueryClient() return ( <button onClick={(e) => { e.preventDefault() queryClient.cancelQueries({ queryKey: ['todos'] }) }} > Cancel </button> ) ## Limitations Cancellation does not work when working with Suspense hooks: useSuspenseQuery, useSuspenseQueries and useSuspenseInfiniteQuery. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/scroll-restoration Traditionally, when you navigate to a previously visited page on a web browser, you would find that the page would be scrolled to the exact position where you were before you navigated away from that page. This is called **scroll restoration** and has been in a bit of a regression since web applications have started moving towards client side data fetching. With TanStack Query however, that's no longer the case. Out of the box, "scroll restoration" for all queries (including paginated and infinite queries) Just Works™️ in TanStack Query. The reason for this is that query results are cached and able to be retrieved synchronously when a query is rendered. As long as your queries are being cached long enough (the default time is 5 minutes) and have not been garbage collected, scroll restoration will work out of the box all the time. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/filters Some methods within TanStack Query accept a QueryFilters or MutationFilters object. ## Query Filters A query filter is an object with certain conditions to match a query with: tsx // Cancel all queries await queryClient.cancelQueries() // Remove all inactive queries that begin with `posts` in the key queryClient.removeQueries({ queryKey: ['posts'], type: 'inactive' }) // Refetch all active queries await queryClient.refetchQueries({ type: 'active' }) // Refetch all active queries that begin with `posts` in the key await queryClient.refetchQueries({ queryKey: ['posts'], type: 'active' }) // Cancel all queries await queryClient.cancelQueries() // Remove all inactive queries that begin with `posts` in the key queryClient.removeQueries({ queryKey: ['posts'], type: 'inactive' }) // Refetch all active queries await queryClient.refetchQueries({ type: 'active' }) // Refetch all active queries that begin with `posts` in the key await queryClient.refetchQueries({ queryKey: ['posts'], type: 'active' }) A query filter object supports the following properties: * queryKey?: QueryKey * Set this property to define a query key to match on. * exact?: boolean * If you don't want to search queries inclusively by query key, you can pass the exact: true option to return only the query with the exact query key you have passed. * type?: 'active' | 'inactive' | 'all' * Defaults to all * When set to active it will match active queries. * When set to inactive it will match inactive queries. * stale?: boolean * When set to true it will match stale queries. * When set to false it will match fresh queries. * fetchStatus?: FetchStatus * When set to fetching it will match queries that are currently fetching. * When set to paused it will match queries that wanted to fetch, but have been paused. * When set to idle it will match queries that are not fetching. * predicate?: (query: Query) => boolean * This predicate function will be used as a final filter on all matching queries. If no other filters are specified, this function will be evaluated against every query in the cache. ## Mutation Filters A mutation filter is an object with certain conditions to match a mutation with: tsx // Get the number of all fetching mutations await queryClient.isMutating() // Filter mutations by mutationKey await queryClient.isMutating({ mutationKey: ['post'] }) // Filter mutations using a predicate function await queryClient.isMutating({ predicate: (mutation) => mutation.state.variables?.id === 1, }) // Get the number of all fetching mutations await queryClient.isMutating() // Filter mutations by mutationKey await queryClient.isMutating({ mutationKey: ['post'] }) // Filter mutations using a predicate function await queryClient.isMutating({ predicate: (mutation) => mutation.state.variables?.id === 1, }) A mutation filter object supports the following properties: * mutationKey?: MutationKey * Set this property to define a mutation key to match on. * exact?: boolean * If you don't want to search mutations inclusively by mutation key, you can pass the exact: true option to return only the mutation with the exact mutation key you have passed. * status?: MutationStatus * Allows for filtering mutations according to their status. * predicate?: (mutation: Mutation) => boolean * This predicate function will be used as a final filter on all matching mutations. If no other filters are specified, this function will be evaluated against every mutation in the cache. ## Utils ### matchQuery tsx const isMatching = matchQuery(filters, query) const isMatching = matchQuery(filters, query) Returns a boolean that indicates whether a query matches the provided set of query filters. ### matchMutation tsx const isMatching = matchMutation(filters, mutation) const isMatching = matchMutation(filters, mutation) Returns a boolean that indicates whether a mutation matches the provided set of mutation filters. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/request-waterfalls Application performance is a broad and complex area and while React Query can't make your APIs faster, there are still things to be mindful about in how you use React Query to ensure the best performance. The biggest performance footgun when using React Query, or indeed any data fetching library that lets you fetch data inside of components, is request waterfalls. The rest of this page will explain what they are, how you can spot them and how you can restructure your application or APIs to avoid them. The Prefetching & Router Integration guide builds on this and teaches you how to prefetch data ahead of time when it's not possible or feasible to restructure your application or APIs. The Server Rendering & Hydration guide teaches you how to prefetch data on the server and pass that data down to the client so you don't have to fetch it again. The Advanced Server Rendering guide further teaches you how to apply these patterns to Server Components and Streaming Server Rendering. ## What is a Request Waterfall? A request waterfall is what happens when a request for a resource (code, css, images, data) does not start until _after_ another request for a resource has finished. Consider a web page. Before you can load things like the CSS, JS etc, the browser first needs to load the markup. This is a request waterfall. 1. |-> Markup 2. |-> CSS 2. |-> JS 2. |-> Image 1. |-> Markup 2. |-> CSS 2. |-> JS 2. |-> Image If you fetch your CSS inside a JS file, you now have a double waterfall: 1. |-> Markup 2. |-> JS 3. |-> CSS 1. |-> Markup 2. |-> JS 3. |-> CSS If that CSS uses a background image, it's a triple waterfall: 1. |-> Markup 2. |-> JS 3. |-> CSS 4. |-> Image 1. |-> Markup 2. |-> JS 3. |-> CSS 4. |-> Image The best way to spot and analyze your request waterfalls is usually by opening your browsers devtools "Network" tab. Each waterfall represents at least one roundtrip to the server, unless the resource is locally cached (in practice, some of these waterfalls might represent more than one roundtrip because the browser needs to establish a connection which requires some back and forth, but let's ignore that here). Because of this, the negative effects of request waterfalls are highly dependent on the users latency. Consider the example of the triple waterfall, which actually represents 4 server roundtrips. With 250ms latency, which is not uncommon on 3g networks or in bad network conditions, we end up with a total time of 4\*250=1000ms **only counting latency**. If we were able to flatten that to the first example with only 2 roundtrips, we get 500ms instead, possibly loading that background image in half the time! ## Request Waterfalls & React Query Now let's consider React Query. We'll focus on the case without Server Rendering first. Before we can even start making a query, we need to load the JS, so before we can show that data on the screen, we have a double waterfall: 1. |-> Markup 2. |-> JS 3. |-> Query 1. |-> Markup 2. |-> JS 3. |-> Query With this as a basis, let's look at a few different patterns that can lead to Request Waterfalls in React Query, and how to avoid them. * Single Component Waterfalls / Serial Queries * Nested Component Waterfalls * Code Splitting ### Single Component Waterfalls / Serial Queries When a single component first fetches one query, and then another, that's a request waterfall. This can happen when the second query is a Dependent Query, that is, it depends on data from the first query when fetching: tsx // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) While not always feasible, for optimal performance it's better to restructure your API so you can fetch both of these in a single query. In the example above, instead of first fetching getUserByEmail to be able to getProjectsByUser, introducing a new getProjectsByUserEmail query would flatten the waterfall. > Another way to mitigate dependent queries without restructuring your API is to move the waterfall to the server where latency is lower. This is the idea behind Server Components which are covered in the Advanced Server Rendering guide. Another example of serial queries is when you use React Query with Suspense: tsx function App () { // The following queries will execute in serial, causing separate roundtrips to the server: const usersQuery = useSuspenseQuery({ queryKey: ['users'], queryFn: fetchUsers }) const teamsQuery = useSuspenseQuery({ queryKey: ['teams'], queryFn: fetchTeams }) const projectsQuery = useSuspenseQuery({ queryKey: ['projects'], queryFn: fetchProjects }) // Note that since the queries above suspend rendering, no data // gets rendered until all of the queries finished ... } function App () { // The following queries will execute in serial, causing separate roundtrips to the server: const usersQuery = useSuspenseQuery({ queryKey: ['users'], queryFn: fetchUsers }) const teamsQuery = useSuspenseQuery({ queryKey: ['teams'], queryFn: fetchTeams }) const projectsQuery = useSuspenseQuery({ queryKey: ['projects'], queryFn: fetchProjects }) // Note that since the queries above suspend rendering, no data // gets rendered until all of the queries finished ... } Note that with regular useQuery these would happen in parallel. Luckily, this is easy to fix, by always using the hook useSuspenseQueries when you have multiple suspenseful queries in a component. tsx const [usersQuery, teamsQuery, projectsQuery] = useSuspenseQueries({ queries: [ { queryKey: ['users'], queryFn: fetchUsers }, { queryKey: ['teams'], queryFn: fetchTeams }, { queryKey: ['projects'], queryFn: fetchProjects }, ], }) const [usersQuery, teamsQuery, projectsQuery] = useSuspenseQueries({ queries: [ { queryKey: ['users'], queryFn: fetchUsers }, { queryKey: ['teams'], queryFn: fetchTeams }, { queryKey: ['projects'], queryFn: fetchProjects }, ], }) ### Nested Component Waterfalls Nested Component Waterfalls is when both a parent and a child component contains queries, and the parent does not render the child until its query is done. This can happen both with useQuery and useSuspenseQuery. If the child renders conditionally based on the data in the parent, or if the child relies on some part of the result being passed down as a prop from the parent to make its query, we have a _dependent_ nested component waterfall. Let's first look at an example where the child is **not** dependent on the parent. tsx function Article({ id }) { const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) if (isPending) { return 'Loading article...' } return ( <> <ArticleHeader articleData={articleData} /> <ArticleBody articleData={articleData} /> <Comments id={id} /> </> ) } function Comments({ id }) { const { data, isPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) ... } function Article({ id }) { const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) if (isPending) { return 'Loading article...' } return ( <> <ArticleHeader articleData={articleData} /> <ArticleBody articleData={articleData} /> <Comments id={id} /> </> ) } function Comments({ id }) { const { data, isPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) ... } Note that while <Comments> takes a prop id from the parent, that id is already available when the <Article> renders so there is no reason we could not fetch the comments at the same time as the article. In real world applications, the child might be nested far below the parent and these kinds of waterfalls are often trickier to spot and fix, but for our example, one way to flatten the waterfall would be to hoist the comments query to the parent instead: tsx function Article({ id }) { const { data: articleData, isPending: articlePending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) const { data: commentsData, isPending: commentsPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) if (articlePending) { return 'Loading article...' } return ( <> <ArticleHeader articleData={articleData} /> <ArticleBody articleData={articleData} /> {commentsPending ? ( 'Loading comments...' ) : ( <Comments commentsData={commentsData} /> )} </> ) } function Article({ id }) { const { data: articleData, isPending: articlePending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) const { data: commentsData, isPending: commentsPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) if (articlePending) { return 'Loading article...' } return ( <> <ArticleHeader articleData={articleData} /> <ArticleBody articleData={articleData} /> {commentsPending ? ( 'Loading comments...' ) : ( <Comments commentsData={commentsData} /> )} </> ) } The two queries will now fetch in parallel. Note that if you are using suspense, you'd want to combine these two queries into a single useSuspenseQueries instead. Another way to flatten this waterfall would be to prefetch the comments in the <Article> component, or prefetch both of these queries at the router level on page load or page navigation, read more about this in the Prefetching & Router Integration guide. Next, let's look at a _Dependent Nested Component Waterfall_. tsx function Feed() { const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: getFeed, }) if (isPending) { return 'Loading feed...' } return ( <> {data.map((feedItem) => { if (feedItem.type === 'GRAPH') { return <GraphFeedItem key={feedItem.id} feedItem={feedItem} /> } return <StandardFeedItem key={feedItem.id} feedItem={feedItem} /> })} </> ) } function GraphFeedItem({ feedItem }) { const { data, isPending } = useQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) ... } function Feed() { const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: getFeed, }) if (isPending) { return 'Loading feed...' } return ( <> {data.map((feedItem) => { if (feedItem.type === 'GRAPH') { return <GraphFeedItem key={feedItem.id} feedItem={feedItem} /> } return <StandardFeedItem key={feedItem.id} feedItem={feedItem} /> })} </> ) } function GraphFeedItem({ feedItem }) { const { data, isPending } = useQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) ... } The second query getGraphDataById is dependent on its parent in two different ways. First of all, it doesn't ever happen unless the feedItem is a graph, and second, it needs an id from the parent. 1. |> getFeed() 2. |> getGraphDataById() 1. |> getFeed() 2. |> getGraphDataById() In this example, we can't trivially flatten the waterfall by just hoisting the query to the parent, or even adding prefetching. Just like the dependent query example at the beginning of this guide, one option is to refactor our API to include the graph data in the getFeed query. Another more advanced solution is to leverage Server Components to move the waterfall to the server where latency is lower (read more about this in the Advanced Server Rendering guide) but note that this can be a very big architectural change. You can have good performance even with a few query waterfalls here and there, just know they are a common performance concern and be mindful about them. An especially insidious version is when Code Splitting is involved, let's take a look at this next. ### Code Splitting Splitting an applications JS-code into smaller chunks and only loading the necessary parts is usually a critical step in achieving good performance. It does have a downside however, in that it often introduces request waterfalls. When that code split code also has a query inside it, this problem is worsened further. Consider this a slightly modified version of the Feed example. tsx // This lazy loads the GraphFeedItem component, meaning // it wont start loading until something renders it const GraphFeedItem = React.lazy(() => import('./GraphFeedItem')) function Feed() { const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: getFeed, }) if (isPending) { return 'Loading feed...' } return ( <> {data.map((feedItem) => { if (feedItem.type === 'GRAPH') { return <GraphFeedItem key={feedItem.id} feedItem={feedItem} /> } return <StandardFeedItem key={feedItem.id} feedItem={feedItem} /> })} </> ) } // GraphFeedItem.tsx function GraphFeedItem({ feedItem }) { const { data, isPending } = useQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) ... } // This lazy loads the GraphFeedItem component, meaning // it wont start loading until something renders it const GraphFeedItem = React.lazy(() => import('./GraphFeedItem')) function Feed() { const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: getFeed, }) if (isPending) { return 'Loading feed...' } return ( <> {data.map((feedItem) => { if (feedItem.type === 'GRAPH') { return <GraphFeedItem key={feedItem.id} feedItem={feedItem} /> } return <StandardFeedItem key={feedItem.id} feedItem={feedItem} /> })} </> ) } // GraphFeedItem.tsx function GraphFeedItem({ feedItem }) { const { data, isPending } = useQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) ... } This example has a double waterfall, looking like this: 1. |> getFeed() 2. |> JS for <GraphFeedItem> 3. |> getGraphDataById() 1. |> getFeed() 2. |> JS for <GraphFeedItem> 3. |> getGraphDataById() But that's just looking at the code from the example, if we consider what the first page load of this page looks like, we actually have to complete 5 round trips to the server before we can render the graph! 1. |> Markup 2. |> JS for <Feed> 3. |> getFeed() 4. |> JS for <GraphFeedItem> 5. |> getGraphDataById() 1. |> Markup 2. |> JS for <Feed> 3. |> getFeed() 4. |> JS for <GraphFeedItem> 5. |> getGraphDataById() Note that this looks a bit different when server rendering, we will explore that further in the Server Rendering & Hydration guide. Also note that it's not uncommon for the route that contains <Feed> to also be code split, which could add yet another hop. In the code split case, it might actually help to hoist the getGraphDataById query to the <Feed> component and make it conditional, or add a conditional prefetch. That query could then be fetched in parallel with the code, turning the example part into this: 1. |> getFeed() 2. |> getGraphDataById() 2. |> JS for <GraphFeedItem> 1. |> getFeed() 2. |> getGraphDataById() 2. |> JS for <GraphFeedItem> This is very much a tradeoff however. You are now including the data fetching code for getGraphDataById in the same bundle as <Feed>, so evaluate what is best for your case. Read more about how to do this in the Prefetching & Router Integration guide. > The tradeoff between: > > * Include all data fetching code in the main bundle, even if we seldom use it > * Put the data fetching code in the code split bundle, but with a request waterfall > > is not great and has been one of the motivations for Server Components. With Server Components, it's possible to avoid both, read more about how this applies to React Query in the Advanced Server Rendering guide. ## Summary and takeaways Request Waterfalls are a very common and complex performance concern with many tradeoffs. There are many ways to accidentally introduce them into your application: * Adding a query to a child, not realizing a parent already has a query * Adding a query to a parent, not realizing a child already has a query * Moving a component with descendants that has a query to a new parent with an ancestor that has a query * Etc.. Because of this accidental complexity, it pays off to be mindful of waterfalls and regularly examine your application looking for them (a good way is to examine the Network tab every now and then!). You don't necessarily have to flatten them all to have good performance, but keep an eye out for the high impact ones. In the next guide, we'll look at more ways to flatten waterfalls, by leveraging Prefetching & Router Integration. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/prefetching When you know or suspect that a certain piece of data will be needed, you can use prefetching to populate the cache with that data ahead of time, leading to a faster experience. There are a few different prefetching patterns: 1. In event handlers 2. In components 3. Via router integration 4. During Server Rendering (another form of router integration) In this guide, we'll take a look at the first three, while the fourth will be covered in depth in the Server Rendering & Hydration guide and the Advanced Server Rendering guide. One specific use of prefetching is to avoid Request Waterfalls, for an in-depth background and explanation of those, see the Performance & Request Waterfalls guide. ## prefetchQuery & prefetchInfiniteQuery Before jumping into the different specific prefetch patterns, let's look at the prefetchQuery and prefetchInfiniteQuery functions. First a few basics: * Out of the box, these functions use the default staleTime configured for the queryClient to determine whether existing data in the cache is fresh or needs to be fetched again * You can also pass a specific staleTime like this: prefetchQuery({ queryKey: \['todos'\], queryFn: fn, staleTime: 5000 }) * This staleTime is only used for the prefetch, you still need to set it for any useQuery call as well * If you want to ignore staleTime and instead always return data if it's available in the cache, you can use the ensureQueryData function. * Tip: If you are prefetching on the server, set a default staleTime higher than 0 for that queryClient to avoid having to pass in a specific staleTime to each prefetch call * If no instances of useQuery appear for a prefetched query, it will be deleted and garbage collected after the time specified in gcTime * These functions returns Promise<void> and thus never return query data. If that's something you need, use fetchQuery/fetchInfiniteQuery instead. * The prefetch functions never throws errors because they usually try to fetch again in a useQuery which is a nice graceful fallback. If you need to catch errors, use fetchQuery/fetchInfiniteQuery instead. This is how you use prefetchQuery: tsx const prefetchTodos = async () => { // The results of this query will be cached like a normal query await queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos, }) } const prefetchTodos = async () => { // The results of this query will be cached like a normal query await queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos, }) } Infinite Queries can be prefetched like regular Queries. Per default, only the first page of the Query will be prefetched and will be stored under the given QueryKey. If you want to prefetch more than one page, you can use the pages option, in which case you also have to provide a getNextPageParam function: tsx const prefetchProjects = async () => { // The results of this query will be cached like a normal query await queryClient.prefetchInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, pages: 3, // prefetch the first 3 pages }) } const prefetchProjects = async () => { // The results of this query will be cached like a normal query await queryClient.prefetchInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, pages: 3, // prefetch the first 3 pages }) } Next, let's look at how you can use these and other ways to prefetch in different situations. ## Prefetch in event handlers A straightforward form of prefetching is doing it when the user interacts with something. In this example we'll use queryClient.prefetchQuery to start a prefetch on onMouseEnter or onFocus. tsx function ShowDetailsButton() { const queryClient = useQueryClient() const prefetch = () => { queryClient.prefetchQuery({ queryKey: ['details'], queryFn: getDetailsData, // Prefetch only fires when data is older than the staleTime, // so in a case like this you definitely want to set one staleTime: 60000, }) } return ( <button onMouseEnter={prefetch} onFocus={prefetch} onClick={...}> Show Details </button> ) } function ShowDetailsButton() { const queryClient = useQueryClient() const prefetch = () => { queryClient.prefetchQuery({ queryKey: ['details'], queryFn: getDetailsData, // Prefetch only fires when data is older than the staleTime, // so in a case like this you definitely want to set one staleTime: 60000, }) } return ( <button onMouseEnter={prefetch} onFocus={prefetch} onClick={...}> Show Details </button> ) } ## Prefetch in components Prefetching during the component lifecycle is useful when we know some child or descendant will need a particular piece of data, but we can't render that until some other query has finished loading. Let's borrow an example from the Request Waterfall guide to explain: tsx function Article({ id }) { const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) if (isPending) { return 'Loading article...' } return ( <> <ArticleHeader articleData={articleData} /> <ArticleBody articleData={articleData} /> <Comments id={id} /> </> ) } function Comments({ id }) { const { data, isPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) ... } function Article({ id }) { const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) if (isPending) { return 'Loading article...' } return ( <> <ArticleHeader articleData={articleData} /> <ArticleBody articleData={articleData} /> <Comments id={id} /> </> ) } function Comments({ id }) { const { data, isPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) ... } This results in a request waterfall looking like this: 1. |> getArticleById() 2. |> getArticleCommentsById() 1. |> getArticleById() 2. |> getArticleCommentsById() As mentioned in that guide, one way to flatten this waterfall and improve performance is to hoist the getArticleCommentsById query to the parent and pass down the result as a prop, but what if this is not feasible or desirable, for example when the components are unrelated and have multiple levels between them? In that case, we can instead prefetch the query in the parent. The simplest way to do this is to use a query but ignore the result: tsx function Article({ id }) { const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) // Prefetch useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, // Optional optimization to avoid rerenders when this query changes: notifyOnChangeProps: [], }) if (isPending) { return 'Loading article...' } return ( <> <ArticleHeader articleData={articleData} /> <ArticleBody articleData={articleData} /> <Comments id={id} /> </> ) } function Comments({ id }) { const { data, isPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) ... } function Article({ id }) { const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) // Prefetch useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, // Optional optimization to avoid rerenders when this query changes: notifyOnChangeProps: [], }) if (isPending) { return 'Loading article...' } return ( <> <ArticleHeader articleData={articleData} /> <ArticleBody articleData={articleData} /> <Comments id={id} /> </> ) } function Comments({ id }) { const { data, isPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) ... } This starts fetching 'article-comments' immediately and flattens the waterfall: 1. |> getArticleById() 1. |> getArticleCommentsById() 1. |> getArticleById() 1. |> getArticleCommentsById() If you want to prefetch together with Suspense, you will have to do things a bit differently. You can't use useSuspenseQueries to prefetch, since the prefetch would block the component from rendering. You also can not use useQuery for the prefetch, because that wouldn't start the prefetch until after suspenseful query had resolved. For this scenario, you can use the usePrefetchQuery or the usePrefetchInfiniteQuery hooks available in the library. You can now use useSuspenseQuery in the component that actually needs the data. You _might_ want to wrap this later component in its own <Suspense> boundary so the "secondary" query we are prefetching does not block rendering of the "primary" data. tsx function ArticleLayout({ id }) { usePrefetchQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) return ( <Suspense fallback="Loading article"> <Article id={id} /> </Suspense> ) } function Article({ id }) { const { data: articleData, isPending } = useSuspenseQuery({ queryKey: ['article', id], queryFn: getArticleById, }) ... } function ArticleLayout({ id }) { usePrefetchQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) return ( <Suspense fallback="Loading article"> <Article id={id} /> </Suspense> ) } function Article({ id }) { const { data: articleData, isPending } = useSuspenseQuery({ queryKey: ['article', id], queryFn: getArticleById, }) ... } Another way is to prefetch inside of the query function. This makes sense if you know that every time an article is fetched it's very likely comments will also be needed. For this, we'll use queryClient.prefetchQuery: tsx const queryClient = useQueryClient() const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: (...args) => { queryClient.prefetchQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) return getArticleById(...args) }, }) const queryClient = useQueryClient() const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: (...args) => { queryClient.prefetchQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) return getArticleById(...args) }, }) Prefetching in an effect also works, but note that if you are using useSuspenseQuery in the same component, this effect wont run until _after_ the query finishes which might not be what you want. tsx const queryClient = useQueryClient() useEffect(() => { queryClient.prefetchQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) }, [queryClient, id]) const queryClient = useQueryClient() useEffect(() => { queryClient.prefetchQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) }, [queryClient, id]) To recap, if you want to prefetch a query during the component lifecycle, there are a few different ways to do it, pick the one that suits your situation best: * Prefetch before a suspense boundary using usePrefetchQuery or usePrefetchInfiniteQuery hooks * Use useQuery or useSuspenseQueries and ignore the result * Prefetch inside the query function * Prefetch in an effect Let's look at a slightly more advanced case next. ### Dependent Queries & Code Splitting Sometimes we want to prefetch conditionally, based on the result of another fetch. Consider this example borrowed from the Performance & Request Waterfalls guide: tsx // This lazy loads the GraphFeedItem component, meaning // it wont start loading until something renders it const GraphFeedItem = React.lazy(() => import('./GraphFeedItem')) function Feed() { const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: getFeed, }) if (isPending) { return 'Loading feed...' } return ( <> {data.map((feedItem) => { if (feedItem.type === 'GRAPH') { return <GraphFeedItem key={feedItem.id} feedItem={feedItem} /> } return <StandardFeedItem key={feedItem.id} feedItem={feedItem} /> })} </> ) } // GraphFeedItem.tsx function GraphFeedItem({ feedItem }) { const { data, isPending } = useQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) ... } // This lazy loads the GraphFeedItem component, meaning // it wont start loading until something renders it const GraphFeedItem = React.lazy(() => import('./GraphFeedItem')) function Feed() { const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: getFeed, }) if (isPending) { return 'Loading feed...' } return ( <> {data.map((feedItem) => { if (feedItem.type === 'GRAPH') { return <GraphFeedItem key={feedItem.id} feedItem={feedItem} /> } return <StandardFeedItem key={feedItem.id} feedItem={feedItem} /> })} </> ) } // GraphFeedItem.tsx function GraphFeedItem({ feedItem }) { const { data, isPending } = useQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) ... } As noted over in that guide, this example leads to the following double request waterfall: 1. |> getFeed() 2. |> JS for <GraphFeedItem> 3. |> getGraphDataById() 1. |> getFeed() 2. |> JS for <GraphFeedItem> 3. |> getGraphDataById() If we can not restructure our API so getFeed() also returns the getGraphDataById() data when necessary, there is no way to get rid of the getFeed->getGraphDataById waterfall, but by leveraging conditional prefetching, we can at least load the code and data in parallel. Just like described above, there are multiple ways to do this, but for this example, we'll do it in the query function: tsx function Feed() { const queryClient = useQueryClient() const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: async (...args) => { const feed = await getFeed(...args) for (const feedItem of feed) { if (feedItem.type === 'GRAPH') { queryClient.prefetchQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) } } return feed } }) ... } function Feed() { const queryClient = useQueryClient() const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: async (...args) => { const feed = await getFeed(...args) for (const feedItem of feed) { if (feedItem.type === 'GRAPH') { queryClient.prefetchQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) } } return feed } }) ... } This would load the code and data in parallel: 1. |> getFeed() 2. |> JS for <GraphFeedItem> 2. |> getGraphDataById() 1. |> getFeed() 2. |> JS for <GraphFeedItem> 2. |> getGraphDataById() There is a tradeoff however, in that the code for getGraphDataById is now included in the parent bundle instead of in JS for <GraphFeedItem> so you'll need to determine what's the best performance tradeoff on a case by case basis. If GraphFeedItem are likely, it's probably worth to include the code in the parent. If they are exceedingly rare, it's probably not. ## Router Integration Because data fetching in the component tree itself can easily lead to request waterfalls and the different fixes for that can be cumbersome as they accumulate throughout the application, an attractive way to do prefetching is integrating it at the router level. In this approach, you explicitly declare for each _route_ what data is going to be needed for that component tree, ahead of time. Because Server Rendering has traditionally needed all data to be loaded before rendering starts, this has been the dominating approach for SSR'd apps for a long time. This is still a common approach and you can read more about it in the Server Rendering & Hydration guide. For now, let's focus on the client side case and look at an example of how you can make this work with Tanstack Router. These examples leave out a lot of setup and boilerplate to stay concise, you can check out a full React Query example over in the Tanstack Router docs. When integrating at the router level, you can choose to either _block_ rendering of that route until all data is present, or you can start a prefetch but not await the result. That way, you can start rendering the route as soon as possible. You can also mix these two approaches and await some critical data, but start rendering before all the secondary data has finished loading. In this example, we'll configure an /article route to not render until the article data has finished loading, as well as start prefetching comments as soon as possible, but not block rendering the route if comments haven't finished loading yet. tsx const queryClient = new QueryClient() const routerContext = new RouterContext() const rootRoute = routerContext.createRootRoute({ component: () => { ... } }) const articleRoute = new Route({ getParentRoute: () => rootRoute, path: 'article', beforeLoad: () => { return { articleQueryOptions: { queryKey: ['article'], queryFn: fetchArticle }, commentsQueryOptions: { queryKey: ['comments'], queryFn: fetchComments }, } }, loader: async ({ context: { queryClient }, routeContext: { articleQueryOptions, commentsQueryOptions }, }) => { // Fetch comments asap, but don't block queryClient.prefetchQuery(commentsQueryOptions) // Don't render the route at all until article has been fetched await queryClient.prefetchQuery(articleQueryOptions) }, component: ({ useRouteContext }) => { const { articleQueryOptions, commentsQueryOptions } = useRouteContext() const articleQuery = useQuery(articleQueryOptions) const commentsQuery = useQuery(commentsQueryOptions) return ( ... ) }, errorComponent: () => 'Oh crap!', }) const queryClient = new QueryClient() const routerContext = new RouterContext() const rootRoute = routerContext.createRootRoute({ component: () => { ... } }) const articleRoute = new Route({ getParentRoute: () => rootRoute, path: 'article', beforeLoad: () => { return { articleQueryOptions: { queryKey: ['article'], queryFn: fetchArticle }, commentsQueryOptions: { queryKey: ['comments'], queryFn: fetchComments }, } }, loader: async ({ context: { queryClient }, routeContext: { articleQueryOptions, commentsQueryOptions }, }) => { // Fetch comments asap, but don't block queryClient.prefetchQuery(commentsQueryOptions) // Don't render the route at all until article has been fetched await queryClient.prefetchQuery(articleQueryOptions) }, component: ({ useRouteContext }) => { const { articleQueryOptions, commentsQueryOptions } = useRouteContext() const articleQuery = useQuery(articleQueryOptions) const commentsQuery = useQuery(commentsQueryOptions) return ( ... ) }, errorComponent: () => 'Oh crap!', }) Integration with other routers is also possible, see the React Router example for another demonstration. ## Manually Priming a Query If you already have the data for your query synchronously available, you don't need to prefetch it. You can just use the Query Client's setQueryData method to directly add or update a query's cached result by key. tsx queryClient.setQueryData(['todos'], todos) queryClient.setQueryData(['todos'], todos) ## Further reading For a deep-dive on how to get data into your Query Cache before you fetch, have a look at #17: Seeding the Query Cache from the Community Resources. Integrating with Server Side routers and frameworks is very similar to what we just saw, with the addition that the data has to passed from the server to the client to be hydrated into the cache there. To learn how, continue on to the Server Rendering & Hydration guide. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/ssr In this guide you'll learn how to use React Query with server rendering. See the guide on Prefetching & Router Integration for some background. You might also want to check out the Performance & Request Waterfalls guide before that. For advanced server rendering patterns, such as streaming, Server Components and the new Next.js app router, see the Advanced Server Rendering guide. If you just want to see some code, you can skip ahead to the Full Next.js pages router example or the Full Remix example below. ## Server Rendering & React Query So what is server rendering anyway? The rest of this guide will assume you are familiar with the concept, but let's spend some time to look at how it relates to React Query. Server rendering is the act of generating the initial html on the server, so that the user has some content to look at as soon as the page loads. This can happen on demand when a page is requested (SSR). It can also happen ahead of time either because a previous request was cached, or at build time (SSG). If you've read the Request Waterfalls guide, you might remember this: 1. |-> Markup (without content) 2. |-> JS 3. |-> Query 1. |-> Markup (without content) 2. |-> JS 3. |-> Query With a client rendered application, these are the minimum 3 server roundtrips you will need to make before getting any content on the screen for the user. One way of viewing server rendering is that it turns the above into this: 1. |-> Markup (with content AND initial data) 2. |-> JS 1. |-> Markup (with content AND initial data) 2. |-> JS As soon as **1.** is complete, the user can see the content and when **2.** finishes, the page is interactive and clickable. Because the markup also contains the initial data we need, step **3.** does not need to run on the client at all, at least until you want to revalidate the data for some reason. This is all from the clients perspective. On the server, we need to **prefetch** that data before we generate/render the markup, we need to **dehydrate** that data into a serializable format we can embed in the markup, and on the client we need to **hydrate** that data into a React Query cache so we can avoid doing a new fetch on the client. Read on to learn how to implement these three steps with React Query. ## A quick note on Suspense This guide uses the regular useQuery API. While we don't necessarily recommend it, it is possible to replace this with useSuspenseQuery instead **as long as you always prefetch all your queries**. The upside is that you get to use <Suspense> for loading states on the client. If you do forget to prefetch a query when you are using useSuspenseQuery, the consequences will depend on the framework you are using. In some cases, the data will Suspend and get fetched on the server but never be hydrated to the client, where it will fetch again. In these cases you will get a markup hydration mismatch, because the server and the client tried to render different things. ## Initial setup The first steps of using React Query is always to create a queryClient and wrap the application in a <QueryClientProvider>. When doing server rendering, it's important to create the queryClient instance **inside of your app**, in React state (an instance ref works fine too). **This ensures that data is not shared between different users and requests**, while still only creating the queryClient once per component lifecycle. Next.js pages router: tsx // _app.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' // NEVER DO THIS: // const queryClient = new QueryClient() // // Creating the queryClient at the file root level makes the cache shared // between all requests and means _all_ data gets passed to _all_ users. // Besides being bad for performance, this also leaks any sensitive data. export default function MyApp({ Component, pageProps }) { // Instead do this, which ensures each request has its own cache: const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> ) } // _app.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' // NEVER DO THIS: // const queryClient = new QueryClient() // // Creating the queryClient at the file root level makes the cache shared // between all requests and means _all_ data gets passed to _all_ users. // Besides being bad for performance, this also leaks any sensitive data. export default function MyApp({ Component, pageProps }) { // Instead do this, which ensures each request has its own cache: const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> ) } Remix: tsx // app/root.tsx import { Outlet } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp() { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Outlet /> </QueryClientProvider> ) } // app/root.tsx import { Outlet } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp() { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Outlet /> </QueryClientProvider> ) } ## Get started fast with initialData The quickest way to get started is to not involve React Query at all when it comes to prefetching and not use the dehydrate/hydrate APIs. What you do instead is passing the raw data in as the initialData option to useQuery. Let's look at an example using Next.js pages router, using getServerSideProps. tsx export async function getServerSideProps() { const posts = await getPosts() return { props: { posts } } } function Posts(props) { const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: props.posts, }) // ... } export async function getServerSideProps() { const posts = await getPosts() return { props: { posts } } } function Posts(props) { const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: props.posts, }) // ... } This also works with getStaticProps or even the older getInitialProps and the same pattern can be applied in any other framework that has equivalent functions. This is what the same example looks like with Remix: tsx export async function loader() { const posts = await getPosts() return json({ posts }) } function Posts() { const { posts } = useLoaderData<typeof loader>() const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: posts, }) // ... } export async function loader() { const posts = await getPosts() return json({ posts }) } function Posts() { const { posts } = useLoaderData<typeof loader>() const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: posts, }) // ... } The setup is minimal and this can be a quick solution for some cases, but there are a **few tradeoffs to consider** when compared to the full approach: * If you are calling useQuery in a component deeper down in the tree you need to pass the initialData down to that point * If you are calling useQuery with the same query in multiple locations, passing initialData to only one of them can be brittle and break when your app changes since. If you remove or move the component that has the useQuery with initialData, the more deeply nested useQuery might no longer have any data. Passing initialData to **all** queries that needs it can also be cumbersome. * There is no way to know at what time the query was fetched on the server, so dataUpdatedAt and determining if the query needs refetching is based on when the page loaded instead * If there is already data in the cache for a query, initialData will never overwrite this data, **even if the new data is fresher than the old one**. * To understand why this is especially bad, consider the getServerSideProps example above. If you navigate back and forth to a page several times, getServerSideProps would get called each time and fetch new data, but because we are using the initialData option, the client cache and data would never be updated. Setting up the full hydration solution is straightforward and does not have these drawbacks, this will be the focus for the rest of the documentation. ## Using the Hydration APIs With just a little more setup, you can use a queryClient to prefetch queries during a preload phase, pass a serialized version of that queryClient to the rendering part of the app and reuse it there. This avoids the drawbacks above. Feel free to skip ahead for full Next.js pages router and Remix examples, but at a general level these are the extra steps: * In the framework loader function, create a const queryClient = new QueryClient(options) * In the loader function, do await queryClient.prefetchQuery(...) for each query you want to prefetch * You want to use await Promise.all(...) to fetch the queries in parallel when possible * It's fine to have queries that aren't prefetched. These wont be server rendered, instead they will be fetched on the client after the application is interactive. This can be great for content that are shown only after user interaction, or is far down on the page to avoid blocking more critical content. * From the loader, return dehydrate(queryClient), note that the exact syntax to return this differs between frameworks * Wrap your tree with <HydrationBoundary state={dehydratedState}> where dehydratedState comes from the framework loader. How you get dehydratedState also differs between frameworks. * This can be done for each route, or at the top of the application to avoid boilerplate, see examples > An interesting detail is that there are actually _three_ queryClients involved. The framework loaders are a form of "preloading" phase that happens before rendering, and this phase has its own queryClient that does the prefetching. The dehydrated result of this phase gets passed to **both** the server rendering process **and** the client rendering process which each has its own queryClient. This ensures they both start with the same data so they can return the same markup. > Server Components are another form of "preloading" phase, that can also "preload" (pre-render) parts of a React component tree. Read more in the Advanced Server Rendering guide. ### Full Next.js pages router example > For app router documentation, see the Advanced Server Rendering guide. Initial setup: tsx // _app.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> ) } // _app.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> ) } In each route: tsx // pages/posts.tsx import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' // This could also be getServerSideProps export async function getStaticProps() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return { props: { dehydratedState: dehydrate(queryClient), }, } } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } // pages/posts.tsx import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' // This could also be getServerSideProps export async function getStaticProps() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return { props: { dehydratedState: dehydrate(queryClient), }, } } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } ### Full Remix example Initial setup: tsx // app/root.tsx import { Outlet } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp() { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Outlet /> </QueryClientProvider> ) } // app/root.tsx import { Outlet } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp() { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Outlet /> </QueryClientProvider> ) } In each route, note that it's fine to do this in nested routes too: tsx // app/routes/posts.tsx import { json } from '@remix-run/node' import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' export async function loader() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return json({ dehydratedState: dehydrate(queryClient) }) } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute() { const { dehydratedState } = useLoaderData<typeof loader>() return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } // app/routes/posts.tsx import { json } from '@remix-run/node' import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' export async function loader() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return json({ dehydratedState: dehydrate(queryClient) }) } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute() { const { dehydratedState } = useLoaderData<typeof loader>() return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } ## Optional - Remove boilerplate Having this part in every route might seem like a lot of boilerplate: tsx export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } While there is nothing wrong with this approach, if you want to get rid of this boilerplate, here's how you can modify your setup in Next.js: tsx // _app.tsx import { HydrationBoundary, QueryClient, QueryClientProvider, } from '@tanstack/react-query' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState(() => new QueryClient()) return ( <QueryClientProvider client={queryClient}> <HydrationBoundary state={pageProps.dehydratedState}> <Component {...pageProps} /> </HydrationBoundary> </QueryClientProvider> ) } // pages/posts.tsx // Remove PostsRoute with the HydrationBoundary and instead export Posts directly: export default function Posts() { ... } // _app.tsx import { HydrationBoundary, QueryClient, QueryClientProvider, } from '@tanstack/react-query' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState(() => new QueryClient()) return ( <QueryClientProvider client={queryClient}> <HydrationBoundary state={pageProps.dehydratedState}> <Component {...pageProps} /> </HydrationBoundary> </QueryClientProvider> ) } // pages/posts.tsx // Remove PostsRoute with the HydrationBoundary and instead export Posts directly: export default function Posts() { ... } With Remix, this is a little bit more involved, we recommend checking out the use-dehydrated-state package. ## Prefetching dependent queries Over in the Prefetching guide we learned how to prefetch dependent queries, but how do we do this in framework loaders? Consider the following code, taken from the Dependent Queries guide: tsx // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) How would we prefetch this so it can be server rendered? Here's an example: tsx // For Remix, rename this to loader instead export async function getServerSideProps() { const queryClient = new QueryClient() const user = await queryClient.fetchQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) if (user?.userId) { await queryClient.prefetchQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, }) } // For Remix: // return json({ dehydratedState: dehydrate(queryClient) }) return { props: { dehydratedState: dehydrate(queryClient) } } } // For Remix, rename this to loader instead export async function getServerSideProps() { const queryClient = new QueryClient() const user = await queryClient.fetchQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) if (user?.userId) { await queryClient.prefetchQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, }) } // For Remix: // return json({ dehydratedState: dehydrate(queryClient) }) return { props: { dehydratedState: dehydrate(queryClient) } } } This can get more complex of course, but since these loader functions are just JavaScript, you can use the full power of the language to build your logic. Make sure you prefetch all queries that you want to be server rendered. ## Error handling React Query defaults to a graceful degradation strategy. This means: * queryClient.prefetchQuery(...) never throws errors * dehydrate(...) only includes successful queries, not failed ones This will lead to any failed queries being retried on the client and that the server rendered output will include loading states instead of the full content. While a good default, sometimes this is not what you want. When critical content is missing, you might want to respond with a 404 or 500 status code depending on the situation. For these cases, use queryClient.fetchQuery(...) instead, which will throw errors when it fails, letting you handle things in a suitable way. tsx let result try { result = await queryClient.fetchQuery(...) } catch (error) { // Handle the error, refer to your framework documentation } // You might also want to check and handle any invalid `result` here let result try { result = await queryClient.fetchQuery(...) } catch (error) { // Handle the error, refer to your framework documentation } // You might also want to check and handle any invalid `result` here If you for some reason want to include failed queries in the dehydrated state to avoid retries, you can use the option shouldDehydrateQuery to override the default function and implement your own logic: tsx dehydrate(queryClient, { shouldDehydrateQuery: (query) => { // This will include all queries, including failed ones, // but you can also implement your own logic by inspecting `query` return true }, }) dehydrate(queryClient, { shouldDehydrateQuery: (query) => { // This will include all queries, including failed ones, // but you can also implement your own logic by inspecting `query` return true }, }) ## Serialization When doing return { props: { dehydratedState: dehydrate(queryClient) } } in Next.js, or return json({ dehydratedState: dehydrate(queryClient) }) in Remix, what happens is that the dehydratedState representation of the queryClient is serialized by the framework so it can be embedded into the markup and transported to the client. By default, these frameworks only supports returning things that are safely serializable/parsable, and therefore does not support undefined, Error, Date, Map, Set, BigInt, Infinity, NaN, \-0, regular expressions etc. This also means that you can not return any of these things from your queries. If returning these values is something you want, check out superjson or similar packages. If you are using a custom SSR setup, you need to take care of this step yourself. Your first instinct might be to use JSON.stringify(dehydratedState), but because this doesn't escape things like <script>alert('Oh no..')</script> by default, this can easily lead to **XSS-vulnerabilities** in your application. superjson also **does not** escape values and is unsafe to use by itself in a custom SSR setup (unless you add an extra step for escaping the output). Instead we recommend using a library like Serialize JavaScript or devalue which are both safe against XSS injections out of the box. ## A note about request waterfalls In the Performance & Request Waterfalls guide we mentioned we would revisit how server rendering changes one of the more complex nested waterfalls. Check back for the specific code example, but as a refresher, we have a code split <GraphFeedItem> component inside a <Feed> component. This only renders if the feed contains a graph item and both of these components fetches their own data. With client rendering, this leads to the following request waterfall: 1. |> Markup (without content) 2. |> JS for <Feed> 3. |> getFeed() 4. |> JS for <GraphFeedItem> 5. |> getGraphDataById() 1. |> Markup (without content) 2. |> JS for <Feed> 3. |> getFeed() 4. |> JS for <GraphFeedItem> 5. |> getGraphDataById() The nice thing about server rendering is that we can turn the above into: 1. |> Markup (with content AND initial data) 2. |> JS for <Feed> 2. |> JS for <GraphFeedItem> 1. |> Markup (with content AND initial data) 2. |> JS for <Feed> 2. |> JS for <GraphFeedItem> Note that the queries are no longer fetched on the client, instead their data was included in the markup. The reason we can now load the JS in parallel is that since <GraphFeedItem> was rendered on the server we know that we are going to need this JS on the client as well and can insert a script-tag for this chunk in the markup. On the server, we would still have this request waterfall: 1. |> getFeed() 2. |> getGraphDataById() 1. |> getFeed() 2. |> getGraphDataById() We simply can not know before we have fetched the feed if we also need to fetch graph data, they are dependent queries. Because this happens on the server where latency is generally both lower and more stable, this often isn't such a big deal. Amazing, we've mostly flattened our waterfalls! There's a catch though. Let's call this page the /feed page, and let's pretend we also have another page like /posts. If we type in www.example.com/feed directly in the url bar and hit enter, we get all these great server rendering benefits, BUT, if we instead type in www.example.com/posts and then **click a link** to /feed, we're back to to this: 1. |> JS for <Feed> 2. |> getFeed() 3. |> JS for <GraphFeedItem> 4. |> getGraphDataById() 1. |> JS for <Feed> 2. |> getFeed() 3. |> JS for <GraphFeedItem> 4. |> getGraphDataById() This is because with SPA's, server rendering only works for the initial page load, not for any subsequent navigation. Modern frameworks often try to solve this by fetching the initial code and data in parallel, so if you were using Next.js or Remix with the prefetching patterns we outlined in this guide, including how to prefetch dependent queries, it would actually look like this instead: 1. |> JS for <Feed> 1. |> getFeed() + getGraphDataById() 2. |> JS for <GraphFeedItem> 1. |> JS for <Feed> 1. |> getFeed() + getGraphDataById() 2. |> JS for <GraphFeedItem> This is much better, but if we want to improve this further we can flatten this to a single roundtrip with Server Components. Learn how in the Advanced Server Rendering guide. ## Tips, Tricks and Caveats ### Staleness is measured from when the query was fetched on the server A query is considered stale depending on when it was dataUpdatedAt. A caveat here is that the server needs to have the correct time for this to work properly, but UTC time is used, so timezones do not factor into this. Because staleTime defaults to 0, queries will be refetched in the background on page load by default. You might want to use a higher staleTime to avoid this double fetching, especially if you don't cache your markup. This refetching of stale queries is a perfect match when caching markup in a CDN! You can set the cache time of the page itself decently high to avoid having to re-render pages on the server, but configure the staleTime of the queries lower to make sure data is refetched in the background as soon as a user visits the page. Maybe you want to cache the pages for a week, but refetch the data automatically on page load if it's older than a day? ### High memory consumption on server In case you are creating the QueryClient for every request, React Query creates the isolated cache for this client, which is preserved in memory for the gcTime period. That may lead to high memory consumption on server in case of high number of requests during that period. On the server, gcTime defaults to Infinity which disables manual garbage collection and will automatically clear memory once a request has finished. If you are explicitly setting a non-Infinity gcTime then you will be responsible for clearing the cache early. Avoid setting gcTime to 0 as it may result in a hydration error. This occurs because the Hydration Boundary places necessary data into the cache for rendering, but if the garbage collector removes the data before the rendering completes, issues may arise. If you require a shorter gcTime, we recommend setting it to 2 \* 1000 to allow sufficient time for the app to reference the data. To clear the cache after it is not needed and to lower memory consumption, you can add a call to queryClient.clear() after the request is handled and dehydrated state has been sent to the client. Alternatively, you can set a smaller gcTime. ### Caveat for Next.js rewrites There's a catch if you're using Next.js' rewrites feature together with Automatic Static Optimization or getStaticProps: It will cause a second hydration by React Query. That's because Next.js needs to ensure that they parse the rewrites on the client and collect any params after hydration so that they can be provided in router.query. The result is missing referential equality for all the hydration data, which for example triggers wherever your data is used as props of components or in the dependency array of useEffects/useMemos. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr Welcome to the Advanced Server Rendering guide, where you will learn all about using React Query with streaming, Server Components and the Next.js app router. You might want to read the Server Rendering & Hydration guide before this one as it teaches the basics for using React Query with SSR, and Performance & Request Waterfalls as well as Prefetching & Router Integration also contains valuable background. Before we start, let's note that while the initialData approach outlined in the SSR guide also works with Server Components, we'll focus this guide on the hydration APIs. ## Server Components & Next.js app router We won't cover Server Components in depth here, but the short version is that they are components that are guaranteed to _only_ run on the server, both for the initial page view and **also on page transitions**. This is similar to how Next.js getServerSideProps/getStaticProps and Remix loader works, as these also always run on the server but while those can only return data, Server Components can do a lot more. The data part is central to React Query however, so let's focus on that. How do we take what we learned in the Server Rendering guide about passing data prefetched in framework loaders to the app and apply that to Server Components and the Next.js app router? The best way to start thinking about this is to consider Server Components as "just" another framework loader. ### A quick note on terminology So far in these guides, we've been talking about the _server_ and the _client_. It's important to note that confusingly enough this does not match 1-1 with _Server Components_ and _Client Components_. Server Components are guaranteed to only run on the server, but Client Components can actually run in both places. The reason for this is that they can also render during the initial _server rendering_ pass. One way to think of this is that even though Server Components also _render_, they happen during a "loader phase" (always happens on the server), while Client Components run during the "application phase". That application can run both on the server during SSR, and in for example a browser. Where exactly that application runs and if it runs during SSR or not might differ between frameworks. ### Initial setup The first step of any React Query setup is always to create a queryClient and wrap your application in a QueryClientProvider. With Server Components, this looks mostly the same across frameworks, one difference being the filename conventions: tsx // In Next.js, this file would be called: app/providers.tsx 'use client' // Since QueryClientProvider relies on useContext under the hood, we have to put 'use client' on top import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export default function Providers({ children }: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> ) } // In Next.js, this file would be called: app/providers.tsx 'use client' // Since QueryClientProvider relies on useContext under the hood, we have to put 'use client' on top import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export default function Providers({ children }: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> ) } tsx // In Next.js, this file would be called: app/layout.tsx import Providers from './providers' export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <head /> <body> <Providers>{children}</Providers> </body> </html> ) } // In Next.js, this file would be called: app/layout.tsx import Providers from './providers' export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <head /> <body> <Providers>{children}</Providers> </body> </html> ) } This part is pretty similar to what we did in the SSR guide, we just need to split things up into two different files. ### Prefetching and de/hydrating data Let's next look at how to actually prefetch data and dehydrate and hydrate it. This is what it looked like using the **Next.js pages router**: tsx // pages/posts.tsx import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' // This could also be getServerSideProps export async function getStaticProps() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return { props: { dehydratedState: dehydrate(queryClient), }, } } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way // // Note that we are using useQuery here instead of useSuspenseQuery. // Because this data has already been prefetched, there is no need to // ever suspend in the component itself. If we forget or remove the // prefetch, this will instead fetch the data on the client, while // using useSuspenseQuery would have had worse side effects. const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } // pages/posts.tsx import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' // This could also be getServerSideProps export async function getStaticProps() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return { props: { dehydratedState: dehydrate(queryClient), }, } } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way // // Note that we are using useQuery here instead of useSuspenseQuery. // Because this data has already been prefetched, there is no need to // ever suspend in the component itself. If we forget or remove the // prefetch, this will instead fetch the data on the client, while // using useSuspenseQuery would have had worse side effects. const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } Converting this to the app router actually looks pretty similar, we just need to move things around a bit. First, we'll create a Server Component to do the prefetching part: tsx // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Posts from './posts' export default async function PostsPage() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( // Neat! Serialization is now as easy as passing props. // HydrationBoundary is a Client Component, so hydration will happen there. <HydrationBoundary state={dehydrate(queryClient)}> <Posts /> </HydrationBoundary> ) } // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Posts from './posts' export default async function PostsPage() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( // Neat! Serialization is now as easy as passing props. // HydrationBoundary is a Client Component, so hydration will happen there. <HydrationBoundary state={dehydrate(queryClient)}> <Posts /> </HydrationBoundary> ) } Next, we'll look at what the Client Component part looks like: tsx // app/posts/posts.tsx 'use client' export default function Posts() { // This useQuery could just as well happen in some deeper // child to <Posts>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: () => getPosts(), }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix. const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } // app/posts/posts.tsx 'use client' export default function Posts() { // This useQuery could just as well happen in some deeper // child to <Posts>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: () => getPosts(), }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix. const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } One neat thing about the examples above is that the only thing that is Next.js-specific here are the file names, everything else would look the same in any other framework that supports Server Components. In the SSR guide, we noted that you could get rid of the boilerplate of having <HydrationBoundary> in every route. This is not possible with Server Components. > NOTE: If you encounter a type error while using async Server Components with TypeScript versions lower than 5.1.3 and @types/react versions lower than 18.2.8, it is recommended to update to the latest versions of both. Alternatively, you can use the temporary workaround of adding {/\* @ts-expect-error Server Component \*/} when calling this component inside another. For more information, see Async Server Component TypeScript Error in the Next.js 13 docs. > NOTE: If you encounter an error Only plain objects, and a few built-ins, can be passed to Server Actions. Classes or null prototypes are not supported. make sure that you're **not** passing to queryFn a function reference, instead call the function because queryFn args has a bunch of properties and not all of it would be serializable. see Server Action only works when queryFn isn't a reference. ### Nesting Server Components A nice thing about Server Components is that they can be nested and exist on many levels in the React tree, making it possible to prefetch data closer to where it's actually used instead of only at the top of the application (just like Remix loaders). This can be as simple as a Server Component rendering another Server Component (we'll leave the Client Components out in this example for brevity): tsx // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Posts from './posts' import CommentsServerComponent from './comments-server' export default async function PostsPage() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( <HydrationBoundary state={dehydrate(queryClient)}> <Posts /> <CommentsServerComponent /> </HydrationBoundary> ) } // app/posts/comments-server.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Comments from './comments' export default async function CommentsServerComponent() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) return ( <HydrationBoundary state={dehydrate(queryClient)}> <Comments /> </HydrationBoundary> ) } // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Posts from './posts' import CommentsServerComponent from './comments-server' export default async function PostsPage() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( <HydrationBoundary state={dehydrate(queryClient)}> <Posts /> <CommentsServerComponent /> </HydrationBoundary> ) } // app/posts/comments-server.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Comments from './comments' export default async function CommentsServerComponent() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) return ( <HydrationBoundary state={dehydrate(queryClient)}> <Comments /> </HydrationBoundary> ) } As you can see, it's perfectly fine to use <HydrationBoundary> in multiple places, and create and dehydrate multiple queryClient for prefetching. Note that because we are awaiting getPosts before rendering CommentsServerComponent this would lead to a server side waterfall: 1. |> getPosts() 2. |> getComments() 1. |> getPosts() 2. |> getComments() If the server latency to the data is low, this might not be a huge issue, but is still worth pointing out. In Next.js, besides prefetching data in page.tsx, you can also do it in layout.tsx, and in parallel routes. Because these are all part of the routing, Next.js knows how to fetch them all in parallel. So if CommentsServerComponent above was instead expressed as a parallel route, the waterfall would be flattened automatically. As more frameworks start supporting Server Components, they might have other routing conventions. Read your framework docs for details. ### Alternative: Use a single queryClient for prefetching In the example above, we create a new queryClient for each Server Component that fetches data. This is the recommended approach, but if you want to, you can alternatively create a single one that is reused across all Server Components: tsx // app/getQueryClient.tsx import { QueryClient } from '@tanstack/react-query' import { cache } from 'react' // cache() is scoped per request, so we don't leak data between requests const getQueryClient = cache(() => new QueryClient()) export default getQueryClient // app/getQueryClient.tsx import { QueryClient } from '@tanstack/react-query' import { cache } from 'react' // cache() is scoped per request, so we don't leak data between requests const getQueryClient = cache(() => new QueryClient()) export default getQueryClient The benefit of this is that you can call getQueryClient() to get a hold of this client anywhere that gets called from a Server Component, including utility functions. The downside is that every time you call dehydrate(getQueryClient()), you serialize _the entire_ queryClient, including queries that have already been serialized before and are unrelated to the current Server Component which is unnecessary overhead. Next.js already dedupes requests that utilize fetch(), but if you are using something else in your queryFn, or if you use a framework that does _not_ dedupe these requests automatically, using a single queryClient as described above might make sense, despite the duplicated serialization. > As a future improvement, we might look into creating a dehydrateNew() function (name pending) that only dehydrate queries that are _new_ since the last call to dehydrateNew(). Feel free to get in touch if this sounds interesting and like something you want to help out with! ### Data ownership and revalidation With Server Components, it's important to think about data ownership and revalidation. To explain why, let's look at a modified example from above: tsx // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Posts from './posts' export default async function PostsPage() { const queryClient = new QueryClient() // Note we are now using fetchQuery() const posts = await queryClient.fetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( <HydrationBoundary state={dehydrate(queryClient)}> {/* This is the new part */} <div>Nr of posts: {posts.length}</div> <Posts /> </HydrationBoundary> ) } // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Posts from './posts' export default async function PostsPage() { const queryClient = new QueryClient() // Note we are now using fetchQuery() const posts = await queryClient.fetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( <HydrationBoundary state={dehydrate(queryClient)}> {/* This is the new part */} <div>Nr of posts: {posts.length}</div> <Posts /> </HydrationBoundary> ) } We are now rendering data from the getPosts query both in a Server Component and in a Client Component. This will be fine for the initial page render, but what happens when the query revalidates on the client for some reason when staleTime has been passed? React Query has no idea of how to _revalidate the Server Component_, so if it refetches the data on the client, causing React to rerender the list of posts, the Nr of posts: {posts.length} will end up out of sync. This is fine if you set staleTime: Infinity, so that React Query never revalidates, but this is probably not what you want if you are using React Query in the first place. Using React Query with Server Components makes most sense if: * You have an app using React Query and want to migrate to Server Components without rewriting all the data fetching * You want a familiar programming paradigm, but want to still sprinkle in the benefits of Server Components where it makes most sense * You have some use case that React Query covers, but that your framework of choice does not cover It's hard to give general advice on when it makes sense to pair React Query with Server Components and not. **If you are just starting out with a new Server Components app, we suggest you start out with any tools for data fetching your framework provides you with and avoid bringing in React Query until you actually need it.** This might be never, and that's fine, use the right tool for the job! If you do use it, a good rule of thumb is to avoid queryClient.fetchQuery unless you need to catch errors. If you do use it, don't render its result on the server or pass the result to another component, even a Client Component one. From the React Query perspective, treat Server Components as a place to prefetch data, nothing more. Of course, it's fine to have Server Components own some data, and Client Components own other, just make sure those two realities don't get out of sync. ## Streaming with Server Components The Next.js app router automatically streams any part of the application that is ready to be displayed to the browser as soon as possible, so finished content can be displayed immediately without waiting for still pending content. It does this along <Suspense> boundary lines. Note that if you create a file loading.tsx, this automatically creates a <Suspense> boundary behind the scenes. With the prefetching patterns described above, React Query is perfectly compatible with this form of streaming. As the data for each Suspense boundary resolves, Next.js can render and stream the finished content to the browser. This works even if you are using useQuery as outlined above because the suspending actually happens when you await the prefetch. As of React Query v5.40.0, you don't have to await all prefetches for this to work, as pending Queries can also be dehydrated and sent to the client. This lets you kick off prefetches as early as possible without letting them block an entire Suspense boundary, and streams the _data_ to the client as the query finishes. This can be useful for example if you want to prefetch some content that is only visible after some user interaction, or say if you want to await and render the first page of an infinite query, but start prefetching page 2 without blocking rendering. To make this work, we have to instruct the queryClient to also dehydrate pending Queries. We can do this globally, or by passing that option directly to dehydrate. We will also need to move the getQueryClient() function out of our app/providers.tsx file as we want to use it in our server component and our client provider. tsx // app/get-query-client.ts import { isServer, QueryClient, defaultShouldDehydrateQuery, } from '@tanstack/react-query' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { staleTime: 60 * 1000, }, dehydrate: { // include pending queries in dehydration shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === 'pending', shouldRedactErrors: (error) => { // We should not catch Next.js server errors // as that's how Next.js detects dynamic pages // so we cannot redact them. // Next.js also automatically redacts errors for us // with better digests. return false }, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined export function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } // app/get-query-client.ts import { isServer, QueryClient, defaultShouldDehydrateQuery, } from '@tanstack/react-query' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { staleTime: 60 * 1000, }, dehydrate: { // include pending queries in dehydration shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === 'pending', shouldRedactErrors: (error) => { // We should not catch Next.js server errors // as that's how Next.js detects dynamic pages // so we cannot redact them. // Next.js also automatically redacts errors for us // with better digests. return false }, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined export function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } > Note: This works in NextJs and Server Components because React can serialize Promises over the wire when you pass them down to Client Components. Then, all we need to do is provide a HydrationBoundary, but we don't need to await prefetches anymore: tsx // app/posts/page.tsx import { dehydrate, HydrationBoundary } from '@tanstack/react-query' import { getQueryClient } from './get-query-client' import Posts from './posts' // the function doesn't need to be `async` because we don't `await` anything export default function PostsPage() { const queryClient = getQueryClient() // look ma, no await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( <HydrationBoundary state={dehydrate(queryClient)}> <Posts /> </HydrationBoundary> ) } // app/posts/page.tsx import { dehydrate, HydrationBoundary } from '@tanstack/react-query' import { getQueryClient } from './get-query-client' import Posts from './posts' // the function doesn't need to be `async` because we don't `await` anything export default function PostsPage() { const queryClient = getQueryClient() // look ma, no await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( <HydrationBoundary state={dehydrate(queryClient)}> <Posts /> </HydrationBoundary> ) } On the client, the Promise will be put into the QueryCache for us. That means we can now call useSuspenseQuery inside the Posts component to "use" that Promise (which was created on the Server): tsx // app/posts/posts.tsx 'use client' export default function Posts() { const { data } = useSuspenseQuery({ queryKey: ['posts'], queryFn: getPosts }) // ... } // app/posts/posts.tsx 'use client' export default function Posts() { const { data } = useSuspenseQuery({ queryKey: ['posts'], queryFn: getPosts }) // ... } > Note that you could also useQuery instead of useSuspenseQuery, and the Promise would still be picked up correctly. However, NextJs won't suspend in that case and the component will render in the pending status, which also opts out of server rendering the content. If you're using non-JSON data types and serialize the query results on the server, you can specify the dehydrate.serializeData and hydrate.deserializeData options to serialize and deserialize the data on each side of the boundary to ensure the data in the cache is the same format both on the server and the client: tsx // app/get-query-client.ts import { QueryClient, defaultShouldDehydrateQuery } from '@tanstack/react-query' import { deserialize, serialize } from './transformer' function makeQueryClient() { return new QueryClient({ defaultOptions: { // ... hydrate: { deserializeData: deserialize, }, dehydrate: { serializeData: serialize, }, }, }) } // ... // app/get-query-client.ts import { QueryClient, defaultShouldDehydrateQuery } from '@tanstack/react-query' import { deserialize, serialize } from './transformer' function makeQueryClient() { return new QueryClient({ defaultOptions: { // ... hydrate: { deserializeData: deserialize, }, dehydrate: { serializeData: serialize, }, }, }) } // ... tsx // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import { getQueryClient } from './get-query-client' import { serialize } from './transformer' import Posts from './posts' export default function PostsPage() { const queryClient = getQueryClient() // look ma, no await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: () => getPosts().then(serialize), // <-- serialize the data on the server }) return ( <HydrationBoundary state={dehydrate(queryClient)}> <Posts /> </HydrationBoundary> ) } // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import { getQueryClient } from './get-query-client' import { serialize } from './transformer' import Posts from './posts' export default function PostsPage() { const queryClient = getQueryClient() // look ma, no await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: () => getPosts().then(serialize), // <-- serialize the data on the server }) return ( <HydrationBoundary state={dehydrate(queryClient)}> <Posts /> </HydrationBoundary> ) } tsx // app/posts/posts.tsx 'use client' export default function Posts() { const { data } = useSuspenseQuery({ queryKey: ['posts'], queryFn: getPosts }) // ... } // app/posts/posts.tsx 'use client' export default function Posts() { const { data } = useSuspenseQuery({ queryKey: ['posts'], queryFn: getPosts }) // ... } Now, your getPosts function can return e.g. Temporal datetime objects and the data will be serialized and deserialized on the client, assuming your transformer can serialize and deserialize those data types. For more information, check out the Next.js App with Prefetching Example. ## Experimental streaming without prefetching in Next.js While we recommend the prefetching solution detailed above because it flattens request waterfalls both on the initial page load **and** any subsequent page navigation, there is an experimental way to skip prefetching altogether and still have streaming SSR work: @tanstack/react-query-next-experimental This package will allow you to fetch data on the server (in a Client Component) by just calling useSuspenseQuery in your component. Results will then be streamed from the server to the client as SuspenseBoundaries resolve. If you call useSuspenseQuery without wrapping it in a <Suspense> boundary, the HTML response won't start until the fetch resolves. This can be when you want depending on the situation, but keep in mind that this will hurt your TTFB. To achieve this, wrap your app in the ReactQueryStreamedHydration component: tsx // app/providers.tsx 'use client' import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import * as React from 'react' import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export function Providers(props: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}> <ReactQueryStreamedHydration> {props.children} </ReactQueryStreamedHydration> </QueryClientProvider> ) } // app/providers.tsx 'use client' import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import * as React from 'react' import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export function Providers(props: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}> <ReactQueryStreamedHydration> {props.children} </ReactQueryStreamedHydration> </QueryClientProvider> ) } For more information, check out the NextJs Suspense Streaming Example. The big upside is that you no longer need to prefetch queries manually to have SSR work, and it even still streams in the result! This gives you phenomenal DX and lower code complexity. The downside is easiest to explain if we look back at the complex request waterfall example in the Performance & Request Waterfalls guide. Server Components with prefetching effectively eliminates the request waterfalls both for the initial page load **and** any subsequent navigation. This prefetch-less approach however will only flatten the waterfalls on the initial page load but ends up the same deep waterfall as the original example on page navigations: 1. |> JS for <Feed> 2. |> getFeed() 3. |> JS for <GraphFeedItem> 4. |> getGraphDataById() 1. |> JS for <Feed> 2. |> getFeed() 3. |> JS for <GraphFeedItem> 4. |> getGraphDataById() This is even worse than with getServerSideProps/getStaticProps, since with those we could at least parallelize data- and code-fetching. If you value DX/iteration/shipping speed with low code complexity over performance, don't have deeply nested queries, or are on top of your request waterfalls with parallel fetching using tools like useSuspenseQueries, this can be a good tradeoff. > It might be possible to combine the two approaches, but even we haven't tried that out yet. If you do try this, please report back your findings, or even update these docs with some tips! ## Final words Server Components and streaming are still fairly new concepts and we are still figuring out how React Query fits in and what improvements we can make to the API. We welcome suggestions, feedback and bug reports! Similarly, it would be impossible to teach all the intricacies of this new paradigm all in one guide, on the first try. If you are missing some piece of information here or have suggestions on how to improve this content, also get in touch, or even better, click the "Edit on GitHub" button below and help us out. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/caching > Please thoroughly read the Important Defaults before reading this guide ## Basic Example This caching example illustrates the story and lifecycle of: * Query Instances with and without cache data * Background Refetching * Inactive Queries * Garbage Collection Let's assume we are using the default gcTime of **5 minutes** and the default staleTime of 0. * A new instance of useQuery({ queryKey: \['todos'\], queryFn: fetchTodos }) mounts. * Since no other queries have been made with the \['todos'\] query key, this query will show a hard loading state and make a network request to fetch the data. * When the network request has completed, the returned data will be cached under the \['todos'\] key. * The hook will mark the data as stale after the configured staleTime (defaults to 0, or immediately). * A second instance of useQuery({ queryKey: \['todos'\], queryFn: fetchTodos }) mounts elsewhere. * Since the cache already has data for the \['todos'\] key from the first query, that data is immediately returned from the cache. * The new instance triggers a new network request using its query function. * Note that regardless of whether both fetchTodos query functions are identical or not, both queries' status are updated (including isFetching, isPending, and other related values) because they have the same query key. * When the request completes successfully, the cache's data under the \['todos'\] key is updated with the new data, and both instances are updated with the new data. * Both instances of the useQuery({ queryKey: \['todos'\], queryFn: fetchTodos }) query are unmounted and no longer in use. * Since there are no more active instances of this query, a garbage collection timeout is set using gcTime to delete and garbage collect the query (defaults to **5 minutes**). * Before the cache timeout has completed, another instance of useQuery({ queryKey: \['todos'\], queryFn: fetchTodos }) mounts. The query immediately returns the available cached data while the fetchTodos function is being run in the background. When it completes successfully, it will populate the cache with fresh data. * The final instance of useQuery({ queryKey: \['todos'\], queryFn: fetchTodos }) unmounts. * No more instances of useQuery({ queryKey: \['todos'\], queryFn: fetchTodos }) appear within **5 minutes**. * The cached data under the \['todos'\] key is deleted and garbage collected. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/render-optimizations React Query applies a couple of optimizations automatically to ensure that your components only re-render when they actually need to. This is done by the following means: ## structural sharing React Query uses a technique called "structural sharing" to ensure that as many references as possible will be kept intact between re-renders. If data is fetched over the network, usually, you'll get a completely new reference by json parsing the response. However, React Query will keep the original reference if _nothing_ changed in the data. If a subset changed, React Query will keep the unchanged parts and only replace the changed parts. > Note: This optimization only works if the queryFn returns JSON compatible data. You can turn it off by setting structuralSharing: false globally or on a per-query basis, or you can implement your own structural sharing by passing a function to it. ### referential identity The top level object returned from useQuery, useInfiniteQuery, useMutation and the Array returned from useQueries is **not referentially stable**. It will be a new reference on every render. However, the data properties returned from these hooks will be as stable as possible. ## tracked properties React Query will only trigger a re-render if one of the properties returned from useQuery is actually "used". This is done by using custom getters. This avoids a lot of unnecessary re-renders, e.g. because properties like isFetching or isStale might change often, but are not used in the component. You can customize this feature by setting notifyOnChangeProps manually globally or on a per-query basis. If you want to turn that feature off, you can set notifyOnChangeProps: 'all'. > Note: Custom getters are invoked by accessing a property, either via destructuring or by accessing it directly. If you use object rest destructuring, you will disable this optimization. We have a lint rule to guard against this pitfall. ## select You can use the select option to select a subset of the data that your component should subscribe to. This is useful for highly optimized data transformations or to avoid unnecessary re-renders. js export const useTodos = (select) => { return useQuery({ queryKey: ['todos'], queryFn: fetchTodos, select, }) } export const useTodoCount = () => { return useTodos((data) => data.length) } export const useTodos = (select) => { return useQuery({ queryKey: ['todos'], queryFn: fetchTodos, select, }) } export const useTodoCount = () => { return useTodos((data) => data.length) } A component using the useTodoCount custom hook will only re-render if the length of the todos changes. It will **not** re-render if e.g. the name of a todo changed. > Note: select operates on successfully cached data and is not the appropriate place to throw errors. The source of truth for errors is the queryFn, and a select function that returns an error results in data being undefined and isSuccess being true. We recommend handling errors in the queryFn if you wish to have a query fail on incorrect data, or outside of the query hook if you have a error case not related to caching. ### memoization The select function will only re-run if: * the select function itself changed referentially * data changed This means that an inlined select function, as shown above, will run on every render. To avoid this, you can wrap the select function in useCallback, or extract it to a stable function reference if it doesn't have any dependencies: js // wrapped in useCallback export const useTodoCount = () => { return useTodos(useCallback((data) => data.length, [])) } // wrapped in useCallback export const useTodoCount = () => { return useTodos(useCallback((data) => data.length, [])) } js // extracted to a stable function reference const selectTodoCount = (data) => data.length export const useTodoCount = () => { return useTodos(selectTodoCount) } // extracted to a stable function reference const selectTodoCount = (data) => data.length export const useTodoCount = () => { return useTodos(selectTodoCount) } ## Further Reading For an in-depth guide about these topics, read React Query Render Optimizations from the Community Resources. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/default-query-function If you find yourself wishing for whatever reason that you could just share the same query function for your entire app and just use query keys to identify what it should fetch, you can do that by providing a **default query function** to TanStack Query: tsx // Define a default query function that will receive the query key const defaultQueryFn = async ({ queryKey }) => { const { data } = await axios.get( `https://jsonplaceholder.typicode.com${queryKey[0]}`, ) return data } // provide the default query function to your app with defaultOptions const queryClient = new QueryClient({ defaultOptions: { queries: { queryFn: defaultQueryFn, }, }, }) function App() { return ( <QueryClientProvider client={queryClient}> <YourApp /> </QueryClientProvider> ) } // All you have to do now is pass a key! function Posts() { const { status, data, error, isFetching } = useQuery({ queryKey: ['/posts'] }) // ... } // You can even leave out the queryFn and just go straight into options function Post({ postId }) { const { status, data, error, isFetching } = useQuery({ queryKey: [`/posts/${postId}`], enabled: !!postId, }) // ... } // Define a default query function that will receive the query key const defaultQueryFn = async ({ queryKey }) => { const { data } = await axios.get( `https://jsonplaceholder.typicode.com${queryKey[0]}`, ) return data } // provide the default query function to your app with defaultOptions const queryClient = new QueryClient({ defaultOptions: { queries: { queryFn: defaultQueryFn, }, }, }) function App() { return ( <QueryClientProvider client={queryClient}> <YourApp /> </QueryClientProvider> ) } // All you have to do now is pass a key! function Posts() { const { status, data, error, isFetching } = useQuery({ queryKey: ['/posts'] }) // ... } // You can even leave out the queryFn and just go straight into options function Post({ postId }) { const { status, data, error, isFetching } = useQuery({ queryKey: [`/posts/${postId}`], enabled: !!postId, }) // ... } If you ever want to override the default queryFn, you can just provide your own like you normally would. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/suspense React Query can also be used with React's Suspense for Data Fetching APIs. For this, we have dedicated hooks: * useSuspenseQuery * useSuspenseInfiniteQuery * useSuspenseQueries * Additionally, you can use the useQuery().promise and React.use() (Experimental) When using suspense mode, status states and error objects are not needed and are then replaced by usage of the React.Suspense component (including the use of the fallback prop and React error boundaries for catching errors). Please read the Resetting Error Boundaries and look at the Suspense Example for more information on how to set up suspense mode. If you want mutations to propagate errors to the nearest error boundary (similar to queries), you can set the throwOnError option to true as well. Enabling suspense mode for a query: tsx import { useSuspenseQuery } from '@tanstack/react-query' const { data } = useSuspenseQuery({ queryKey, queryFn }) import { useSuspenseQuery } from '@tanstack/react-query' const { data } = useSuspenseQuery({ queryKey, queryFn }) This works nicely in TypeScript, because data is guaranteed to be defined (as errors and loading states are handled by Suspense- and ErrorBoundaries). On the flip side, you therefore can't conditionally enable / disable the Query. This generally shouldn't be necessary for dependent Queries because with suspense, all your Queries inside one component are fetched in serial. placeholderData also doesn't exist for this Query. To prevent the UI from being replaced by a fallback during an update, wrap your updates that change the QueryKey into startTransition. ### throwOnError default Not all errors are thrown to the nearest Error Boundary per default - we're only throwing errors if there is no other data to show. That means if a Query ever successfully got data in the cache, the component will render, even if data is stale. Thus, the default for throwOnError is: throwOnError: (error, query) => typeof query.state.data === 'undefined' throwOnError: (error, query) => typeof query.state.data === 'undefined' Since you can't change throwOnError (because it would allow for data to become potentially undefined), you have to throw errors manually if you want all errors to be handled by Error Boundaries: tsx import { useSuspenseQuery } from '@tanstack/react-query' const { data, error, isFetching } = useSuspenseQuery({ queryKey, queryFn }) if (error && !isFetching) { throw error } // continue rendering data import { useSuspenseQuery } from '@tanstack/react-query' const { data, error, isFetching } = useSuspenseQuery({ queryKey, queryFn }) if (error && !isFetching) { throw error } // continue rendering data ## Resetting Error Boundaries Whether you are using **suspense** or **throwOnError** in your queries, you will need a way to let queries know that you want to try again when re-rendering after some error occurred. Query errors can be reset with the QueryErrorResetBoundary component or with the useQueryErrorResetBoundary hook. When using the component it will reset any query errors within the boundaries of the component: tsx import { QueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => ( <QueryErrorResetBoundary> {({ reset }) => ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> There was an error! <Button onClick={() => resetErrorBoundary()}>Try again</Button> </div> )} > <Page /> </ErrorBoundary> )} </QueryErrorResetBoundary> ) import { QueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => ( <QueryErrorResetBoundary> {({ reset }) => ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> There was an error! <Button onClick={() => resetErrorBoundary()}>Try again</Button> </div> )} > <Page /> </ErrorBoundary> )} </QueryErrorResetBoundary> ) When using the hook it will reset any query errors within the closest QueryErrorResetBoundary. If there is no boundary defined it will reset them globally: tsx import { useQueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => { const { reset } = useQueryErrorResetBoundary() return ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> There was an error! <Button onClick={() => resetErrorBoundary()}>Try again</Button> </div> )} > <Page /> </ErrorBoundary> ) } import { useQueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => { const { reset } = useQueryErrorResetBoundary() return ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> There was an error! <Button onClick={() => resetErrorBoundary()}>Try again</Button> </div> )} > <Page /> </ErrorBoundary> ) } ## Fetch-on-render vs Render-as-you-fetch Out of the box, React Query in suspense mode works really well as a **Fetch-on-render** solution with no additional configuration. This means that when your components attempt to mount, they will trigger query fetching and suspend, but only once you have imported them and mounted them. If you want to take it to the next level and implement a **Render-as-you-fetch** model, we recommend implementing Prefetching on routing callbacks and/or user interactions events to start loading queries before they are mounted and hopefully even before you start importing or mounting their parent components. ## Suspense on the Server with streaming If you are using NextJs, you can use our **experimental** integration for Suspense on the Server: @tanstack/react-query-next-experimental. This package will allow you to fetch data on the server (in a client component) by just calling useSuspenseQuery in your component. Results will then be streamed from the server to the client as SuspenseBoundaries resolve. To achieve this, wrap your app in the ReactQueryStreamedHydration component: tsx // app/providers.tsx 'use client' import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import * as React from 'react' import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export function Providers(props: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}> <ReactQueryStreamedHydration> {props.children} </ReactQueryStreamedHydration> </QueryClientProvider> ) } // app/providers.tsx 'use client' import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import * as React from 'react' import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export function Providers(props: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}> <ReactQueryStreamedHydration> {props.children} </ReactQueryStreamedHydration> </QueryClientProvider> ) } For more information, check out the NextJs Suspense Streaming Example and the Advanced Rendering & Hydration guide. ## Using useQuery().promise and React.use() (Experimental) > To enable this feature, you need to set the experimental\_prefetchInRender option to true when creating your QueryClient **Example code:** tsx const queryClient = new QueryClient({ defaultOptions: { queries: { experimental_prefetchInRender: true, }, }, }) const queryClient = new QueryClient({ defaultOptions: { queries: { experimental_prefetchInRender: true, }, }, }) **Usage:** tsx import React from 'react' import { useQuery } from '@tanstack/react-query' import { fetchTodos, type Todo } from './api' function TodoList({ query }: { query: UseQueryResult<Todo[]> }) { const data = React.use(query.promise) return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) } export function App() { const query = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) return ( <> <h1>Todos</h1> <React.Suspense fallback={<div>Loading...</div>}> <TodoList query={query} /> </React.Suspense> </> ) } import React from 'react' import { useQuery } from '@tanstack/react-query' import { fetchTodos, type Todo } from './api' function TodoList({ query }: { query: UseQueryResult<Todo[]> }) { const data = React.use(query.promise) return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) } export function App() { const query = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) return ( <> <h1>Todos</h1> <React.Suspense fallback={<div>Loading...</div>}> <TodoList query={query} /> </React.Suspense> </> ) } --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/network-mode/#network-mode-online TanStack Query provides three different network modes to distinguish how Queries and Mutations should behave if you have no network connection. This mode can be set for each Query / Mutation individually, or globally via the query / mutation defaults. Since TanStack Query is most often used for data fetching in combination with data fetching libraries, the default network mode is online. ## Network Mode: online In this mode, Queries and Mutations will not fire unless you have network connection. This is the default mode. If a fetch is initiated for a query, it will always stay in the state (pending, error, success) it is in if the fetch cannot be made because there is no network connection. However, a fetchStatus is exposed additionally. This can be either: * fetching: The queryFn is really executing - a request is in-flight. * paused: The query is not executing - it is paused until you have connection again * idle: The query is not fetching and not paused The flags isFetching and isPaused are derived from this state and exposed for convenience. > Keep in mind that it might not be enough to check for pending state to show a loading spinner. Queries can be in state: 'pending', but fetchStatus: 'paused' if they are mounting for the first time, and you have no network connection. If a query runs because you are online, but you go offline while the fetch is still happening, TanStack Query will also pause the retry mechanism. Paused queries will then continue to run once you re-gain network connection. This is independent of refetchOnReconnect (which also defaults to true in this mode), because it is not a refetch, but rather a continue. If the query has been cancelled in the meantime, it will not continue. ## Network Mode: always In this mode, TanStack Query will always fetch and ignore the online / offline state. This is likely the mode you want to choose if you use TanStack Query in an environment where you don't need an active network connection for your Queries to work - e.g. if you just read from AsyncStorage, or if you just want to return Promise.resolve(5) from your queryFn. * Queries will never be paused because you have no network connection. * Retries will also not pause - your Query will go to error state if it fails. * refetchOnReconnect defaults to false in this mode, because reconnecting to the network is not a good indicator anymore that stale queries should be refetched. You can still turn it on if you want. ## Network Mode: offlineFirst This mode is the middle ground between the first two options, where TanStack Query will run the queryFn once, but then pause retries. This is very handy if you have a serviceWorker that intercepts a request for caching like in an offline-first PWA, or if you use HTTP caching via the Cache-Control header. In those situations, the first fetch might succeed because it comes from an offline storage / cache. However, if there is a cache miss, the network request will go out and fail, in which case this mode behaves like an online query - pausing retries. ## Devtools The TanStack Query Devtools will show Queries in a paused state if they would be fetching, but there is no network connection. There is also a toggle button to _Mock offline behavior_. Please note that this button will _not_ actually mess with your network connection (you can do that in the browser devtools), but it will set the OnlineManager in an offline state. ## Signature * networkMode: 'online' | 'always' | 'offlineFirst' * optional * defaults to 'online' --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/disabling-queries/#typesafe-disabling-of-queries-using-skiptoken If you ever want to disable a query from automatically running, you can use the enabled = false option. The enabled option also accepts a callback that returns a boolean. When enabled is false: * If the query has cached data, then the query will be initialized in the status === 'success' or isSuccess state. * If the query does not have cached data, then the query will start in the status === 'pending' and fetchStatus === 'idle' state. * The query will not automatically fetch on mount. * The query will not automatically refetch in the background. * The query will ignore query client invalidateQueries and refetchQueries calls that would normally result in the query refetching. * refetch returned from useQuery can be used to manually trigger the query to fetch. However, it will not work with skipToken. > TypeScript users may prefer to use skipToken as an alternative to enabled = false. tsx function Todos() { const { isLoading, isError, data, error, refetch, isFetching } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, enabled: false, }) return ( <div> <button onClick={() => refetch()}>Fetch Todos</button> {data ? ( <> <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> </> ) : isError ? ( <span>Error: {error.message}</span> ) : isLoading ? ( <span>Loading...</span> ) : ( <span>Not ready ...</span> )} <div>{isFetching ? 'Fetching...' : null}</div> </div> ) } function Todos() { const { isLoading, isError, data, error, refetch, isFetching } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, enabled: false, }) return ( <div> <button onClick={() => refetch()}>Fetch Todos</button> {data ? ( <> <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> </> ) : isError ? ( <span>Error: {error.message}</span> ) : isLoading ? ( <span>Loading...</span> ) : ( <span>Not ready ...</span> )} <div>{isFetching ? 'Fetching...' : null}</div> </div> ) } Permanently disabling a query opts out of many great features that TanStack Query has to offer (like background refetches), and it's also not the idiomatic way. It takes you from the declarative approach (defining dependencies when your query should run) into an imperative mode (fetch whenever I click here). It is also not possible to pass parameters to refetch. Oftentimes, all you want is a lazy query that defers the initial fetch: ## Lazy Queries The enabled option can not only be used to permanently disable a query, but also to enable / disable it at a later time. A good example would be a filter form where you only want to fire off the first request once the user has entered a filter value: tsx function Todos() { const [filter, setFilter] = React.useState('') const { data } = useQuery({ queryKey: ['todos', filter], queryFn: () => fetchTodos(filter), // ⬇️ disabled as long as the filter is empty enabled: !!filter, }) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data} />} </div> ) } function Todos() { const [filter, setFilter] = React.useState('') const { data } = useQuery({ queryKey: ['todos', filter], queryFn: () => fetchTodos(filter), // ⬇️ disabled as long as the filter is empty enabled: !!filter, }) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data} />} </div> ) } ### isLoading (Previously: isInitialLoading) Lazy queries will be in status: 'pending' right from the start because pending means that there is no data yet. This is technically true, however, since we are not currently fetching any data (as the query is not _enabled_), it also means you likely cannot use this flag to show a loading spinner. If you are using disabled or lazy queries, you can use the isLoading flag instead. It's a derived flag that is computed from: isPending && isFetching so it will only be true if the query is currently fetching for the first time. ## Typesafe disabling of queries using skipToken If you are using TypeScript, you can use the skipToken to disable a query. This is useful when you want to disable a query based on a condition, but you still want to keep the query to be type safe. > IMPORTANT: refetch from useQuery will not work with skipToken. Other than that, skipToken works the same as enabled: false. tsx import { skipToken, useQuery } from '@tanstack/react-query' function Todos() { const [filter, setFilter] = React.useState<string | undefined>() const { data } = useQuery({ queryKey: ['todos', filter], // ⬇️ disabled as long as the filter is undefined or empty queryFn: filter ? () => fetchTodos(filter) : skipToken, }) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data} />} </div> ) } import { skipToken, useQuery } from '@tanstack/react-query' function Todos() { const [filter, setFilter] = React.useState<string | undefined>() const { data } = useQuery({ queryKey: ['todos', filter], // ⬇️ disabled as long as the filter is undefined or empty queryFn: filter ? () => fetchTodos(filter) : skipToken, }) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data} />} </div> ) } --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/ssr/#full-nextjs-pages-router-example In this guide you'll learn how to use React Query with server rendering. See the guide on Prefetching & Router Integration for some background. You might also want to check out the Performance & Request Waterfalls guide before that. For advanced server rendering patterns, such as streaming, Server Components and the new Next.js app router, see the Advanced Server Rendering guide. If you just want to see some code, you can skip ahead to the Full Next.js pages router example or the Full Remix example below. ## Server Rendering & React Query So what is server rendering anyway? The rest of this guide will assume you are familiar with the concept, but let's spend some time to look at how it relates to React Query. Server rendering is the act of generating the initial html on the server, so that the user has some content to look at as soon as the page loads. This can happen on demand when a page is requested (SSR). It can also happen ahead of time either because a previous request was cached, or at build time (SSG). If you've read the Request Waterfalls guide, you might remember this: 1. |-> Markup (without content) 2. |-> JS 3. |-> Query 1. |-> Markup (without content) 2. |-> JS 3. |-> Query With a client rendered application, these are the minimum 3 server roundtrips you will need to make before getting any content on the screen for the user. One way of viewing server rendering is that it turns the above into this: 1. |-> Markup (with content AND initial data) 2. |-> JS 1. |-> Markup (with content AND initial data) 2. |-> JS As soon as **1.** is complete, the user can see the content and when **2.** finishes, the page is interactive and clickable. Because the markup also contains the initial data we need, step **3.** does not need to run on the client at all, at least until you want to revalidate the data for some reason. This is all from the clients perspective. On the server, we need to **prefetch** that data before we generate/render the markup, we need to **dehydrate** that data into a serializable format we can embed in the markup, and on the client we need to **hydrate** that data into a React Query cache so we can avoid doing a new fetch on the client. Read on to learn how to implement these three steps with React Query. ## A quick note on Suspense This guide uses the regular useQuery API. While we don't necessarily recommend it, it is possible to replace this with useSuspenseQuery instead **as long as you always prefetch all your queries**. The upside is that you get to use <Suspense> for loading states on the client. If you do forget to prefetch a query when you are using useSuspenseQuery, the consequences will depend on the framework you are using. In some cases, the data will Suspend and get fetched on the server but never be hydrated to the client, where it will fetch again. In these cases you will get a markup hydration mismatch, because the server and the client tried to render different things. ## Initial setup The first steps of using React Query is always to create a queryClient and wrap the application in a <QueryClientProvider>. When doing server rendering, it's important to create the queryClient instance **inside of your app**, in React state (an instance ref works fine too). **This ensures that data is not shared between different users and requests**, while still only creating the queryClient once per component lifecycle. Next.js pages router: tsx // _app.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' // NEVER DO THIS: // const queryClient = new QueryClient() // // Creating the queryClient at the file root level makes the cache shared // between all requests and means _all_ data gets passed to _all_ users. // Besides being bad for performance, this also leaks any sensitive data. export default function MyApp({ Component, pageProps }) { // Instead do this, which ensures each request has its own cache: const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> ) } // _app.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' // NEVER DO THIS: // const queryClient = new QueryClient() // // Creating the queryClient at the file root level makes the cache shared // between all requests and means _all_ data gets passed to _all_ users. // Besides being bad for performance, this also leaks any sensitive data. export default function MyApp({ Component, pageProps }) { // Instead do this, which ensures each request has its own cache: const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> ) } Remix: tsx // app/root.tsx import { Outlet } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp() { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Outlet /> </QueryClientProvider> ) } // app/root.tsx import { Outlet } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp() { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Outlet /> </QueryClientProvider> ) } ## Get started fast with initialData The quickest way to get started is to not involve React Query at all when it comes to prefetching and not use the dehydrate/hydrate APIs. What you do instead is passing the raw data in as the initialData option to useQuery. Let's look at an example using Next.js pages router, using getServerSideProps. tsx export async function getServerSideProps() { const posts = await getPosts() return { props: { posts } } } function Posts(props) { const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: props.posts, }) // ... } export async function getServerSideProps() { const posts = await getPosts() return { props: { posts } } } function Posts(props) { const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: props.posts, }) // ... } This also works with getStaticProps or even the older getInitialProps and the same pattern can be applied in any other framework that has equivalent functions. This is what the same example looks like with Remix: tsx export async function loader() { const posts = await getPosts() return json({ posts }) } function Posts() { const { posts } = useLoaderData<typeof loader>() const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: posts, }) // ... } export async function loader() { const posts = await getPosts() return json({ posts }) } function Posts() { const { posts } = useLoaderData<typeof loader>() const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: posts, }) // ... } The setup is minimal and this can be a quick solution for some cases, but there are a **few tradeoffs to consider** when compared to the full approach: * If you are calling useQuery in a component deeper down in the tree you need to pass the initialData down to that point * If you are calling useQuery with the same query in multiple locations, passing initialData to only one of them can be brittle and break when your app changes since. If you remove or move the component that has the useQuery with initialData, the more deeply nested useQuery might no longer have any data. Passing initialData to **all** queries that needs it can also be cumbersome. * There is no way to know at what time the query was fetched on the server, so dataUpdatedAt and determining if the query needs refetching is based on when the page loaded instead * If there is already data in the cache for a query, initialData will never overwrite this data, **even if the new data is fresher than the old one**. * To understand why this is especially bad, consider the getServerSideProps example above. If you navigate back and forth to a page several times, getServerSideProps would get called each time and fetch new data, but because we are using the initialData option, the client cache and data would never be updated. Setting up the full hydration solution is straightforward and does not have these drawbacks, this will be the focus for the rest of the documentation. ## Using the Hydration APIs With just a little more setup, you can use a queryClient to prefetch queries during a preload phase, pass a serialized version of that queryClient to the rendering part of the app and reuse it there. This avoids the drawbacks above. Feel free to skip ahead for full Next.js pages router and Remix examples, but at a general level these are the extra steps: * In the framework loader function, create a const queryClient = new QueryClient(options) * In the loader function, do await queryClient.prefetchQuery(...) for each query you want to prefetch * You want to use await Promise.all(...) to fetch the queries in parallel when possible * It's fine to have queries that aren't prefetched. These wont be server rendered, instead they will be fetched on the client after the application is interactive. This can be great for content that are shown only after user interaction, or is far down on the page to avoid blocking more critical content. * From the loader, return dehydrate(queryClient), note that the exact syntax to return this differs between frameworks * Wrap your tree with <HydrationBoundary state={dehydratedState}> where dehydratedState comes from the framework loader. How you get dehydratedState also differs between frameworks. * This can be done for each route, or at the top of the application to avoid boilerplate, see examples > An interesting detail is that there are actually _three_ queryClients involved. The framework loaders are a form of "preloading" phase that happens before rendering, and this phase has its own queryClient that does the prefetching. The dehydrated result of this phase gets passed to **both** the server rendering process **and** the client rendering process which each has its own queryClient. This ensures they both start with the same data so they can return the same markup. > Server Components are another form of "preloading" phase, that can also "preload" (pre-render) parts of a React component tree. Read more in the Advanced Server Rendering guide. ### Full Next.js pages router example > For app router documentation, see the Advanced Server Rendering guide. Initial setup: tsx // _app.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> ) } // _app.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> ) } In each route: tsx // pages/posts.tsx import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' // This could also be getServerSideProps export async function getStaticProps() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return { props: { dehydratedState: dehydrate(queryClient), }, } } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } // pages/posts.tsx import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' // This could also be getServerSideProps export async function getStaticProps() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return { props: { dehydratedState: dehydrate(queryClient), }, } } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } ### Full Remix example Initial setup: tsx // app/root.tsx import { Outlet } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp() { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Outlet /> </QueryClientProvider> ) } // app/root.tsx import { Outlet } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp() { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Outlet /> </QueryClientProvider> ) } In each route, note that it's fine to do this in nested routes too: tsx // app/routes/posts.tsx import { json } from '@remix-run/node' import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' export async function loader() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return json({ dehydratedState: dehydrate(queryClient) }) } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute() { const { dehydratedState } = useLoaderData<typeof loader>() return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } // app/routes/posts.tsx import { json } from '@remix-run/node' import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' export async function loader() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return json({ dehydratedState: dehydrate(queryClient) }) } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute() { const { dehydratedState } = useLoaderData<typeof loader>() return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } ## Optional - Remove boilerplate Having this part in every route might seem like a lot of boilerplate: tsx export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } While there is nothing wrong with this approach, if you want to get rid of this boilerplate, here's how you can modify your setup in Next.js: tsx // _app.tsx import { HydrationBoundary, QueryClient, QueryClientProvider, } from '@tanstack/react-query' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState(() => new QueryClient()) return ( <QueryClientProvider client={queryClient}> <HydrationBoundary state={pageProps.dehydratedState}> <Component {...pageProps} /> </HydrationBoundary> </QueryClientProvider> ) } // pages/posts.tsx // Remove PostsRoute with the HydrationBoundary and instead export Posts directly: export default function Posts() { ... } // _app.tsx import { HydrationBoundary, QueryClient, QueryClientProvider, } from '@tanstack/react-query' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState(() => new QueryClient()) return ( <QueryClientProvider client={queryClient}> <HydrationBoundary state={pageProps.dehydratedState}> <Component {...pageProps} /> </HydrationBoundary> </QueryClientProvider> ) } // pages/posts.tsx // Remove PostsRoute with the HydrationBoundary and instead export Posts directly: export default function Posts() { ... } With Remix, this is a little bit more involved, we recommend checking out the use-dehydrated-state package. ## Prefetching dependent queries Over in the Prefetching guide we learned how to prefetch dependent queries, but how do we do this in framework loaders? Consider the following code, taken from the Dependent Queries guide: tsx // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) How would we prefetch this so it can be server rendered? Here's an example: tsx // For Remix, rename this to loader instead export async function getServerSideProps() { const queryClient = new QueryClient() const user = await queryClient.fetchQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) if (user?.userId) { await queryClient.prefetchQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, }) } // For Remix: // return json({ dehydratedState: dehydrate(queryClient) }) return { props: { dehydratedState: dehydrate(queryClient) } } } // For Remix, rename this to loader instead export async function getServerSideProps() { const queryClient = new QueryClient() const user = await queryClient.fetchQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) if (user?.userId) { await queryClient.prefetchQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, }) } // For Remix: // return json({ dehydratedState: dehydrate(queryClient) }) return { props: { dehydratedState: dehydrate(queryClient) } } } This can get more complex of course, but since these loader functions are just JavaScript, you can use the full power of the language to build your logic. Make sure you prefetch all queries that you want to be server rendered. ## Error handling React Query defaults to a graceful degradation strategy. This means: * queryClient.prefetchQuery(...) never throws errors * dehydrate(...) only includes successful queries, not failed ones This will lead to any failed queries being retried on the client and that the server rendered output will include loading states instead of the full content. While a good default, sometimes this is not what you want. When critical content is missing, you might want to respond with a 404 or 500 status code depending on the situation. For these cases, use queryClient.fetchQuery(...) instead, which will throw errors when it fails, letting you handle things in a suitable way. tsx let result try { result = await queryClient.fetchQuery(...) } catch (error) { // Handle the error, refer to your framework documentation } // You might also want to check and handle any invalid `result` here let result try { result = await queryClient.fetchQuery(...) } catch (error) { // Handle the error, refer to your framework documentation } // You might also want to check and handle any invalid `result` here If you for some reason want to include failed queries in the dehydrated state to avoid retries, you can use the option shouldDehydrateQuery to override the default function and implement your own logic: tsx dehydrate(queryClient, { shouldDehydrateQuery: (query) => { // This will include all queries, including failed ones, // but you can also implement your own logic by inspecting `query` return true }, }) dehydrate(queryClient, { shouldDehydrateQuery: (query) => { // This will include all queries, including failed ones, // but you can also implement your own logic by inspecting `query` return true }, }) ## Serialization When doing return { props: { dehydratedState: dehydrate(queryClient) } } in Next.js, or return json({ dehydratedState: dehydrate(queryClient) }) in Remix, what happens is that the dehydratedState representation of the queryClient is serialized by the framework so it can be embedded into the markup and transported to the client. By default, these frameworks only supports returning things that are safely serializable/parsable, and therefore does not support undefined, Error, Date, Map, Set, BigInt, Infinity, NaN, \-0, regular expressions etc. This also means that you can not return any of these things from your queries. If returning these values is something you want, check out superjson or similar packages. If you are using a custom SSR setup, you need to take care of this step yourself. Your first instinct might be to use JSON.stringify(dehydratedState), but because this doesn't escape things like <script>alert('Oh no..')</script> by default, this can easily lead to **XSS-vulnerabilities** in your application. superjson also **does not** escape values and is unsafe to use by itself in a custom SSR setup (unless you add an extra step for escaping the output). Instead we recommend using a library like Serialize JavaScript or devalue which are both safe against XSS injections out of the box. ## A note about request waterfalls In the Performance & Request Waterfalls guide we mentioned we would revisit how server rendering changes one of the more complex nested waterfalls. Check back for the specific code example, but as a refresher, we have a code split <GraphFeedItem> component inside a <Feed> component. This only renders if the feed contains a graph item and both of these components fetches their own data. With client rendering, this leads to the following request waterfall: 1. |> Markup (without content) 2. |> JS for <Feed> 3. |> getFeed() 4. |> JS for <GraphFeedItem> 5. |> getGraphDataById() 1. |> Markup (without content) 2. |> JS for <Feed> 3. |> getFeed() 4. |> JS for <GraphFeedItem> 5. |> getGraphDataById() The nice thing about server rendering is that we can turn the above into: 1. |> Markup (with content AND initial data) 2. |> JS for <Feed> 2. |> JS for <GraphFeedItem> 1. |> Markup (with content AND initial data) 2. |> JS for <Feed> 2. |> JS for <GraphFeedItem> Note that the queries are no longer fetched on the client, instead their data was included in the markup. The reason we can now load the JS in parallel is that since <GraphFeedItem> was rendered on the server we know that we are going to need this JS on the client as well and can insert a script-tag for this chunk in the markup. On the server, we would still have this request waterfall: 1. |> getFeed() 2. |> getGraphDataById() 1. |> getFeed() 2. |> getGraphDataById() We simply can not know before we have fetched the feed if we also need to fetch graph data, they are dependent queries. Because this happens on the server where latency is generally both lower and more stable, this often isn't such a big deal. Amazing, we've mostly flattened our waterfalls! There's a catch though. Let's call this page the /feed page, and let's pretend we also have another page like /posts. If we type in www.example.com/feed directly in the url bar and hit enter, we get all these great server rendering benefits, BUT, if we instead type in www.example.com/posts and then **click a link** to /feed, we're back to to this: 1. |> JS for <Feed> 2. |> getFeed() 3. |> JS for <GraphFeedItem> 4. |> getGraphDataById() 1. |> JS for <Feed> 2. |> getFeed() 3. |> JS for <GraphFeedItem> 4. |> getGraphDataById() This is because with SPA's, server rendering only works for the initial page load, not for any subsequent navigation. Modern frameworks often try to solve this by fetching the initial code and data in parallel, so if you were using Next.js or Remix with the prefetching patterns we outlined in this guide, including how to prefetch dependent queries, it would actually look like this instead: 1. |> JS for <Feed> 1. |> getFeed() + getGraphDataById() 2. |> JS for <GraphFeedItem> 1. |> JS for <Feed> 1. |> getFeed() + getGraphDataById() 2. |> JS for <GraphFeedItem> This is much better, but if we want to improve this further we can flatten this to a single roundtrip with Server Components. Learn how in the Advanced Server Rendering guide. ## Tips, Tricks and Caveats ### Staleness is measured from when the query was fetched on the server A query is considered stale depending on when it was dataUpdatedAt. A caveat here is that the server needs to have the correct time for this to work properly, but UTC time is used, so timezones do not factor into this. Because staleTime defaults to 0, queries will be refetched in the background on page load by default. You might want to use a higher staleTime to avoid this double fetching, especially if you don't cache your markup. This refetching of stale queries is a perfect match when caching markup in a CDN! You can set the cache time of the page itself decently high to avoid having to re-render pages on the server, but configure the staleTime of the queries lower to make sure data is refetched in the background as soon as a user visits the page. Maybe you want to cache the pages for a week, but refetch the data automatically on page load if it's older than a day? ### High memory consumption on server In case you are creating the QueryClient for every request, React Query creates the isolated cache for this client, which is preserved in memory for the gcTime period. That may lead to high memory consumption on server in case of high number of requests during that period. On the server, gcTime defaults to Infinity which disables manual garbage collection and will automatically clear memory once a request has finished. If you are explicitly setting a non-Infinity gcTime then you will be responsible for clearing the cache early. Avoid setting gcTime to 0 as it may result in a hydration error. This occurs because the Hydration Boundary places necessary data into the cache for rendering, but if the garbage collector removes the data before the rendering completes, issues may arise. If you require a shorter gcTime, we recommend setting it to 2 \* 1000 to allow sufficient time for the app to reference the data. To clear the cache after it is not needed and to lower memory consumption, you can add a call to queryClient.clear() after the request is handled and dehydrated state has been sent to the client. Alternatively, you can set a smaller gcTime. ### Caveat for Next.js rewrites There's a catch if you're using Next.js' rewrites feature together with Automatic Static Optimization or getStaticProps: It will cause a second hydration by React Query. That's because Next.js needs to ensure that they parse the rewrites on the client and collect any params after hydration so that they can be provided in router.query. The result is missing referential equality for all the hydration data, which for example triggers wherever your data is used as props of components or in the dependency array of useEffects/useMemos. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/suspense/#resetting-error-boundaries React Query can also be used with React's Suspense for Data Fetching APIs. For this, we have dedicated hooks: * useSuspenseQuery * useSuspenseInfiniteQuery * useSuspenseQueries * Additionally, you can use the useQuery().promise and React.use() (Experimental) When using suspense mode, status states and error objects are not needed and are then replaced by usage of the React.Suspense component (including the use of the fallback prop and React error boundaries for catching errors). Please read the Resetting Error Boundaries and look at the Suspense Example for more information on how to set up suspense mode. If you want mutations to propagate errors to the nearest error boundary (similar to queries), you can set the throwOnError option to true as well. Enabling suspense mode for a query: tsx import { useSuspenseQuery } from '@tanstack/react-query' const { data } = useSuspenseQuery({ queryKey, queryFn }) import { useSuspenseQuery } from '@tanstack/react-query' const { data } = useSuspenseQuery({ queryKey, queryFn }) This works nicely in TypeScript, because data is guaranteed to be defined (as errors and loading states are handled by Suspense- and ErrorBoundaries). On the flip side, you therefore can't conditionally enable / disable the Query. This generally shouldn't be necessary for dependent Queries because with suspense, all your Queries inside one component are fetched in serial. placeholderData also doesn't exist for this Query. To prevent the UI from being replaced by a fallback during an update, wrap your updates that change the QueryKey into startTransition. ### throwOnError default Not all errors are thrown to the nearest Error Boundary per default - we're only throwing errors if there is no other data to show. That means if a Query ever successfully got data in the cache, the component will render, even if data is stale. Thus, the default for throwOnError is: throwOnError: (error, query) => typeof query.state.data === 'undefined' throwOnError: (error, query) => typeof query.state.data === 'undefined' Since you can't change throwOnError (because it would allow for data to become potentially undefined), you have to throw errors manually if you want all errors to be handled by Error Boundaries: tsx import { useSuspenseQuery } from '@tanstack/react-query' const { data, error, isFetching } = useSuspenseQuery({ queryKey, queryFn }) if (error && !isFetching) { throw error } // continue rendering data import { useSuspenseQuery } from '@tanstack/react-query' const { data, error, isFetching } = useSuspenseQuery({ queryKey, queryFn }) if (error && !isFetching) { throw error } // continue rendering data ## Resetting Error Boundaries Whether you are using **suspense** or **throwOnError** in your queries, you will need a way to let queries know that you want to try again when re-rendering after some error occurred. Query errors can be reset with the QueryErrorResetBoundary component or with the useQueryErrorResetBoundary hook. When using the component it will reset any query errors within the boundaries of the component: tsx import { QueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => ( <QueryErrorResetBoundary> {({ reset }) => ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> There was an error! <Button onClick={() => resetErrorBoundary()}>Try again</Button> </div> )} > <Page /> </ErrorBoundary> )} </QueryErrorResetBoundary> ) import { QueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => ( <QueryErrorResetBoundary> {({ reset }) => ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> There was an error! <Button onClick={() => resetErrorBoundary()}>Try again</Button> </div> )} > <Page /> </ErrorBoundary> )} </QueryErrorResetBoundary> ) When using the hook it will reset any query errors within the closest QueryErrorResetBoundary. If there is no boundary defined it will reset them globally: tsx import { useQueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => { const { reset } = useQueryErrorResetBoundary() return ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> There was an error! <Button onClick={() => resetErrorBoundary()}>Try again</Button> </div> )} > <Page /> </ErrorBoundary> ) } import { useQueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => { const { reset } = useQueryErrorResetBoundary() return ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> There was an error! <Button onClick={() => resetErrorBoundary()}>Try again</Button> </div> )} > <Page /> </ErrorBoundary> ) } ## Fetch-on-render vs Render-as-you-fetch Out of the box, React Query in suspense mode works really well as a **Fetch-on-render** solution with no additional configuration. This means that when your components attempt to mount, they will trigger query fetching and suspend, but only once you have imported them and mounted them. If you want to take it to the next level and implement a **Render-as-you-fetch** model, we recommend implementing Prefetching on routing callbacks and/or user interactions events to start loading queries before they are mounted and hopefully even before you start importing or mounting their parent components. ## Suspense on the Server with streaming If you are using NextJs, you can use our **experimental** integration for Suspense on the Server: @tanstack/react-query-next-experimental. This package will allow you to fetch data on the server (in a client component) by just calling useSuspenseQuery in your component. Results will then be streamed from the server to the client as SuspenseBoundaries resolve. To achieve this, wrap your app in the ReactQueryStreamedHydration component: tsx // app/providers.tsx 'use client' import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import * as React from 'react' import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export function Providers(props: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}> <ReactQueryStreamedHydration> {props.children} </ReactQueryStreamedHydration> </QueryClientProvider> ) } // app/providers.tsx 'use client' import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import * as React from 'react' import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export function Providers(props: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}> <ReactQueryStreamedHydration> {props.children} </ReactQueryStreamedHydration> </QueryClientProvider> ) } For more information, check out the NextJs Suspense Streaming Example and the Advanced Rendering & Hydration guide. ## Using useQuery().promise and React.use() (Experimental) > To enable this feature, you need to set the experimental\_prefetchInRender option to true when creating your QueryClient **Example code:** tsx const queryClient = new QueryClient({ defaultOptions: { queries: { experimental_prefetchInRender: true, }, }, }) const queryClient = new QueryClient({ defaultOptions: { queries: { experimental_prefetchInRender: true, }, }, }) **Usage:** tsx import React from 'react' import { useQuery } from '@tanstack/react-query' import { fetchTodos, type Todo } from './api' function TodoList({ query }: { query: UseQueryResult<Todo[]> }) { const data = React.use(query.promise) return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) } export function App() { const query = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) return ( <> <h1>Todos</h1> <React.Suspense fallback={<div>Loading...</div>}> <TodoList query={query} /> </React.Suspense> </> ) } import React from 'react' import { useQuery } from '@tanstack/react-query' import { fetchTodos, type Todo } from './api' function TodoList({ query }: { query: UseQueryResult<Todo[]> }) { const data = React.use(query.promise) return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) } export function App() { const query = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) return ( <> <h1>Todos</h1> <React.Suspense fallback={<div>Loading...</div>}> <TodoList query={query} /> </React.Suspense> </> ) } --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/important-defaults Out of the box, TanStack Query is configured with **aggressive but sane** defaults. **Sometimes these defaults can catch new users off guard or make learning/debugging difficult if they are unknown by the user.** Keep them in mind as you continue to learn and use TanStack Query: * Query instances via useQuery or useInfiniteQuery by default **consider cached data as stale**. > To change this behavior, you can configure your queries both globally and per-query using the staleTime option. Specifying a longer staleTime means queries will not refetch their data as often * Stale queries are refetched automatically in the background when: * New instances of the query mount * The window is refocused * The network is reconnected * The query is optionally configured with a refetch interval > To change this functionality, you can use options like refetchOnMount, refetchOnWindowFocus, refetchOnReconnect and refetchInterval. * Query results that have no more active instances of useQuery, useInfiniteQuery or query observers are labeled as "inactive" and remain in the cache in case they are used again at a later time. * By default, "inactive" queries are garbage collected after **5 minutes**. > To change this, you can alter the default gcTime for queries to something other than 1000 \* 60 \* 5 milliseconds. * Queries that fail are **silently retried 3 times, with exponential backoff delay** before capturing and displaying an error to the UI. > To change this, you can alter the default retry and retryDelay options for queries to something other than 3 and the default exponential backoff function. * Query results by default are **structurally shared to detect if data has actually changed** and if not, **the data reference remains unchanged** to better help with value stabilization with regards to useMemo and useCallback. If this concept sounds foreign, then don't worry about it! 99.9% of the time you will not need to disable this and it makes your app more performant at zero cost to you. > Structural sharing only works with JSON-compatible values, any other value types will always be considered as changed. If you are seeing performance issues because of large responses for example, you can disable this feature with the config.structuralSharing flag. If you are dealing with non-JSON compatible values in your query responses and still want to detect if data has changed or not, you can provide your own custom function as config.structuralSharing to compute a value from the old and new responses, retaining references as required. ## Further Reading Have a look at the following articles from our Community Resources for further explanations of the defaults: * Practical React Query * React Query as a State Manager --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/queries ## Query Basics A query is a declarative dependency on an asynchronous source of data that is tied to a **unique key**. A query can be used with any Promise based method (including GET and POST methods) to fetch data from a server. If your method modifies data on the server, we recommend using Mutations instead. To subscribe to a query in your components or custom hooks, call the useQuery hook with at least: * A **unique key for the query** * A function that returns a promise that: * Resolves the data, or * Throws an error tsx import { useQuery } from '@tanstack/react-query' function App() { const info = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList }) } import { useQuery } from '@tanstack/react-query' function App() { const info = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList }) } The **unique key** you provide is used internally for refetching, caching, and sharing your queries throughout your application. The query result returned by useQuery contains all of the information about the query that you'll need for templating and any other usage of the data: tsx const result = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList }) const result = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList }) The result object contains a few very important states you'll need to be aware of to be productive. A query can only be in one of the following states at any given moment: * isPending or status === 'pending' - The query has no data yet * isError or status === 'error' - The query encountered an error * isSuccess or status === 'success' - The query was successful and data is available Beyond those primary states, more information is available depending on the state of the query: * error - If the query is in an isError state, the error is available via the error property. * data - If the query is in an isSuccess state, the data is available via the data property. * isFetching - In any state, if the query is fetching at any time (including background refetching) isFetching will be true. For **most** queries, it's usually sufficient to check for the isPending state, then the isError state, then finally, assume that the data is available and render the successful state: tsx function Todos() { const { isPending, isError, data, error } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) if (isPending) { return <span>Loading...</span> } if (isError) { return <span>Error: {error.message}</span> } // We can assume by this point that `isSuccess === true` return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) } function Todos() { const { isPending, isError, data, error } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) if (isPending) { return <span>Loading...</span> } if (isError) { return <span>Error: {error.message}</span> } // We can assume by this point that `isSuccess === true` return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) } If booleans aren't your thing, you can always use the status state as well: tsx function Todos() { const { status, data, error } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) if (status === 'pending') { return <span>Loading...</span> } if (status === 'error') { return <span>Error: {error.message}</span> } // also status === 'success', but "else" logic works, too return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) } function Todos() { const { status, data, error } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) if (status === 'pending') { return <span>Loading...</span> } if (status === 'error') { return <span>Error: {error.message}</span> } // also status === 'success', but "else" logic works, too return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) } TypeScript will also narrow the type of data correctly if you've checked for pending and error before accessing it. ### FetchStatus In addition to the status field, you will also get an additional fetchStatus property with the following options: * fetchStatus === 'fetching' - The query is currently fetching. * fetchStatus === 'paused' - The query wanted to fetch, but it is paused. Read more about this in the Network Mode guide. * fetchStatus === 'idle' - The query is not doing anything at the moment. ### Why two different states? Background refetches and stale-while-revalidate logic make all combinations for status and fetchStatus possible. For example: * a query in success status will usually be in idle fetchStatus, but it could also be in fetching if a background refetch is happening. * a query that mounts and has no data will usually be in pending status and fetching fetchStatus, but it could also be paused if there is no network connection. So keep in mind that a query can be in pending state without actually fetching data. As a rule of thumb: * The status gives information about the data: Do we have any or not? * The fetchStatus gives information about the queryFn: Is it running or not? ## Further Reading For an alternative way of performing status checks, have a look at the Community Resources. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/query-keys At its core, TanStack Query manages query caching for you based on query keys. Query keys have to be an Array at the top level, and 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, and **unique to the query's data**, you can use it! ## Simple Query Keys The simplest form of a key is an array with constants values. This format is useful for: * Generic List/Index resources * Non-hierarchical resources tsx // A list of todos useQuery({ queryKey: ['todos'], ... }) // Something else, whatever! useQuery({ queryKey: ['something', 'special'], ... }) // A list of todos useQuery({ queryKey: ['todos'], ... }) // Something else, whatever! useQuery({ queryKey: ['something', 'special'], ... }) ## Array Keys with variables When a query needs more information to uniquely describe its data, you can use an array with a string and any number of serializable objects to describe it. This is useful for: * Hierarchical or nested resources * It's common to pass an ID, index, or other primitive to uniquely identify the item * Queries with additional parameters * It's common to pass an object of additional options tsx // An individual todo useQuery({ queryKey: ['todo', 5], ... }) // An individual todo in a "preview" format useQuery({ queryKey: ['todo', 5, { preview: true }], ...}) // A list of todos that are "done" useQuery({ queryKey: ['todos', { type: 'done' }], ... }) // An individual todo useQuery({ queryKey: ['todo', 5], ... }) // An individual todo in a "preview" format useQuery({ queryKey: ['todo', 5, { preview: true }], ...}) // A list of todos that are "done" useQuery({ queryKey: ['todos', { type: 'done' }], ... }) ## Query Keys are hashed deterministically! This means that no matter the order of keys in objects, all of the following queries are considered equal: tsx useQuery({ queryKey: ['todos', { status, page }], ... }) useQuery({ queryKey: ['todos', { page, status }], ...}) useQuery({ queryKey: ['todos', { page, status, other: undefined }], ... }) useQuery({ queryKey: ['todos', { status, page }], ... }) useQuery({ queryKey: ['todos', { page, status }], ...}) useQuery({ queryKey: ['todos', { page, status, other: undefined }], ... }) The following query keys, however, are not equal. Array item order matters! tsx useQuery({ queryKey: ['todos', status, page], ... }) useQuery({ queryKey: ['todos', page, status], ...}) useQuery({ queryKey: ['todos', undefined, page, status], ...}) useQuery({ queryKey: ['todos', status, page], ... }) useQuery({ queryKey: ['todos', page, status], ...}) useQuery({ queryKey: ['todos', undefined, page, status], ...}) ## If your query function depends on a variable, include it in your query key Since query keys uniquely describe the data they are fetching, they should include any variables you use in your query function that **change**. For example: tsx function Todos({ todoId }) { const result = useQuery({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId), }) } function Todos({ todoId }) { const result = useQuery({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId), }) } Note that 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 any time a variable changes, _queries will be refetched automatically_ (depending on your staleTime settings). See the exhaustive-deps section for more information and examples. ## Further reading For tips on organizing Query Keys in larger applications, have a look at Effective React Query Keys and check the Query Key Factory Package from the Community Resources. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/query-functions A query function can be literally any function that **returns a promise**. The promise that is returned should either **resolve the data** or **throw an error**. All of the following are valid query function configurations: tsx useQuery({ queryKey: ['todos'], queryFn: fetchAllTodos }) useQuery({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId) }) useQuery({ queryKey: ['todos', todoId], queryFn: async () => { const data = await fetchTodoById(todoId) return data }, }) useQuery({ queryKey: ['todos', todoId], queryFn: ({ queryKey }) => fetchTodoById(queryKey[1]), }) useQuery({ queryKey: ['todos'], queryFn: fetchAllTodos }) useQuery({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId) }) useQuery({ queryKey: ['todos', todoId], queryFn: async () => { const data = await fetchTodoById(todoId) return data }, }) useQuery({ queryKey: ['todos', todoId], queryFn: ({ queryKey }) => fetchTodoById(queryKey[1]), }) ## Handling and Throwing Errors For TanStack Query to determine a query has errored, the query function **must throw** or return a **rejected Promise**. Any error that is thrown in the query function will be persisted on the error state of the query. tsx const { error } = useQuery({ queryKey: ['todos', todoId], queryFn: async () => { if (somethingGoesWrong) { throw new Error('Oh no!') } if (somethingElseGoesWrong) { return Promise.reject(new Error('Oh no!')) } return data }, }) const { error } = useQuery({ queryKey: ['todos', todoId], queryFn: async () => { if (somethingGoesWrong) { throw new Error('Oh no!') } if (somethingElseGoesWrong) { return Promise.reject(new Error('Oh no!')) } return data }, }) ## Usage with fetch and other clients that do not throw by default While most utilities like axios or graphql-request automatically throw errors for unsuccessful HTTP calls, some utilities like fetch do not throw errors by default. If that's the case, you'll need to throw them on your own. Here is a simple way to do that with the popular fetch API: tsx useQuery({ queryKey: ['todos', todoId], queryFn: async () => { const response = await fetch('/todos/' + todoId) if (!response.ok) { throw new Error('Network response was not ok') } return response.json() }, }) useQuery({ queryKey: ['todos', todoId], queryFn: async () => { const response = await fetch('/todos/' + todoId) if (!response.ok) { throw new Error('Network response was not ok') } return response.json() }, }) ## Query Function Variables Query keys are not just for uniquely identifying the data you are fetching, but are also conveniently passed into your query function as part of the QueryFunctionContext. While not always necessary, this makes it possible to extract your query functions if needed: tsx function Todos({ status, page }) { const result = useQuery({ queryKey: ['todos', { status, page }], queryFn: fetchTodoList, }) } // Access the key, status and page variables in your query function! function fetchTodoList({ queryKey }) { const [_key, { status, page }] = queryKey return new Promise() } function Todos({ status, page }) { const result = useQuery({ queryKey: ['todos', { status, page }], queryFn: fetchTodoList, }) } // Access the key, status and page variables in your query function! function fetchTodoList({ queryKey }) { const [_key, { status, page }] = queryKey return new Promise() } ### QueryFunctionContext The QueryFunctionContext is the object passed to each query function. It consists of: * queryKey: QueryKey: Query Keys * client: QueryClient: QueryClient * signal?: AbortSignal * AbortSignal instance provided by TanStack Query * Can be used for Query Cancellation * meta: Record<string, unknown> | undefined * an optional field you can fill with additional information about your query Additionally, Infinite Queries get the following options passed: * pageParam: TPageParam * the page parameter used to fetch the current page * direction: 'forward' | 'backward' * **deprecated** * the direction of the current page fetch * To get access to the direction of the current page fetch, please add a direction to pageParam from getNextPageParam and getPreviousPageParam. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/query-options One of the best ways to share queryKey and queryFn between multiple places, yet keep them co-located to one another, is to use the queryOptions helper. At runtime, this helper just returns whatever you pass into it, but it has a lot of advantages when using it with TypeScript. You can define all possible options for a query in one place, and you'll also get type inference and type safety for all of them. ts import { queryOptions } from '@tanstack/react-query' function groupOptions(id: number) { return queryOptions({ queryKey: ['groups', id], queryFn: () => fetchGroups(id), staleTime: 5 * 1000, }) } // usage: useQuery(groupOptions(1)) useSuspenseQuery(groupOptions(5)) useQueries({ queries: [groupOptions(1), groupOptions(2)], }) queryClient.prefetchQuery(groupOptions(23)) queryClient.setQueryData(groupOptions(42).queryKey, newGroups) import { queryOptions } from '@tanstack/react-query' function groupOptions(id: number) { return queryOptions({ queryKey: ['groups', id], queryFn: () => fetchGroups(id), staleTime: 5 * 1000, }) } // usage: useQuery(groupOptions(1)) useSuspenseQuery(groupOptions(5)) useQueries({ queries: [groupOptions(1), groupOptions(2)], }) queryClient.prefetchQuery(groupOptions(23)) queryClient.setQueryData(groupOptions(42).queryKey, newGroups) For Infinite Queries, a separate infiniteQueryOptions helper is available. You can still override some options at the component level. A very common and useful pattern is to create per-component select functions: ts // Type inference still works, so query.data will be the return type of select instead of queryFn const query = useQuery({ ...groupOptions(1), select: (data) => data.groupName, }) // Type inference still works, so query.data will be the return type of select instead of queryFn const query = useQuery({ ...groupOptions(1), select: (data) => data.groupName, }) --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/network-mode TanStack Query provides three different network modes to distinguish how Queries and Mutations should behave if you have no network connection. This mode can be set for each Query / Mutation individually, or globally via the query / mutation defaults. Since TanStack Query is most often used for data fetching in combination with data fetching libraries, the default network mode is online. ## Network Mode: online In this mode, Queries and Mutations will not fire unless you have network connection. This is the default mode. If a fetch is initiated for a query, it will always stay in the state (pending, error, success) it is in if the fetch cannot be made because there is no network connection. However, a fetchStatus is exposed additionally. This can be either: * fetching: The queryFn is really executing - a request is in-flight. * paused: The query is not executing - it is paused until you have connection again * idle: The query is not fetching and not paused The flags isFetching and isPaused are derived from this state and exposed for convenience. > Keep in mind that it might not be enough to check for pending state to show a loading spinner. Queries can be in state: 'pending', but fetchStatus: 'paused' if they are mounting for the first time, and you have no network connection. If a query runs because you are online, but you go offline while the fetch is still happening, TanStack Query will also pause the retry mechanism. Paused queries will then continue to run once you re-gain network connection. This is independent of refetchOnReconnect (which also defaults to true in this mode), because it is not a refetch, but rather a continue. If the query has been cancelled in the meantime, it will not continue. ## Network Mode: always In this mode, TanStack Query will always fetch and ignore the online / offline state. This is likely the mode you want to choose if you use TanStack Query in an environment where you don't need an active network connection for your Queries to work - e.g. if you just read from AsyncStorage, or if you just want to return Promise.resolve(5) from your queryFn. * Queries will never be paused because you have no network connection. * Retries will also not pause - your Query will go to error state if it fails. * refetchOnReconnect defaults to false in this mode, because reconnecting to the network is not a good indicator anymore that stale queries should be refetched. You can still turn it on if you want. ## Network Mode: offlineFirst This mode is the middle ground between the first two options, where TanStack Query will run the queryFn once, but then pause retries. This is very handy if you have a serviceWorker that intercepts a request for caching like in an offline-first PWA, or if you use HTTP caching via the Cache-Control header. In those situations, the first fetch might succeed because it comes from an offline storage / cache. However, if there is a cache miss, the network request will go out and fail, in which case this mode behaves like an online query - pausing retries. ## Devtools The TanStack Query Devtools will show Queries in a paused state if they would be fetching, but there is no network connection. There is also a toggle button to _Mock offline behavior_. Please note that this button will _not_ actually mess with your network connection (you can do that in the browser devtools), but it will set the OnlineManager in an offline state. ## Signature * networkMode: 'online' | 'always' | 'offlineFirst' * optional * defaults to 'online' --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/parallel-queries "Parallel" queries are queries that are executed in parallel, or at the same time so as to maximize fetching concurrency. ## Manual Parallel Queries When the number of parallel queries does not change, there is **no extra effort** to use parallel queries. Just use any number of TanStack Query's useQuery and useInfiniteQuery hooks side-by-side! tsx function App () { // The following queries will execute in parallel const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers }) const teamsQuery = useQuery({ queryKey: ['teams'], queryFn: fetchTeams }) const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: fetchProjects }) ... } function App () { // The following queries will execute in parallel const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers }) const teamsQuery = useQuery({ queryKey: ['teams'], queryFn: fetchTeams }) const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: fetchProjects }) ... } > When using React Query in suspense mode, this pattern of parallelism does not work, since the first query would throw a promise internally and would suspend the component before the other queries run. To get around this, you'll either need to use the useSuspenseQueries hook (which is suggested) or orchestrate your own parallelism with separate components for each useSuspenseQuery instance. ## Dynamic Parallel Queries with useQueries If the number of queries you need to execute is changing from render to render, you cannot use manual querying since that would violate the rules of hooks. Instead, TanStack Query provides a useQueries hook, which you can use to dynamically execute as many queries in parallel as you'd like. useQueries accepts an **options object** with a **queries key** whose value is an **array of query objects**. It returns an **array of query results**: tsx function App({ users }) { const userQueries = useQueries({ queries: users.map((user) => { return { queryKey: ['user', user.id], queryFn: () => fetchUserById(user.id), } }), }) } function App({ users }) { const userQueries = useQueries({ queries: users.map((user) => { return { queryKey: ['user', user.id], queryFn: () => fetchUserById(user.id), } }), }) } --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/dependent-queries ## useQuery dependent Query Dependent (or serial) queries depend on previous ones to finish before they can execute. To achieve this, it's as easy as using the enabled option to tell a query when it is ready to run: tsx // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) The projects query will start in: tsx status: 'pending' isPending: true fetchStatus: 'idle' status: 'pending' isPending: true fetchStatus: 'idle' As soon as the user is available, the projects query will be enabled and will then transition to: tsx status: 'pending' isPending: true fetchStatus: 'fetching' status: 'pending' isPending: true fetchStatus: 'fetching' Once we have the projects, it will go to: tsx status: 'success' isPending: false fetchStatus: 'idle' status: 'success' isPending: false fetchStatus: 'idle' ## useQueries dependent Query Dynamic parallel query - useQueries can depend on a previous query also, here's how to achieve this: tsx // Get the users ids const { data: userIds } = useQuery({ queryKey: ['users'], queryFn: getUsersData, select: (users) => users.map((user) => user.id), }) // Then get the users messages const usersMessages = useQueries({ queries: userIds ? userIds.map((id) => { return { queryKey: ['messages', id], queryFn: () => getMessagesByUsers(id), } }) : [], // if users is undefined, an empty array will be returned }) // Get the users ids const { data: userIds } = useQuery({ queryKey: ['users'], queryFn: getUsersData, select: (users) => users.map((user) => user.id), }) // Then get the users messages const usersMessages = useQueries({ queries: userIds ? userIds.map((id) => { return { queryKey: ['messages', id], queryFn: () => getMessagesByUsers(id), } }) : [], // if users is undefined, an empty array will be returned }) **Note** that useQueries return an **array of query results** ## A note about performance Dependent queries by definition constitutes a form of request waterfall, which hurts performance. If we pretend both queries take the same amount of time, doing them serially instead of in parallel always takes twice as much time, which is especially hurtful when it happens on a client that has high latency. If you can, it's always better to restructure the backend APIs so that both queries can be fetched in parallel, though that might not always be practically feasible. In the example above, instead of first fetching getUserByEmail to be able to getProjectsByUser, introducing a new getProjectsByUserEmail query would flatten the waterfall. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/background-fetching-indicators A query's status === 'pending' state is sufficient enough to show the initial hard-loading state for a query, but sometimes you may want to display an additional indicator that a query is refetching in the background. To do this, queries also supply you with an isFetching boolean that you can use to show that it's in a fetching state, regardless of the state of the status variable: tsx function Todos() { const { status, data: todos, error, isFetching, } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, }) return status === 'pending' ? ( <span>Loading...</span> ) : status === 'error' ? ( <span>Error: {error.message}</span> ) : ( <> {isFetching ? <div>Refreshing...</div> : null} <div> {todos.map((todo) => ( <Todo todo={todo} /> ))} </div> </> ) } function Todos() { const { status, data: todos, error, isFetching, } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, }) return status === 'pending' ? ( <span>Loading...</span> ) : status === 'error' ? ( <span>Error: {error.message}</span> ) : ( <> {isFetching ? <div>Refreshing...</div> : null} <div> {todos.map((todo) => ( <Todo todo={todo} /> ))} </div> </> ) } ## Displaying Global Background Fetching Loading State In addition to individual query loading states, if you would like to show a global loading indicator when **any** queries are fetching (including in the background), you can use the useIsFetching hook: tsx import { useIsFetching } from '@tanstack/react-query' function GlobalLoadingIndicator() { const isFetching = useIsFetching() return isFetching ? ( <div>Queries are fetching in the background...</div> ) : null } import { useIsFetching } from '@tanstack/react-query' function GlobalLoadingIndicator() { const isFetching = useIsFetching() return isFetching ? ( <div>Queries are fetching in the background...</div> ) : null } --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/window-focus-refetching If a user leaves your application and returns and the query data is stale, **TanStack Query automatically requests fresh data for you in the background**. You can disable this globally or per-query using the refetchOnWindowFocus option: #### Disabling Globally tsx // const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, // default: true }, }, }) function App() { return <QueryClientProvider client={queryClient}>...</QueryClientProvider> } // const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, // default: true }, }, }) function App() { return <QueryClientProvider client={queryClient}>...</QueryClientProvider> } #### Disabling Per-Query tsx useQuery({ queryKey: ['todos'], queryFn: fetchTodos, refetchOnWindowFocus: false, }) useQuery({ queryKey: ['todos'], queryFn: fetchTodos, refetchOnWindowFocus: false, }) ## Custom Window Focus Event In rare circumstances, you may want to manage your own window focus events that trigger TanStack Query to revalidate. To do this, TanStack Query provides a focusManager.setEventListener function that supplies you the callback that should be fired when the window is focused and allows you to set up your own events. When calling focusManager.setEventListener, the previously set handler is removed (which in most cases will be the default handler) and your new handler is used instead. For example, this is the default handler: tsx focusManager.setEventListener((handleFocus) => { // Listen to visibilitychange if (typeof window !== 'undefined' && window.addEventListener) { const visibilitychangeHandler = () => { handleFocus(document.visibilityState === 'visible') } window.addEventListener('visibilitychange', visibilitychangeHandler, false) return () => { // Be sure to unsubscribe if a new handler is set window.removeEventListener('visibilitychange', visibilitychangeHandler) } } }) focusManager.setEventListener((handleFocus) => { // Listen to visibilitychange if (typeof window !== 'undefined' && window.addEventListener) { const visibilitychangeHandler = () => { handleFocus(document.visibilityState === 'visible') } window.addEventListener('visibilitychange', visibilitychangeHandler, false) return () => { // Be sure to unsubscribe if a new handler is set window.removeEventListener('visibilitychange', visibilitychangeHandler) } } }) ## Managing Focus in React Native Instead of event listeners on window, React Native provides focus information through the AppState module. You can use the AppState "change" event to trigger an update when the app state changes to "active": tsx import { AppState } from 'react-native' import { focusManager } from '@tanstack/react-query' function onAppStateChange(status: AppStateStatus) { if (Platform.OS !== 'web') { focusManager.setFocused(status === 'active') } } useEffect(() => { const subscription = AppState.addEventListener('change', onAppStateChange) return () => subscription.remove() }, []) import { AppState } from 'react-native' import { focusManager } from '@tanstack/react-query' function onAppStateChange(status: AppStateStatus) { if (Platform.OS !== 'web') { focusManager.setFocused(status === 'active') } } useEffect(() => { const subscription = AppState.addEventListener('change', onAppStateChange) return () => subscription.remove() }, []) ## Managing focus state tsx import { focusManager } from '@tanstack/react-query' // Override the default focus state focusManager.setFocused(true) // Fallback to the default focus check focusManager.setFocused(undefined) import { focusManager } from '@tanstack/react-query' // Override the default focus state focusManager.setFocused(true) // Fallback to the default focus check focusManager.setFocused(undefined) --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/disabling-queries If you ever want to disable a query from automatically running, you can use the enabled = false option. The enabled option also accepts a callback that returns a boolean. When enabled is false: * If the query has cached data, then the query will be initialized in the status === 'success' or isSuccess state. * If the query does not have cached data, then the query will start in the status === 'pending' and fetchStatus === 'idle' state. * The query will not automatically fetch on mount. * The query will not automatically refetch in the background. * The query will ignore query client invalidateQueries and refetchQueries calls that would normally result in the query refetching. * refetch returned from useQuery can be used to manually trigger the query to fetch. However, it will not work with skipToken. > TypeScript users may prefer to use skipToken as an alternative to enabled = false. tsx function Todos() { const { isLoading, isError, data, error, refetch, isFetching } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, enabled: false, }) return ( <div> <button onClick={() => refetch()}>Fetch Todos</button> {data ? ( <> <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> </> ) : isError ? ( <span>Error: {error.message}</span> ) : isLoading ? ( <span>Loading...</span> ) : ( <span>Not ready ...</span> )} <div>{isFetching ? 'Fetching...' : null}</div> </div> ) } function Todos() { const { isLoading, isError, data, error, refetch, isFetching } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, enabled: false, }) return ( <div> <button onClick={() => refetch()}>Fetch Todos</button> {data ? ( <> <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> </> ) : isError ? ( <span>Error: {error.message}</span> ) : isLoading ? ( <span>Loading...</span> ) : ( <span>Not ready ...</span> )} <div>{isFetching ? 'Fetching...' : null}</div> </div> ) } Permanently disabling a query opts out of many great features that TanStack Query has to offer (like background refetches), and it's also not the idiomatic way. It takes you from the declarative approach (defining dependencies when your query should run) into an imperative mode (fetch whenever I click here). It is also not possible to pass parameters to refetch. Oftentimes, all you want is a lazy query that defers the initial fetch: ## Lazy Queries The enabled option can not only be used to permanently disable a query, but also to enable / disable it at a later time. A good example would be a filter form where you only want to fire off the first request once the user has entered a filter value: tsx function Todos() { const [filter, setFilter] = React.useState('') const { data } = useQuery({ queryKey: ['todos', filter], queryFn: () => fetchTodos(filter), // ⬇️ disabled as long as the filter is empty enabled: !!filter, }) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data} />} </div> ) } function Todos() { const [filter, setFilter] = React.useState('') const { data } = useQuery({ queryKey: ['todos', filter], queryFn: () => fetchTodos(filter), // ⬇️ disabled as long as the filter is empty enabled: !!filter, }) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data} />} </div> ) } ### isLoading (Previously: isInitialLoading) Lazy queries will be in status: 'pending' right from the start because pending means that there is no data yet. This is technically true, however, since we are not currently fetching any data (as the query is not _enabled_), it also means you likely cannot use this flag to show a loading spinner. If you are using disabled or lazy queries, you can use the isLoading flag instead. It's a derived flag that is computed from: isPending && isFetching so it will only be true if the query is currently fetching for the first time. ## Typesafe disabling of queries using skipToken If you are using TypeScript, you can use the skipToken to disable a query. This is useful when you want to disable a query based on a condition, but you still want to keep the query to be type safe. > IMPORTANT: refetch from useQuery will not work with skipToken. Other than that, skipToken works the same as enabled: false. tsx import { skipToken, useQuery } from '@tanstack/react-query' function Todos() { const [filter, setFilter] = React.useState<string | undefined>() const { data } = useQuery({ queryKey: ['todos', filter], // ⬇️ disabled as long as the filter is undefined or empty queryFn: filter ? () => fetchTodos(filter) : skipToken, }) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data} />} </div> ) } import { skipToken, useQuery } from '@tanstack/react-query' function Todos() { const [filter, setFilter] = React.useState<string | undefined>() const { data } = useQuery({ queryKey: ['todos', filter], // ⬇️ disabled as long as the filter is undefined or empty queryFn: filter ? () => fetchTodos(filter) : skipToken, }) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data} />} </div> ) } --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/query-retries When a useQuery query fails (the query function throws an error), TanStack Query will automatically retry the query if that query's request has not reached the max number of consecutive retries (defaults to 3) or a function is provided to determine if a retry is allowed. You can configure retries both on a global level and an individual query level. * Setting retry = false will disable retries. * Setting retry = 6 will retry failing requests 6 times before showing the final error thrown by the function. * Setting retry = true will infinitely retry failing requests. * Setting retry = (failureCount, error) => ... allows for custom logic based on why the request failed. > On the server, retries default to 0 to make server rendering as fast as possible. tsx import { useQuery } from '@tanstack/react-query' // Make a specific query retry a certain number of times const result = useQuery({ queryKey: ['todos', 1], queryFn: fetchTodoListPage, retry: 10, // Will retry failed requests 10 times before displaying an error }) import { useQuery } from '@tanstack/react-query' // Make a specific query retry a certain number of times const result = useQuery({ queryKey: ['todos', 1], queryFn: fetchTodoListPage, retry: 10, // Will retry failed requests 10 times before displaying an error }) > Info: Contents of the error property will be part of failureReason response property of useQuery until the last retry attempt. So in above example any error contents will be part of failureReason property for first 9 retry attempts (Overall 10 attempts) and finally they will be part of error after last attempt if error persists after all retry attempts. ## Retry Delay By default, retries in TanStack Query do not happen immediately after a request fails. As is standard, a back-off delay is gradually applied to each retry attempt. The default retryDelay is set to double (starting at 1000ms) with each attempt, but not exceed 30 seconds: tsx // Configure for all queries 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> } // Configure for all queries 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> } Though it is not recommended, you can obviously override the retryDelay function/integer in both the Provider and individual query options. If set to an integer instead of a function the delay will always be the same amount of time: tsx const result = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, retryDelay: 1000, // Will always wait 1000ms to retry, regardless of how many retries }) const result = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, retryDelay: 1000, // Will always wait 1000ms to retry, regardless of how many retries }) --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/paginated-queries Rendering paginated data is a very common UI pattern and in TanStack Query, it "just works" by including the page information in the query key: tsx const result = useQuery({ queryKey: ['projects', page], queryFn: fetchProjects, }) const result = useQuery({ queryKey: ['projects', page], queryFn: fetchProjects, }) However, if you run this simple example, you might notice something strange: **The UI jumps in and out of the success and pending states because each new page is treated like a brand new query.** This experience is not optimal and unfortunately is how many tools today insist on working. But not TanStack Query! As you may have guessed, TanStack Query comes with an awesome feature called placeholderData that allows us to get around this. ## Better Paginated Queries with placeholderData Consider the following example where we would ideally want to increment a pageIndex (or cursor) for a query. If we were to use useQuery, **it would still technically work fine**, but the UI would jump in and out of the success and pending states as different queries are created and destroyed for each page or cursor. By setting placeholderData to (previousData) => previousData or keepPreviousData function exported from TanStack Query, we get a few new things: * **The data from the last successful fetch is available while new data is being requested, even though the query key has changed**. * When the new data arrives, the previous data is seamlessly swapped to show the new data. * isPlaceholderData is made available to know what data the query is currently providing you tsx import { keepPreviousData, useQuery } from '@tanstack/react-query' import React from 'react' function Todos() { const [page, setPage] = React.useState(0) const fetchProjects = (page = 0) => fetch('/api/projects?page=' + page).then((res) => res.json()) const { isPending, isError, error, data, isFetching, isPlaceholderData } = useQuery({ queryKey: ['projects', page], queryFn: () => fetchProjects(page), placeholderData: keepPreviousData, }) return ( <div> {isPending ? ( <div>Loading...</div> ) : isError ? ( <div>Error: {error.message}</div> ) : ( <div> {data.projects.map((project) => ( <p key={project.id}>{project.name}</p> ))} </div> )} <span>Current Page: {page + 1}</span> <button onClick={() => setPage((old) => Math.max(old - 1, 0))} disabled={page === 0} > Previous Page </button> <button onClick={() => { if (!isPlaceholderData && data.hasMore) { setPage((old) => old + 1) } }} // Disable the Next Page button until we know a next page is available disabled={isPlaceholderData || !data?.hasMore} > Next Page </button> {isFetching ? <span> Loading...</span> : null} </div> ) } import { keepPreviousData, useQuery } from '@tanstack/react-query' import React from 'react' function Todos() { const [page, setPage] = React.useState(0) const fetchProjects = (page = 0) => fetch('/api/projects?page=' + page).then((res) => res.json()) const { isPending, isError, error, data, isFetching, isPlaceholderData } = useQuery({ queryKey: ['projects', page], queryFn: () => fetchProjects(page), placeholderData: keepPreviousData, }) return ( <div> {isPending ? ( <div>Loading...</div> ) : isError ? ( <div>Error: {error.message}</div> ) : ( <div> {data.projects.map((project) => ( <p key={project.id}>{project.name}</p> ))} </div> )} <span>Current Page: {page + 1}</span> <button onClick={() => setPage((old) => Math.max(old - 1, 0))} disabled={page === 0} > Previous Page </button> <button onClick={() => { if (!isPlaceholderData && data.hasMore) { setPage((old) => old + 1) } }} // Disable the Next Page button until we know a next page is available disabled={isPlaceholderData || !data?.hasMore} > Next Page </button> {isFetching ? <span> Loading...</span> : null} </div> ) } ## Lagging Infinite Query results with placeholderData While not as common, the placeholderData option also works flawlessly with the useInfiniteQuery hook, so you can seamlessly allow your users to continue to see cached data while infinite query keys change over time. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries Rendering lists that can additively "load more" data onto an existing set of data or "infinite scroll" is also a very common UI pattern. TanStack Query supports a useful version of useQuery called useInfiniteQuery for querying these types of lists. When using useInfiniteQuery, you'll notice a few things are different: * data is now an object containing infinite query data: * data.pages array containing the fetched pages * data.pageParams array containing the page params used to fetch the pages * The fetchNextPage and fetchPreviousPage functions are now available (fetchNextPage is required) * The initialPageParam option is now available (and required) to specify the initial page param * The getNextPageParam and getPreviousPageParam options are available for both determining if there is more data to load and the information to fetch it. This information is supplied as an additional parameter in the query function * A hasNextPage boolean is now available and is true if getNextPageParam returns a value other than null or undefined * A hasPreviousPage boolean is now available and is true if getPreviousPageParam returns a value other than null or undefined * The isFetchingNextPage and isFetchingPreviousPage booleans are now available to distinguish between a background refresh state and a loading more state > Note: Options initialData or placeholderData need to conform to the same structure of an object with data.pages and data.pageParams properties. ## Example Let's assume we have an API that returns pages of projects 3 at a time based on a cursor index along with a cursor that can be used to fetch the next group of projects: tsx fetch('/api/projects?cursor=0') // { data: [...], nextCursor: 3} fetch('/api/projects?cursor=3') // { data: [...], nextCursor: 6} fetch('/api/projects?cursor=6') // { data: [...], nextCursor: 9} fetch('/api/projects?cursor=9') // { data: [...] } fetch('/api/projects?cursor=0') // { data: [...], nextCursor: 3} fetch('/api/projects?cursor=3') // { data: [...], nextCursor: 6} fetch('/api/projects?cursor=6') // { data: [...], nextCursor: 9} fetch('/api/projects?cursor=9') // { data: [...] } With this information, we can create a "Load More" UI by: * Waiting for useInfiniteQuery to request the first group of data by default * Returning the information for the next query in getNextPageParam * Calling fetchNextPage function tsx import { useInfiniteQuery } from '@tanstack/react-query' function Projects() { const fetchProjects = async ({ pageParam }) => { const res = await fetch('/api/projects?cursor=' + pageParam) return res.json() } const { data, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, status, } = useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, }) return status === 'pending' ? ( <p>Loading...</p> ) : status === 'error' ? ( <p>Error: {error.message}</p> ) : ( <> {data.pages.map((group, i) => ( <React.Fragment key={i}> {group.data.map((project) => ( <p key={project.id}>{project.name}</p> ))} </React.Fragment> ))} <div> <button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage} > {isFetchingNextPage ? 'Loading more...' : hasNextPage ? 'Load More' : 'Nothing more to load'} </button> </div> <div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div> </> ) } import { useInfiniteQuery } from '@tanstack/react-query' function Projects() { const fetchProjects = async ({ pageParam }) => { const res = await fetch('/api/projects?cursor=' + pageParam) return res.json() } const { data, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, status, } = useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, }) return status === 'pending' ? ( <p>Loading...</p> ) : status === 'error' ? ( <p>Error: {error.message}</p> ) : ( <> {data.pages.map((group, i) => ( <React.Fragment key={i}> {group.data.map((project) => ( <p key={project.id}>{project.name}</p> ))} </React.Fragment> ))} <div> <button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage} > {isFetchingNextPage ? 'Loading more...' : hasNextPage ? 'Load More' : 'Nothing more to load'} </button> </div> <div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div> </> ) } It's essential to understand that calling fetchNextPage while an ongoing fetch is in progress runs the risk of overwriting data refreshes happening in the background. This situation becomes particularly critical when rendering a list and triggering fetchNextPage simultaneously. Remember, there can only be a single ongoing fetch for an InfiniteQuery. A single cache entry is shared for all pages, attempting to fetch twice simultaneously might lead to data overwrites. If you intend to enable simultaneous fetching, you can utilize the { cancelRefetch: false } option (default: true) within fetchNextPage. To ensure a seamless querying process without conflicts, it's highly recommended to verify that the query is not in an isFetching state, especially if the user won't directly control that call. jsx <List onEndReached={() => !isFetchingNextPage && fetchNextPage()} /> <List onEndReached={() => !isFetchingNextPage && fetchNextPage()} /> ## What happens when an infinite query needs to be refetched? When an infinite query becomes stale and needs to be refetched, each group is fetched sequentially, starting from the first one. This ensures that even if the underlying data is mutated, we're not using stale cursors and potentially getting duplicates or skipping records. If an infinite query's results are ever removed from the queryCache, the pagination restarts at the initial state with only the initial group being requested. ## What if I want to implement a bi-directional infinite list? Bi-directional lists can be implemented by using the getPreviousPageParam, fetchPreviousPage, hasPreviousPage and isFetchingPreviousPage properties and functions. tsx useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor, }) useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor, }) ## What if I want to show the pages in reversed order? Sometimes you may want to show the pages in reversed order. If this is case, you can use the select option: tsx useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, select: (data) => ({ pages: [...data.pages].reverse(), pageParams: [...data.pageParams].reverse(), }), }) useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, select: (data) => ({ pages: [...data.pages].reverse(), pageParams: [...data.pageParams].reverse(), }), }) ## What if I want to manually update the infinite query? ### Manually removing first page: tsx queryClient.setQueryData(['projects'], (data) => ({ pages: data.pages.slice(1), pageParams: data.pageParams.slice(1), })) queryClient.setQueryData(['projects'], (data) => ({ pages: data.pages.slice(1), pageParams: data.pageParams.slice(1), })) ### Manually removing a single value from an individual page: tsx const newPagesArray = oldPagesArray?.pages.map((page) => page.filter((val) => val.id !== updatedId), ) ?? [] queryClient.setQueryData(['projects'], (data) => ({ pages: newPagesArray, pageParams: data.pageParams, })) const newPagesArray = oldPagesArray?.pages.map((page) => page.filter((val) => val.id !== updatedId), ) ?? [] queryClient.setQueryData(['projects'], (data) => ({ pages: newPagesArray, pageParams: data.pageParams, })) ### Keep only the first page: tsx queryClient.setQueryData(['projects'], (data) => ({ pages: data.pages.slice(0, 1), pageParams: data.pageParams.slice(0, 1), })) queryClient.setQueryData(['projects'], (data) => ({ pages: data.pages.slice(0, 1), pageParams: data.pageParams.slice(0, 1), })) Make sure to always keep the same data structure of pages and pageParams! ## What if I want to limit the number of pages? In some use cases you may want to limit the number of pages stored in the query data to improve the performance and UX: * when the user can load a large number of pages (memory usage) * when you have to refetch an infinite query that contains dozens of pages (network usage: all the pages are sequentially fetched) The solution is to use a "Limited Infinite Query". This is made possible by using the maxPages option in conjunction with getNextPageParam and getPreviousPageParam to allow fetching pages when needed in both directions. In the following example only 3 pages are kept in the query data pages array. If a refetch is needed, only 3 pages will be refetched sequentially. tsx useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor, maxPages: 3, }) useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, getPreviousPageParam: (firstPage, pages) => firstPage.prevCursor, maxPages: 3, }) ## What if my API doesn't return a cursor? If your API doesn't return a cursor, you can use the pageParam as a cursor. Because getNextPageParam and getPreviousPageParam also get the pageParamof the current page, you can use it to calculate the next / previous page param. tsx return useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, allPages, lastPageParam) => { if (lastPage.length === 0) { return undefined } return lastPageParam + 1 }, getPreviousPageParam: (firstPage, allPages, firstPageParam) => { if (firstPageParam <= 1) { return undefined } return firstPageParam - 1 }, }) return useInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, allPages, lastPageParam) => { if (lastPage.length === 0) { return undefined } return lastPageParam + 1 }, getPreviousPageParam: (firstPage, allPages, firstPageParam) => { if (firstPageParam <= 1) { return undefined } return firstPageParam - 1 }, }) --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/initial-query-data There are many ways to supply initial data for a query to the cache before you need it: * Declaratively: * Provide initialData to a query to prepopulate its cache if empty * Imperatively: * Prefetch the data using queryClient.prefetchQuery * Manually place the data into the cache using queryClient.setQueryData ## Using initialData to prepopulate a query There may be times when you already have the initial data for a query available in your app and can simply provide it directly to your query. If and when this is the case, you can use the config.initialData option to set the initial data for a query and skip the initial loading state! > IMPORTANT: initialData is persisted to the cache, so it is not recommended to provide placeholder, partial or incomplete data to this option and instead use placeholderData tsx const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, }) const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, }) ### staleTime and initialDataUpdatedAt By default, initialData is treated as totally fresh, as if it were just fetched. This also means that it will affect how it is interpreted by the staleTime option. * If you configure your query observer with initialData, and no staleTime (the default staleTime: 0), the query will immediately refetch when it mounts: tsx // Will show initialTodos immediately, but also immediately refetch todos after mount const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, }) // Will show initialTodos immediately, but also immediately refetch todos after mount const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, }) * If you configure your query observer with initialData and a staleTime of 1000 ms, the data will be considered fresh for that same amount of time, as if it was just fetched from your query function. tsx // Show initialTodos immediately, but won't refetch until another interaction event is encountered after 1000 ms const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, staleTime: 1000, }) // Show initialTodos immediately, but won't refetch until another interaction event is encountered after 1000 ms const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, staleTime: 1000, }) * So what if your initialData isn't totally fresh? That leaves us with the last configuration that is actually the most accurate and uses an option called initialDataUpdatedAt. This option allows you to pass a numeric JS timestamp in milliseconds of when the initialData itself was last updated, e.g. what Date.now() provides. Take note that if you have a unix timestamp, you'll need to convert it to a JS timestamp by multiplying it by 1000. tsx // Show initialTodos immediately, but won't refetch until another interaction event is encountered after 1000 ms const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, staleTime: 60 * 1000, // 1 minute // This could be 10 seconds ago or 10 minutes ago initialDataUpdatedAt: initialTodosUpdatedTimestamp, // eg. 1608412420052 }) // Show initialTodos immediately, but won't refetch until another interaction event is encountered after 1000 ms const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: initialTodos, staleTime: 60 * 1000, // 1 minute // This could be 10 seconds ago or 10 minutes ago initialDataUpdatedAt: initialTodosUpdatedTimestamp, // eg. 1608412420052 }) This option allows the staleTime to be used for its original purpose, determining how fresh the data needs to be, while also allowing the data to be refetched on mount if the initialData is older than the staleTime. In the example above, our data needs to be fresh within 1 minute, and we can hint to the query when the initialData was last updated so the query can decide for itself whether the data needs to be refetched again or not. > If you would rather treat your data as **prefetched data**, we recommend that you use the prefetchQuery or fetchQuery APIs to populate the cache beforehand, thus letting you configure your staleTime independently from your initialData ### Initial Data Function If the process for accessing a query's initial data is intensive or just not something you want to perform on every render, you can pass a function as the initialData value. This function will be executed only once when the query is initialized, saving you precious memory and/or CPU: tsx const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: () => getExpensiveTodos(), }) const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), initialData: () => getExpensiveTodos(), }) ### Initial Data from Cache In some circumstances, you may be able to provide the initial data for a query from the cached result of another. A good example of this would be searching the cached data from a todos list query for an individual todo item, then using that as the initial data for your individual todo query: tsx const result = useQuery({ queryKey: ['todo', todoId], queryFn: () => fetch('/todos'), initialData: () => { // Use a todo from the 'todos' query as the initial data for this todo query return queryClient.getQueryData(['todos'])?.find((d) => d.id === todoId) }, }) const result = useQuery({ queryKey: ['todo', todoId], queryFn: () => fetch('/todos'), initialData: () => { // Use a todo from the 'todos' query as the initial data for this todo query return queryClient.getQueryData(['todos'])?.find((d) => d.id === todoId) }, }) ### Initial Data from the cache with initialDataUpdatedAt Getting initial data from the cache means the source query you're using to look up the initial data from is likely old. Instead of using an artificial staleTime to keep your query from refetching immediately, it's suggested that you pass the source query's dataUpdatedAt to initialDataUpdatedAt. This provides the query instance with all the information it needs to determine if and when the query needs to be refetched, regardless of initial data being provided. tsx const result = useQuery({ queryKey: ['todos', todoId], queryFn: () => fetch(`/todos/${todoId}`), initialData: () => queryClient.getQueryData(['todos'])?.find((d) => d.id === todoId), initialDataUpdatedAt: () => queryClient.getQueryState(['todos'])?.dataUpdatedAt, }) const result = useQuery({ queryKey: ['todos', todoId], queryFn: () => fetch(`/todos/${todoId}`), initialData: () => queryClient.getQueryData(['todos'])?.find((d) => d.id === todoId), initialDataUpdatedAt: () => queryClient.getQueryState(['todos'])?.dataUpdatedAt, }) ### Conditional Initial Data from Cache If the source query you're using to look up the initial data from is old, you may not want to use the cached data at all and just fetch from the server. To make this decision easier, you can use the queryClient.getQueryState method instead to get more information about the source query, including a state.dataUpdatedAt timestamp you can use to decide if the query is "fresh" enough for your needs: tsx const result = useQuery({ queryKey: ['todo', todoId], queryFn: () => fetch(`/todos/${todoId}`), initialData: () => { // Get the query state const state = queryClient.getQueryState(['todos']) // If the query exists and has data that is no older than 10 seconds... if (state && Date.now() - state.dataUpdatedAt <= 10 * 1000) { // return the individual todo return state.data.find((d) => d.id === todoId) } // Otherwise, return undefined and let it fetch from a hard loading state! }, }) const result = useQuery({ queryKey: ['todo', todoId], queryFn: () => fetch(`/todos/${todoId}`), initialData: () => { // Get the query state const state = queryClient.getQueryState(['todos']) // If the query exists and has data that is no older than 10 seconds... if (state && Date.now() - state.dataUpdatedAt <= 10 * 1000) { // return the individual todo return state.data.find((d) => d.id === todoId) } // Otherwise, return undefined and let it fetch from a hard loading state! }, }) ## Further reading For a comparison between Initial Data and Placeholder Data, have a look at the Community Resources. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/placeholder-query-data ## What is placeholder data? Placeholder data allows a query to behave as if it already has data, similar to the initialData option, but **the data is not persisted to the cache**. This comes in handy for situations where you have enough partial (or fake) data to render the query successfully while the actual data is fetched in the background. > Example: An individual blog post query could pull "preview" data from a parent list of blog posts that only include title and a small snippet of the post body. You would not want to persist this partial data to the query result of the individual query, but it is useful for showing the content layout as quickly as possible while the actual query finishes to fetch the entire object. There are a few ways to supply placeholder data for a query to the cache before you need it: * Declaratively: * Provide placeholderData to a query to prepopulate its cache if empty * Imperatively: * Prefetch or fetch the data using queryClient and the placeholderData option When we use placeholderData, our Query will not be in a pending state - it will start out as being in success state, because we have data to display - even if that data is just "placeholder" data. To distinguish it from "real" data, we will also have the isPlaceholderData flag set to true on the Query result. ## Placeholder Data as a Value tsx function Todos() { const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), placeholderData: placeholderTodos, }) } function Todos() { const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), placeholderData: placeholderTodos, }) } ### Placeholder Data Memoization If the process for accessing a query's placeholder data is intensive or just not something you want to perform on every render, you can memoize the value: tsx function Todos() { const placeholderData = useMemo(() => generateFakeTodos(), []) const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), placeholderData, }) } function Todos() { const placeholderData = useMemo(() => generateFakeTodos(), []) const result = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/todos'), placeholderData, }) } ## Placeholder Data as a Function placeholderData can also be a function, where you can get access to the data and Query meta information of a "previous" successful Query. This is useful for situations where you want to use the data from one query as the placeholder data for another query. When the QueryKey changes, e.g. from \['todos', 1\] to \['todos', 2\], we can keep displaying "old" data instead of having to show a loading spinner while data is _transitioning_ from one Query to the next. For more information, see Paginated Queries. tsx const result = useQuery({ queryKey: ['todos', id], queryFn: () => fetch(`/todos/${id}`), placeholderData: (previousData, previousQuery) => previousData, }) const result = useQuery({ queryKey: ['todos', id], queryFn: () => fetch(`/todos/${id}`), placeholderData: (previousData, previousQuery) => previousData, }) ### Placeholder Data from Cache In some circumstances, you may be able to provide the placeholder data for a query from the cached result of another. A good example of this would be searching the cached data from a blog post list query for a preview version of the post, then using that as the placeholder data for your individual post query: tsx function Todo({ blogPostId }) { const queryClient = useQueryClient() const result = useQuery({ queryKey: ['blogPost', blogPostId], queryFn: () => fetch(`/blogPosts/${blogPostId}`), placeholderData: () => { // Use the smaller/preview version of the blogPost from the 'blogPosts' // query as the placeholder data for this blogPost query return queryClient .getQueryData(['blogPosts']) ?.find((d) => d.id === blogPostId) }, }) } function Todo({ blogPostId }) { const queryClient = useQueryClient() const result = useQuery({ queryKey: ['blogPost', blogPostId], queryFn: () => fetch(`/blogPosts/${blogPostId}`), placeholderData: () => { // Use the smaller/preview version of the blogPost from the 'blogPosts' // query as the placeholder data for this blogPost query return queryClient .getQueryData(['blogPosts']) ?.find((d) => d.id === blogPostId) }, }) } ## Further reading For a comparison between Placeholder Data and Initial Data, have a look at the Community Resources. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/mutations Unlike queries, mutations are typically used to create/update/delete data or perform server side-effects. For this purpose, TanStack Query exports a useMutation hook. Here's an example of a mutation that adds a new todo to the server: tsx function App() { const mutation = useMutation({ mutationFn: (newTodo) => { return axios.post('/todos', newTodo) }, }) return ( <div> {mutation.isPending ? ( 'Adding todo...' ) : ( <> {mutation.isError ? ( <div>An error occurred: {mutation.error.message}</div> ) : null} {mutation.isSuccess ? <div>Todo added!</div> : null} <button onClick={() => { mutation.mutate({ id: new Date(), title: 'Do Laundry' }) }} > Create Todo </button> </> )} </div> ) } function App() { const mutation = useMutation({ mutationFn: (newTodo) => { return axios.post('/todos', newTodo) }, }) return ( <div> {mutation.isPending ? ( 'Adding todo...' ) : ( <> {mutation.isError ? ( <div>An error occurred: {mutation.error.message}</div> ) : null} {mutation.isSuccess ? <div>Todo added!</div> : null} <button onClick={() => { mutation.mutate({ id: new Date(), title: 'Do Laundry' }) }} > Create Todo </button> </> )} </div> ) } A mutation can only be in one of the following states at any given moment: * isIdle or status === 'idle' - The mutation is currently idle or in a fresh/reset state * isPending or status === 'pending' - The mutation is currently running * isError or status === 'error' - The mutation encountered an error * isSuccess or status === 'success' - The mutation was successful and mutation data is available Beyond those primary states, more information is available depending on the state of the mutation: * error - If the mutation is in an error state, the error is available via the error property. * data - If the mutation is in a success state, the data is available via the data property. In the example above, you also saw that you can pass variables to your mutations function by calling the mutate function with a **single variable or object**. Even with just variables, mutations aren't all that special, but when used with the onSuccess option, the Query Client's invalidateQueries method and the Query Client's setQueryData method, mutations become a very powerful tool. > IMPORTANT: The mutate function is an asynchronous function, which means you cannot use it directly in an event callback in **React 16 and earlier**. If you need to access the event in onSubmit you need to wrap mutate in another function. This is due to React event pooling. tsx // This will not work in React 16 and earlier const CreateTodo = () => { const mutation = useMutation({ mutationFn: (event) => { event.preventDefault() return fetch('/api', new FormData(event.target)) }, }) return <form onSubmit={mutation.mutate}>...</form> } // This will work const CreateTodo = () => { const mutation = useMutation({ mutationFn: (formData) => { return fetch('/api', formData) }, }) const onSubmit = (event) => { event.preventDefault() mutation.mutate(new FormData(event.target)) } return <form onSubmit={onSubmit}>...</form> } // This will not work in React 16 and earlier const CreateTodo = () => { const mutation = useMutation({ mutationFn: (event) => { event.preventDefault() return fetch('/api', new FormData(event.target)) }, }) return <form onSubmit={mutation.mutate}>...</form> } // This will work const CreateTodo = () => { const mutation = useMutation({ mutationFn: (formData) => { return fetch('/api', formData) }, }) const onSubmit = (event) => { event.preventDefault() mutation.mutate(new FormData(event.target)) } return <form onSubmit={onSubmit}>...</form> } ## Resetting Mutation State It's sometimes the case that you need to clear the error or data of a mutation request. To do this, you can use the reset function to handle this: tsx const CreateTodo = () => { const [title, setTitle] = useState('') const mutation = useMutation({ mutationFn: createTodo }) const onCreateTodo = (e) => { e.preventDefault() mutation.mutate({ title }) } return ( <form onSubmit={onCreateTodo}> {mutation.error && ( <h5 onClick={() => mutation.reset()}>{mutation.error}</h5> )} <input type="text" value={title} onChange={(e) => setTitle(e.target.value)} /> <br /> <button type="submit">Create Todo</button> </form> ) } const CreateTodo = () => { const [title, setTitle] = useState('') const mutation = useMutation({ mutationFn: createTodo }) const onCreateTodo = (e) => { e.preventDefault() mutation.mutate({ title }) } return ( <form onSubmit={onCreateTodo}> {mutation.error && ( <h5 onClick={() => mutation.reset()}>{mutation.error}</h5> )} <input type="text" value={title} onChange={(e) => setTitle(e.target.value)} /> <br /> <button type="submit">Create Todo</button> </form> ) } ## Mutation Side Effects useMutation comes with some helper options that allow quick and easy side-effects at any stage during the mutation lifecycle. These come in handy for both invalidating and refetching queries after mutations and even optimistic updates tsx useMutation({ mutationFn: addTodo, onMutate: (variables) => { // A mutation is about to happen! // Optionally return a context containing data to use when for example rolling back return { id: 1 } }, onError: (error, variables, context) => { // An error happened! console.log(`rolling back optimistic update with id ${context.id}`) }, onSuccess: (data, variables, context) => { // Boom baby! }, onSettled: (data, error, variables, context) => { // Error or success... doesn't matter! }, }) useMutation({ mutationFn: addTodo, onMutate: (variables) => { // A mutation is about to happen! // Optionally return a context containing data to use when for example rolling back return { id: 1 } }, onError: (error, variables, context) => { // An error happened! console.log(`rolling back optimistic update with id ${context.id}`) }, onSuccess: (data, variables, context) => { // Boom baby! }, onSettled: (data, error, variables, context) => { // Error or success... doesn't matter! }, }) When returning a promise in any of the callback functions it will first be awaited before the next callback is called: tsx useMutation({ mutationFn: addTodo, onSuccess: async () => { console.log("I'm first!") }, onSettled: async () => { console.log("I'm second!") }, }) useMutation({ mutationFn: addTodo, onSuccess: async () => { console.log("I'm first!") }, onSettled: async () => { console.log("I'm second!") }, }) You might find that you want to **trigger additional callbacks** beyond the ones defined on useMutation when calling mutate. This can be used to trigger component-specific side effects. To do that, you can provide any of the same callback options to the mutate function after your mutation variable. Supported options include: onSuccess, onError and onSettled. Please keep in mind that those additional callbacks won't run if your component unmounts _before_ the mutation finishes. tsx useMutation({ mutationFn: addTodo, onSuccess: (data, variables, context) => { // I will fire first }, onError: (error, variables, context) => { // I will fire first }, onSettled: (data, error, variables, context) => { // I will fire first }, }) mutate(todo, { onSuccess: (data, variables, context) => { // I will fire second! }, onError: (error, variables, context) => { // I will fire second! }, onSettled: (data, error, variables, context) => { // I will fire second! }, }) useMutation({ mutationFn: addTodo, onSuccess: (data, variables, context) => { // I will fire first }, onError: (error, variables, context) => { // I will fire first }, onSettled: (data, error, variables, context) => { // I will fire first }, }) mutate(todo, { onSuccess: (data, variables, context) => { // I will fire second! }, onError: (error, variables, context) => { // I will fire second! }, onSettled: (data, error, variables, context) => { // I will fire second! }, }) ### Consecutive mutations There is a slight difference in handling onSuccess, onError and onSettled callbacks when it comes to consecutive mutations. When passed to the mutate function, they will be fired up only _once_ and only if the component is still mounted. This is due to the fact that mutation observer is removed and resubscribed every time when the mutate function is called. On the contrary, useMutation handlers execute for each mutate call. > Be aware that most likely, mutationFn passed to useMutation is asynchronous. In that case, the order in which mutations are fulfilled may differ from the order of mutate function calls. tsx useMutation({ mutationFn: addTodo, onSuccess: (data, variables, context) => { // Will be called 3 times }, }) const todos = ['Todo 1', 'Todo 2', 'Todo 3'] todos.forEach((todo) => { mutate(todo, { onSuccess: (data, variables, context) => { // Will execute only once, for the last mutation (Todo 3), // regardless which mutation resolves first }, }) }) useMutation({ mutationFn: addTodo, onSuccess: (data, variables, context) => { // Will be called 3 times }, }) const todos = ['Todo 1', 'Todo 2', 'Todo 3'] todos.forEach((todo) => { mutate(todo, { onSuccess: (data, variables, context) => { // Will execute only once, for the last mutation (Todo 3), // regardless which mutation resolves first }, }) }) ## Promises Use mutateAsync instead of mutate to get a promise which will resolve on success or throw on an error. This can for example be used to compose side effects. tsx const mutation = useMutation({ mutationFn: addTodo }) try { const todo = await mutation.mutateAsync(todo) console.log(todo) } catch (error) { console.error(error) } finally { console.log('done') } const mutation = useMutation({ mutationFn: addTodo }) try { const todo = await mutation.mutateAsync(todo) console.log(todo) } catch (error) { console.error(error) } finally { console.log('done') } ## Retry By default, TanStack Query will not retry a mutation on error, but it is possible with the retry option: tsx const mutation = useMutation({ mutationFn: addTodo, retry: 3, }) const mutation = useMutation({ mutationFn: addTodo, retry: 3, }) If mutations fail because the device is offline, they will be retried in the same order when the device reconnects. ## Persist mutations Mutations can be persisted to storage if needed and resumed at a later point. This can be done with the hydration functions: tsx const queryClient = new QueryClient() // Define the "addTodo" mutation queryClient.setMutationDefaults(['addTodo'], { mutationFn: addTodo, onMutate: async (variables) => { // Cancel current queries for the todos list await queryClient.cancelQueries({ queryKey: ['todos'] }) // Create optimistic todo const optimisticTodo = { id: uuid(), title: variables.title } // Add optimistic todo to todos list queryClient.setQueryData(['todos'], (old) => [...old, optimisticTodo]) // Return context with the optimistic todo return { optimisticTodo } }, onSuccess: (result, variables, context) => { // Replace optimistic todo in the todos list with the result queryClient.setQueryData(['todos'], (old) => old.map((todo) => todo.id === context.optimisticTodo.id ? result : todo, ), ) }, onError: (error, variables, context) => { // Remove optimistic todo from the todos list queryClient.setQueryData(['todos'], (old) => old.filter((todo) => todo.id !== context.optimisticTodo.id), ) }, retry: 3, }) // Start mutation in some component: const mutation = useMutation({ mutationKey: ['addTodo'] }) mutation.mutate({ title: 'title' }) // If the mutation has been paused because the device is for example offline, // Then the paused mutation can be dehydrated when the application quits: const state = dehydrate(queryClient) // The mutation can then be hydrated again when the application is started: hydrate(queryClient, state) // Resume the paused mutations: queryClient.resumePausedMutations() const queryClient = new QueryClient() // Define the "addTodo" mutation queryClient.setMutationDefaults(['addTodo'], { mutationFn: addTodo, onMutate: async (variables) => { // Cancel current queries for the todos list await queryClient.cancelQueries({ queryKey: ['todos'] }) // Create optimistic todo const optimisticTodo = { id: uuid(), title: variables.title } // Add optimistic todo to todos list queryClient.setQueryData(['todos'], (old) => [...old, optimisticTodo]) // Return context with the optimistic todo return { optimisticTodo } }, onSuccess: (result, variables, context) => { // Replace optimistic todo in the todos list with the result queryClient.setQueryData(['todos'], (old) => old.map((todo) => todo.id === context.optimisticTodo.id ? result : todo, ), ) }, onError: (error, variables, context) => { // Remove optimistic todo from the todos list queryClient.setQueryData(['todos'], (old) => old.filter((todo) => todo.id !== context.optimisticTodo.id), ) }, retry: 3, }) // Start mutation in some component: const mutation = useMutation({ mutationKey: ['addTodo'] }) mutation.mutate({ title: 'title' }) // If the mutation has been paused because the device is for example offline, // Then the paused mutation can be dehydrated when the application quits: const state = dehydrate(queryClient) // The mutation can then be hydrated again when the application is started: hydrate(queryClient, state) // Resume the paused mutations: queryClient.resumePausedMutations() ### Persisting Offline mutations If you persist offline mutations with the persistQueryClient plugin, mutations cannot be resumed when the page is reloaded unless you provide a default mutation function. This is a technical limitation. When persisting to an external storage, only the state of mutations is persisted, as functions cannot be serialized. After hydration, the component that triggers the mutation might not be mounted, so calling resumePausedMutations might yield an error: No mutationFn found. tsx const persister = createSyncStoragePersister({ storage: window.localStorage, }) const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 24 hours }, }, }) // we need a default mutation function so that paused mutations can resume after a page reload queryClient.setMutationDefaults(['todos'], { mutationFn: ({ id, data }) => { return api.updateTodo(id, data) }, }) export default function App() { return ( <PersistQueryClientProvider client={queryClient} persistOptions={{ persister }} onSuccess={() => { // resume mutations after initial restore from localStorage was successful queryClient.resumePausedMutations() }} > <RestOfTheApp /> </PersistQueryClientProvider> ) } const persister = createSyncStoragePersister({ storage: window.localStorage, }) const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 24 hours }, }, }) // we need a default mutation function so that paused mutations can resume after a page reload queryClient.setMutationDefaults(['todos'], { mutationFn: ({ id, data }) => { return api.updateTodo(id, data) }, }) export default function App() { return ( <PersistQueryClientProvider client={queryClient} persistOptions={{ persister }} onSuccess={() => { // resume mutations after initial restore from localStorage was successful queryClient.resumePausedMutations() }} > <RestOfTheApp /> </PersistQueryClientProvider> ) } We also have an extensive offline example that covers both queries and mutations. ## Mutation Scopes Per default, all mutations run in parallel - even if you invoke .mutate() of the same mutation multiple times. Mutations can be given a scope with an id to avoid that. All mutations with the same scope.id will run in serial, which means when they are triggered, they will start in isPaused: true state if there is already a mutation for that scope in progress. They will be put into a queue and will automatically resume once their time in the queue has come. tsx const mutation = useMutation({ mutationFn: addTodo, scope: { id: 'todo', }, }) const mutation = useMutation({ mutationFn: addTodo, scope: { id: 'todo', }, }) ## Further reading For more information about mutations, have a look at #12: Mastering Mutations in React Query from the Community Resources. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation Waiting for queries to become stale before they are fetched again doesn't always work, especially when you know for a fact that a query's data is out of date because of something the user has done. For that purpose, the QueryClient has an invalidateQueries method that lets you intelligently mark queries as stale and potentially refetch them too! tsx // Invalidate every query in the cache queryClient.invalidateQueries() // Invalidate every query with a key that starts with `todos` queryClient.invalidateQueries({ queryKey: ['todos'] }) // Invalidate every query in the cache queryClient.invalidateQueries() // Invalidate every query with a key that starts with `todos` queryClient.invalidateQueries({ queryKey: ['todos'] }) > Note: Where other libraries that use normalized caches would attempt to update local queries with the new data either imperatively or via schema inference, TanStack Query gives you the tools to avoid the manual labor that comes with maintaining normalized caches and instead prescribes **targeted invalidation, background-refetching and ultimately atomic updates**. When a query is invalidated with invalidateQueries, two things happen: * It is marked as stale. This stale state overrides any staleTime configurations being used in useQuery or related hooks * If the query is currently being rendered via useQuery or related hooks, it will also be refetched in the background ## Query Matching with invalidateQueries When using APIs like invalidateQueries and removeQueries (and others that support partial query matching), you can match multiple queries by their prefix, or get really specific and match an exact query. For information on the types of filters you can use, please see Query Filters. In this example, we can use the todos prefix to invalidate any queries that start with todos in their query key: tsx import { useQuery, useQueryClient } from '@tanstack/react-query' // Get QueryClient from the context const queryClient = useQueryClient() queryClient.invalidateQueries({ queryKey: ['todos'] }) // Both queries below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) const todoListQuery = useQuery({ queryKey: ['todos', { page: 1 }], queryFn: fetchTodoList, }) import { useQuery, useQueryClient } from '@tanstack/react-query' // Get QueryClient from the context const queryClient = useQueryClient() queryClient.invalidateQueries({ queryKey: ['todos'] }) // Both queries below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) const todoListQuery = useQuery({ queryKey: ['todos', { page: 1 }], queryFn: fetchTodoList, }) You can even invalidate queries with specific variables by passing a more specific query key to the invalidateQueries method: tsx queryClient.invalidateQueries({ queryKey: ['todos', { type: 'done' }], }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { type: 'done' }], queryFn: fetchTodoList, }) // However, the following query below will NOT be invalidated const todoListQuery = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) queryClient.invalidateQueries({ queryKey: ['todos', { type: 'done' }], }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { type: 'done' }], queryFn: fetchTodoList, }) // However, the following query below will NOT be invalidated const todoListQuery = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) The invalidateQueries API is very flexible, so even if you want to **only** invalidate todos queries that don't have any more variables or subkeys, you can pass an exact: true option to the invalidateQueries method: tsx queryClient.invalidateQueries({ queryKey: ['todos'], exact: true, }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) // However, the following query below will NOT be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { type: 'done' }], queryFn: fetchTodoList, }) queryClient.invalidateQueries({ queryKey: ['todos'], exact: true, }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) // However, the following query below will NOT be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { type: 'done' }], queryFn: fetchTodoList, }) If you find yourself wanting **even more** granularity, you can pass a predicate function to the invalidateQueries method. This function will receive each Query instance from the query cache and allow you to return true or false for whether you want to invalidate that query: tsx queryClient.invalidateQueries({ predicate: (query) => query.queryKey[0] === 'todos' && query.queryKey[1]?.version >= 10, }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { version: 20 }], queryFn: fetchTodoList, }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { version: 10 }], queryFn: fetchTodoList, }) // However, the following query below will NOT be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { version: 5 }], queryFn: fetchTodoList, }) queryClient.invalidateQueries({ predicate: (query) => query.queryKey[0] === 'todos' && query.queryKey[1]?.version >= 10, }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { version: 20 }], queryFn: fetchTodoList, }) // The query below will be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { version: 10 }], queryFn: fetchTodoList, }) // However, the following query below will NOT be invalidated const todoListQuery = useQuery({ queryKey: ['todos', { version: 5 }], queryFn: fetchTodoList, }) --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/invalidations-from-mutations Invalidating queries is only half the battle. Knowing **when** to invalidate them is the other half. Usually when a mutation in your app succeeds, it's VERY likely that there are related queries in your application that need to be invalidated and possibly refetched to account for the new changes from your mutation. For example, assume we have a mutation to post a new todo: tsx const mutation = useMutation({ mutationFn: postTodo }) const mutation = useMutation({ mutationFn: postTodo }) When a successful postTodo mutation happens, we likely want all todos queries to get invalidated and possibly refetched to show the new todo item. To do this, you can use useMutation's onSuccess options and the client's invalidateQueries function: tsx import { useMutation, useQueryClient } from '@tanstack/react-query' const queryClient = useQueryClient() // When this mutation succeeds, invalidate any queries with the `todos` or `reminders` query key const mutation = useMutation({ mutationFn: addTodo, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }) queryClient.invalidateQueries({ queryKey: ['reminders'] }) }, }) import { useMutation, useQueryClient } from '@tanstack/react-query' const queryClient = useQueryClient() // When this mutation succeeds, invalidate any queries with the `todos` or `reminders` query key const mutation = useMutation({ mutationFn: addTodo, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }) queryClient.invalidateQueries({ queryKey: ['reminders'] }) }, }) You can wire up your invalidations to happen using any of the callbacks available in the useMutation hook --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/updates-from-mutation-responses When dealing with mutations that **update** objects on the server, it's common for the new object to be automatically returned in the response of the mutation. Instead of refetching any queries for that item and wasting a network call for data we already have, we can take advantage of the object returned by the mutation function and update the existing query with the new data immediately using the Query Client's setQueryData method: tsx const queryClient = useQueryClient() const mutation = useMutation({ mutationFn: editTodo, onSuccess: (data) => { queryClient.setQueryData(['todo', { id: 5 }], data) }, }) mutation.mutate({ id: 5, name: 'Do the laundry', }) // The query below will be updated with the response from the // successful mutation const { status, data, error } = useQuery({ queryKey: ['todo', { id: 5 }], queryFn: fetchTodoById, }) const queryClient = useQueryClient() const mutation = useMutation({ mutationFn: editTodo, onSuccess: (data) => { queryClient.setQueryData(['todo', { id: 5 }], data) }, }) mutation.mutate({ id: 5, name: 'Do the laundry', }) // The query below will be updated with the response from the // successful mutation const { status, data, error } = useQuery({ queryKey: ['todo', { id: 5 }], queryFn: fetchTodoById, }) You might want to tie the onSuccess logic into a reusable mutation, for that you can create a custom hook like this: tsx const useMutateTodo = () => { const queryClient = useQueryClient() return useMutation({ mutationFn: editTodo, // Notice the second argument is the variables object that the `mutate` function receives onSuccess: (data, variables) => { queryClient.setQueryData(['todo', { id: variables.id }], data) }, }) } const useMutateTodo = () => { const queryClient = useQueryClient() return useMutation({ mutationFn: editTodo, // Notice the second argument is the variables object that the `mutate` function receives onSuccess: (data, variables) => { queryClient.setQueryData(['todo', { id: variables.id }], data) }, }) } ## Immutability Updates via setQueryData must be performed in an _immutable_ way. **DO NOT** attempt to write directly to the cache by mutating data (that you retrieved from the cache) in place. It might work at first but can lead to subtle bugs along the way. tsx queryClient.setQueryData(['posts', { id }], (oldData) => { if (oldData) { // ❌ do not try this oldData.title = 'my new post title' } return oldData }) queryClient.setQueryData( ['posts', { id }], // ✅ this is the way (oldData) => oldData ? { ...oldData, title: 'my new post title', } : oldData, ) queryClient.setQueryData(['posts', { id }], (oldData) => { if (oldData) { // ❌ do not try this oldData.title = 'my new post title' } return oldData }) queryClient.setQueryData( ['posts', { id }], // ✅ this is the way (oldData) => oldData ? { ...oldData, title: 'my new post title', } : oldData, ) --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates React Query provides two ways to optimistically update your UI before a mutation has completed. You can either use the onMutate option to update your cache directly, or leverage the returned variables to update your UI from the useMutation result. ## Via the UI This is the simpler variant, as it doesn't interact with the cache directly. tsx const addTodoMutation = useMutation({ mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }), // make sure to _return_ the Promise from the query invalidation // so that the mutation stays in `pending` state until the refetch is finished onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), }) const { isPending, submittedAt, variables, mutate, isError } = addTodoMutation const addTodoMutation = useMutation({ mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }), // make sure to _return_ the Promise from the query invalidation // so that the mutation stays in `pending` state until the refetch is finished onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), }) const { isPending, submittedAt, variables, mutate, isError } = addTodoMutation you will then have access to addTodoMutation.variables, which contain the added todo. In your UI list, where the query is rendered, you can append another item to the list while the mutation isPending: tsx <ul> {todoQuery.items.map((todo) => ( <li key={todo.id}>{todo.text}</li> ))} {isPending && <li style={{ opacity: 0.5 }}>{variables}</li>} </ul> <ul> {todoQuery.items.map((todo) => ( <li key={todo.id}>{todo.text}</li> ))} {isPending && <li style={{ opacity: 0.5 }}>{variables}</li>} </ul> We're rendering a temporary item with a different opacity as long as the mutation is pending. Once it completes, the item will automatically no longer be rendered. Given that the refetch succeeded, we should see the item as a "normal item" in our list. If the mutation errors, the item will also disappear. But we could continue to show it, if we want, by checking for the isError state of the mutation. variables are _not_ cleared when the mutation errors, so we can still access them, maybe even show a retry button: tsx { isError && ( <li style={{ color: 'red' }}> {variables} <button onClick={() => mutate(variables)}>Retry</button> </li> ) } { isError && ( <li style={{ color: 'red' }}> {variables} <button onClick={() => mutate(variables)}>Retry</button> </li> ) } ### If the mutation and the query don't live in the same component This approach works very well if the mutation and the query live in the same component, However, you also get access to all mutations in other components via the dedicated useMutationState hook. It is best combined with a mutationKey: tsx // somewhere in your app const { mutate } = useMutation({ mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }), onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), mutationKey: ['addTodo'], }) // access variables somewhere else const variables = useMutationState<string>({ filters: { mutationKey: ['addTodo'], status: 'pending' }, select: (mutation) => mutation.state.variables, }) // somewhere in your app const { mutate } = useMutation({ mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }), onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), mutationKey: ['addTodo'], }) // access variables somewhere else const variables = useMutationState<string>({ filters: { mutationKey: ['addTodo'], status: 'pending' }, select: (mutation) => mutation.state.variables, }) variables will be an Array, because there might be multiple mutations running at the same time. If we need a unique key for the items, we can also select mutation.state.submittedAt. This will even make displaying concurrent optimistic updates a breeze. ## Via the cache When you optimistically update your state before performing a mutation, there is a chance that the mutation will fail. In most of these failure cases, you can just trigger a refetch for your optimistic queries to revert them to their true server state. In some circumstances though, refetching may not work correctly and the mutation error could represent some type of server issue that won't make it possible to refetch. In this event, you can instead choose to roll back your update. To do this, useMutation's onMutate handler option allows you to return a value that will later be passed to both onError and onSettled handlers as the last argument. In most cases, it is most useful to pass a rollback function. ### Updating a list of todos when adding a new todo tsx const queryClient = useQueryClient() useMutation({ mutationFn: updateTodo, // When mutate is called: onMutate: async (newTodo) => { // Cancel any outgoing refetches // (so they don't overwrite our optimistic update) await queryClient.cancelQueries({ queryKey: ['todos'] }) // Snapshot the previous value const previousTodos = queryClient.getQueryData(['todos']) // Optimistically update to the new value queryClient.setQueryData(['todos'], (old) => [...old, newTodo]) // Return a context object with the snapshotted value return { previousTodos } }, // If the mutation fails, // use the context returned from onMutate to roll back onError: (err, newTodo, context) => { queryClient.setQueryData(['todos'], context.previousTodos) }, // Always refetch after error or success: onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), }) const queryClient = useQueryClient() useMutation({ mutationFn: updateTodo, // When mutate is called: onMutate: async (newTodo) => { // Cancel any outgoing refetches // (so they don't overwrite our optimistic update) await queryClient.cancelQueries({ queryKey: ['todos'] }) // Snapshot the previous value const previousTodos = queryClient.getQueryData(['todos']) // Optimistically update to the new value queryClient.setQueryData(['todos'], (old) => [...old, newTodo]) // Return a context object with the snapshotted value return { previousTodos } }, // If the mutation fails, // use the context returned from onMutate to roll back onError: (err, newTodo, context) => { queryClient.setQueryData(['todos'], context.previousTodos) }, // Always refetch after error or success: onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), }) ### Updating a single todo tsx useMutation({ mutationFn: updateTodo, // When mutate is called: onMutate: async (newTodo) => { // Cancel any outgoing refetches // (so they don't overwrite our optimistic update) await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] }) // Snapshot the previous value const previousTodo = queryClient.getQueryData(['todos', newTodo.id]) // Optimistically update to the new value queryClient.setQueryData(['todos', newTodo.id], newTodo) // Return a context with the previous and new todo return { previousTodo, newTodo } }, // If the mutation fails, use the context we returned above onError: (err, newTodo, context) => { queryClient.setQueryData( ['todos', context.newTodo.id], context.previousTodo, ) }, // Always refetch after error or success: onSettled: (newTodo) => queryClient.invalidateQueries({ queryKey: ['todos', newTodo.id] }), }) useMutation({ mutationFn: updateTodo, // When mutate is called: onMutate: async (newTodo) => { // Cancel any outgoing refetches // (so they don't overwrite our optimistic update) await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] }) // Snapshot the previous value const previousTodo = queryClient.getQueryData(['todos', newTodo.id]) // Optimistically update to the new value queryClient.setQueryData(['todos', newTodo.id], newTodo) // Return a context with the previous and new todo return { previousTodo, newTodo } }, // If the mutation fails, use the context we returned above onError: (err, newTodo, context) => { queryClient.setQueryData( ['todos', context.newTodo.id], context.previousTodo, ) }, // Always refetch after error or success: onSettled: (newTodo) => queryClient.invalidateQueries({ queryKey: ['todos', newTodo.id] }), }) You can also use the onSettled function in place of the separate onError and onSuccess handlers if you wish: tsx useMutation({ mutationFn: updateTodo, // ... onSettled: async (newTodo, error, variables, context) => { if (error) { // do something } }, }) useMutation({ mutationFn: updateTodo, // ... onSettled: async (newTodo, error, variables, context) => { if (error) { // do something } }, }) ## When to use what If you only have one place where the optimistic result should be shown, using variables and updating the UI directly is the approach that requires less code and is generally easier to reason about. For example, you don't need to handle rollbacks at all. However, if you have multiple places on the screen that would require to know about the update, manipulating the cache directly will take care of this for you automatically. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/query-cancellation TanStack Query provides each query function with an AbortSignal instance. When a query becomes out-of-date or inactive, this signal will become aborted. This means that all queries are cancellable, and you can respond to the cancellation inside your query function if desired. The best part about this is that it allows you to continue to use normal async/await syntax while getting all the benefits of automatic cancellation. The AbortController API is available in most runtime environments, but if your runtime environment does not support it, you will need to provide a polyfill. There are several available. ## Default behavior By default, queries that unmount or become unused before their promises are resolved are _not_ cancelled. This means that after the promise has resolved, the resulting data will be available in the cache. This is helpful if you've started receiving a query, but then unmount the component before it finishes. If you mount the component again and the query has not been garbage collected yet, data will be available. However, if you consume the AbortSignal, the Promise will be cancelled (e.g. aborting the fetch) and therefore, also the Query must be cancelled. Cancelling the query will result in its state being _reverted_ to its previous state. ## Using fetch tsx const query = useQuery({ queryKey: ['todos'], queryFn: async ({ signal }) => { const todosResponse = await fetch('/todos', { // Pass the signal to one fetch signal, }) const todos = await todosResponse.json() const todoDetails = todos.map(async ({ details }) => { const response = await fetch(details, { // Or pass it to several signal, }) return response.json() }) return Promise.all(todoDetails) }, }) const query = useQuery({ queryKey: ['todos'], queryFn: async ({ signal }) => { const todosResponse = await fetch('/todos', { // Pass the signal to one fetch signal, }) const todos = await todosResponse.json() const todoDetails = todos.map(async ({ details }) => { const response = await fetch(details, { // Or pass it to several signal, }) return response.json() }) return Promise.all(todoDetails) }, }) ## Using axios v0.22.0+ tsx import axios from 'axios' const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => axios.get('/todos', { // Pass the signal to `axios` signal, }), }) import axios from 'axios' const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => axios.get('/todos', { // Pass the signal to `axios` signal, }), }) ### Using axios with version lower than v0.22.0 tsx import axios from 'axios' const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { // Create a new CancelToken source for this request const CancelToken = axios.CancelToken const source = CancelToken.source() const promise = axios.get('/todos', { // Pass the source token to your request cancelToken: source.token, }) // Cancel the request if TanStack Query signals to abort signal?.addEventListener('abort', () => { source.cancel('Query was cancelled by TanStack Query') }) return promise }, }) import axios from 'axios' const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { // Create a new CancelToken source for this request const CancelToken = axios.CancelToken const source = CancelToken.source() const promise = axios.get('/todos', { // Pass the source token to your request cancelToken: source.token, }) // Cancel the request if TanStack Query signals to abort signal?.addEventListener('abort', () => { source.cancel('Query was cancelled by TanStack Query') }) return promise }, }) ## Using XMLHttpRequest tsx const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { return new Promise((resolve, reject) => { var oReq = new XMLHttpRequest() oReq.addEventListener('load', () => { resolve(JSON.parse(oReq.responseText)) }) signal?.addEventListener('abort', () => { oReq.abort() reject() }) oReq.open('GET', '/todos') oReq.send() }) }, }) const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { return new Promise((resolve, reject) => { var oReq = new XMLHttpRequest() oReq.addEventListener('load', () => { resolve(JSON.parse(oReq.responseText)) }) signal?.addEventListener('abort', () => { oReq.abort() reject() }) oReq.open('GET', '/todos') oReq.send() }) }, }) ## Using graphql-request An AbortSignal can be set in the client request method. tsx const client = new GraphQLClient(endpoint) const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { client.request({ document: query, signal }) }, }) const client = new GraphQLClient(endpoint) const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { client.request({ document: query, signal }) }, }) ## Using graphql-request with version lower than v4.0.0 An AbortSignal can be set in the GraphQLClient constructor. tsx const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { const client = new GraphQLClient(endpoint, { signal, }) return client.request(query, variables) }, }) const query = useQuery({ queryKey: ['todos'], queryFn: ({ signal }) => { const client = new GraphQLClient(endpoint, { signal, }) return client.request(query, variables) }, }) ## Manual Cancellation You might want to cancel a query manually. For example, if the request takes a long time to finish, you can allow the user to click a cancel button to stop the request. To do this, you just need to call queryClient.cancelQueries({ queryKey }), which will cancel the query and revert it back to its previous state. If you have consumed the signal passed to the query function, TanStack Query will additionally also cancel the Promise. tsx const query = useQuery({ queryKey: ['todos'], queryFn: async ({ signal }) => { const resp = await fetch('/todos', { signal }) return resp.json() }, }) const queryClient = useQueryClient() return ( <button onClick={(e) => { e.preventDefault() queryClient.cancelQueries({ queryKey: ['todos'] }) }} > Cancel </button> ) const query = useQuery({ queryKey: ['todos'], queryFn: async ({ signal }) => { const resp = await fetch('/todos', { signal }) return resp.json() }, }) const queryClient = useQueryClient() return ( <button onClick={(e) => { e.preventDefault() queryClient.cancelQueries({ queryKey: ['todos'] }) }} > Cancel </button> ) ## Limitations Cancellation does not work when working with Suspense hooks: useSuspenseQuery, useSuspenseQueries and useSuspenseInfiniteQuery. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/scroll-restoration Traditionally, when you navigate to a previously visited page on a web browser, you would find that the page would be scrolled to the exact position where you were before you navigated away from that page. This is called **scroll restoration** and has been in a bit of a regression since web applications have started moving towards client side data fetching. With TanStack Query however, that's no longer the case. Out of the box, "scroll restoration" for all queries (including paginated and infinite queries) Just Works™️ in TanStack Query. The reason for this is that query results are cached and able to be retrieved synchronously when a query is rendered. As long as your queries are being cached long enough (the default time is 5 minutes) and have not been garbage collected, scroll restoration will work out of the box all the time. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/filters Some methods within TanStack Query accept a QueryFilters or MutationFilters object. ## Query Filters A query filter is an object with certain conditions to match a query with: tsx // Cancel all queries await queryClient.cancelQueries() // Remove all inactive queries that begin with `posts` in the key queryClient.removeQueries({ queryKey: ['posts'], type: 'inactive' }) // Refetch all active queries await queryClient.refetchQueries({ type: 'active' }) // Refetch all active queries that begin with `posts` in the key await queryClient.refetchQueries({ queryKey: ['posts'], type: 'active' }) // Cancel all queries await queryClient.cancelQueries() // Remove all inactive queries that begin with `posts` in the key queryClient.removeQueries({ queryKey: ['posts'], type: 'inactive' }) // Refetch all active queries await queryClient.refetchQueries({ type: 'active' }) // Refetch all active queries that begin with `posts` in the key await queryClient.refetchQueries({ queryKey: ['posts'], type: 'active' }) A query filter object supports the following properties: * queryKey?: QueryKey * Set this property to define a query key to match on. * exact?: boolean * If you don't want to search queries inclusively by query key, you can pass the exact: true option to return only the query with the exact query key you have passed. * type?: 'active' | 'inactive' | 'all' * Defaults to all * When set to active it will match active queries. * When set to inactive it will match inactive queries. * stale?: boolean * When set to true it will match stale queries. * When set to false it will match fresh queries. * fetchStatus?: FetchStatus * When set to fetching it will match queries that are currently fetching. * When set to paused it will match queries that wanted to fetch, but have been paused. * When set to idle it will match queries that are not fetching. * predicate?: (query: Query) => boolean * This predicate function will be used as a final filter on all matching queries. If no other filters are specified, this function will be evaluated against every query in the cache. ## Mutation Filters A mutation filter is an object with certain conditions to match a mutation with: tsx // Get the number of all fetching mutations await queryClient.isMutating() // Filter mutations by mutationKey await queryClient.isMutating({ mutationKey: ['post'] }) // Filter mutations using a predicate function await queryClient.isMutating({ predicate: (mutation) => mutation.state.variables?.id === 1, }) // Get the number of all fetching mutations await queryClient.isMutating() // Filter mutations by mutationKey await queryClient.isMutating({ mutationKey: ['post'] }) // Filter mutations using a predicate function await queryClient.isMutating({ predicate: (mutation) => mutation.state.variables?.id === 1, }) A mutation filter object supports the following properties: * mutationKey?: MutationKey * Set this property to define a mutation key to match on. * exact?: boolean * If you don't want to search mutations inclusively by mutation key, you can pass the exact: true option to return only the mutation with the exact mutation key you have passed. * status?: MutationStatus * Allows for filtering mutations according to their status. * predicate?: (mutation: Mutation) => boolean * This predicate function will be used as a final filter on all matching mutations. If no other filters are specified, this function will be evaluated against every mutation in the cache. ## Utils ### matchQuery tsx const isMatching = matchQuery(filters, query) const isMatching = matchQuery(filters, query) Returns a boolean that indicates whether a query matches the provided set of query filters. ### matchMutation tsx const isMatching = matchMutation(filters, mutation) const isMatching = matchMutation(filters, mutation) Returns a boolean that indicates whether a mutation matches the provided set of mutation filters. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/request-waterfalls Application performance is a broad and complex area and while React Query can't make your APIs faster, there are still things to be mindful about in how you use React Query to ensure the best performance. The biggest performance footgun when using React Query, or indeed any data fetching library that lets you fetch data inside of components, is request waterfalls. The rest of this page will explain what they are, how you can spot them and how you can restructure your application or APIs to avoid them. The Prefetching & Router Integration guide builds on this and teaches you how to prefetch data ahead of time when it's not possible or feasible to restructure your application or APIs. The Server Rendering & Hydration guide teaches you how to prefetch data on the server and pass that data down to the client so you don't have to fetch it again. The Advanced Server Rendering guide further teaches you how to apply these patterns to Server Components and Streaming Server Rendering. ## What is a Request Waterfall? A request waterfall is what happens when a request for a resource (code, css, images, data) does not start until _after_ another request for a resource has finished. Consider a web page. Before you can load things like the CSS, JS etc, the browser first needs to load the markup. This is a request waterfall. 1. |-> Markup 2. |-> CSS 2. |-> JS 2. |-> Image 1. |-> Markup 2. |-> CSS 2. |-> JS 2. |-> Image If you fetch your CSS inside a JS file, you now have a double waterfall: 1. |-> Markup 2. |-> JS 3. |-> CSS 1. |-> Markup 2. |-> JS 3. |-> CSS If that CSS uses a background image, it's a triple waterfall: 1. |-> Markup 2. |-> JS 3. |-> CSS 4. |-> Image 1. |-> Markup 2. |-> JS 3. |-> CSS 4. |-> Image The best way to spot and analyze your request waterfalls is usually by opening your browsers devtools "Network" tab. Each waterfall represents at least one roundtrip to the server, unless the resource is locally cached (in practice, some of these waterfalls might represent more than one roundtrip because the browser needs to establish a connection which requires some back and forth, but let's ignore that here). Because of this, the negative effects of request waterfalls are highly dependent on the users latency. Consider the example of the triple waterfall, which actually represents 4 server roundtrips. With 250ms latency, which is not uncommon on 3g networks or in bad network conditions, we end up with a total time of 4\*250=1000ms **only counting latency**. If we were able to flatten that to the first example with only 2 roundtrips, we get 500ms instead, possibly loading that background image in half the time! ## Request Waterfalls & React Query Now let's consider React Query. We'll focus on the case without Server Rendering first. Before we can even start making a query, we need to load the JS, so before we can show that data on the screen, we have a double waterfall: 1. |-> Markup 2. |-> JS 3. |-> Query 1. |-> Markup 2. |-> JS 3. |-> Query With this as a basis, let's look at a few different patterns that can lead to Request Waterfalls in React Query, and how to avoid them. * Single Component Waterfalls / Serial Queries * Nested Component Waterfalls * Code Splitting ### Single Component Waterfalls / Serial Queries When a single component first fetches one query, and then another, that's a request waterfall. This can happen when the second query is a Dependent Query, that is, it depends on data from the first query when fetching: tsx // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) While not always feasible, for optimal performance it's better to restructure your API so you can fetch both of these in a single query. In the example above, instead of first fetching getUserByEmail to be able to getProjectsByUser, introducing a new getProjectsByUserEmail query would flatten the waterfall. > Another way to mitigate dependent queries without restructuring your API is to move the waterfall to the server where latency is lower. This is the idea behind Server Components which are covered in the Advanced Server Rendering guide. Another example of serial queries is when you use React Query with Suspense: tsx function App () { // The following queries will execute in serial, causing separate roundtrips to the server: const usersQuery = useSuspenseQuery({ queryKey: ['users'], queryFn: fetchUsers }) const teamsQuery = useSuspenseQuery({ queryKey: ['teams'], queryFn: fetchTeams }) const projectsQuery = useSuspenseQuery({ queryKey: ['projects'], queryFn: fetchProjects }) // Note that since the queries above suspend rendering, no data // gets rendered until all of the queries finished ... } function App () { // The following queries will execute in serial, causing separate roundtrips to the server: const usersQuery = useSuspenseQuery({ queryKey: ['users'], queryFn: fetchUsers }) const teamsQuery = useSuspenseQuery({ queryKey: ['teams'], queryFn: fetchTeams }) const projectsQuery = useSuspenseQuery({ queryKey: ['projects'], queryFn: fetchProjects }) // Note that since the queries above suspend rendering, no data // gets rendered until all of the queries finished ... } Note that with regular useQuery these would happen in parallel. Luckily, this is easy to fix, by always using the hook useSuspenseQueries when you have multiple suspenseful queries in a component. tsx const [usersQuery, teamsQuery, projectsQuery] = useSuspenseQueries({ queries: [ { queryKey: ['users'], queryFn: fetchUsers }, { queryKey: ['teams'], queryFn: fetchTeams }, { queryKey: ['projects'], queryFn: fetchProjects }, ], }) const [usersQuery, teamsQuery, projectsQuery] = useSuspenseQueries({ queries: [ { queryKey: ['users'], queryFn: fetchUsers }, { queryKey: ['teams'], queryFn: fetchTeams }, { queryKey: ['projects'], queryFn: fetchProjects }, ], }) ### Nested Component Waterfalls Nested Component Waterfalls is when both a parent and a child component contains queries, and the parent does not render the child until its query is done. This can happen both with useQuery and useSuspenseQuery. If the child renders conditionally based on the data in the parent, or if the child relies on some part of the result being passed down as a prop from the parent to make its query, we have a _dependent_ nested component waterfall. Let's first look at an example where the child is **not** dependent on the parent. tsx function Article({ id }) { const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) if (isPending) { return 'Loading article...' } return ( <> <ArticleHeader articleData={articleData} /> <ArticleBody articleData={articleData} /> <Comments id={id} /> </> ) } function Comments({ id }) { const { data, isPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) ... } function Article({ id }) { const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) if (isPending) { return 'Loading article...' } return ( <> <ArticleHeader articleData={articleData} /> <ArticleBody articleData={articleData} /> <Comments id={id} /> </> ) } function Comments({ id }) { const { data, isPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) ... } Note that while <Comments> takes a prop id from the parent, that id is already available when the <Article> renders so there is no reason we could not fetch the comments at the same time as the article. In real world applications, the child might be nested far below the parent and these kinds of waterfalls are often trickier to spot and fix, but for our example, one way to flatten the waterfall would be to hoist the comments query to the parent instead: tsx function Article({ id }) { const { data: articleData, isPending: articlePending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) const { data: commentsData, isPending: commentsPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) if (articlePending) { return 'Loading article...' } return ( <> <ArticleHeader articleData={articleData} /> <ArticleBody articleData={articleData} /> {commentsPending ? ( 'Loading comments...' ) : ( <Comments commentsData={commentsData} /> )} </> ) } function Article({ id }) { const { data: articleData, isPending: articlePending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) const { data: commentsData, isPending: commentsPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) if (articlePending) { return 'Loading article...' } return ( <> <ArticleHeader articleData={articleData} /> <ArticleBody articleData={articleData} /> {commentsPending ? ( 'Loading comments...' ) : ( <Comments commentsData={commentsData} /> )} </> ) } The two queries will now fetch in parallel. Note that if you are using suspense, you'd want to combine these two queries into a single useSuspenseQueries instead. Another way to flatten this waterfall would be to prefetch the comments in the <Article> component, or prefetch both of these queries at the router level on page load or page navigation, read more about this in the Prefetching & Router Integration guide. Next, let's look at a _Dependent Nested Component Waterfall_. tsx function Feed() { const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: getFeed, }) if (isPending) { return 'Loading feed...' } return ( <> {data.map((feedItem) => { if (feedItem.type === 'GRAPH') { return <GraphFeedItem key={feedItem.id} feedItem={feedItem} /> } return <StandardFeedItem key={feedItem.id} feedItem={feedItem} /> })} </> ) } function GraphFeedItem({ feedItem }) { const { data, isPending } = useQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) ... } function Feed() { const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: getFeed, }) if (isPending) { return 'Loading feed...' } return ( <> {data.map((feedItem) => { if (feedItem.type === 'GRAPH') { return <GraphFeedItem key={feedItem.id} feedItem={feedItem} /> } return <StandardFeedItem key={feedItem.id} feedItem={feedItem} /> })} </> ) } function GraphFeedItem({ feedItem }) { const { data, isPending } = useQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) ... } The second query getGraphDataById is dependent on its parent in two different ways. First of all, it doesn't ever happen unless the feedItem is a graph, and second, it needs an id from the parent. 1. |> getFeed() 2. |> getGraphDataById() 1. |> getFeed() 2. |> getGraphDataById() In this example, we can't trivially flatten the waterfall by just hoisting the query to the parent, or even adding prefetching. Just like the dependent query example at the beginning of this guide, one option is to refactor our API to include the graph data in the getFeed query. Another more advanced solution is to leverage Server Components to move the waterfall to the server where latency is lower (read more about this in the Advanced Server Rendering guide) but note that this can be a very big architectural change. You can have good performance even with a few query waterfalls here and there, just know they are a common performance concern and be mindful about them. An especially insidious version is when Code Splitting is involved, let's take a look at this next. ### Code Splitting Splitting an applications JS-code into smaller chunks and only loading the necessary parts is usually a critical step in achieving good performance. It does have a downside however, in that it often introduces request waterfalls. When that code split code also has a query inside it, this problem is worsened further. Consider this a slightly modified version of the Feed example. tsx // This lazy loads the GraphFeedItem component, meaning // it wont start loading until something renders it const GraphFeedItem = React.lazy(() => import('./GraphFeedItem')) function Feed() { const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: getFeed, }) if (isPending) { return 'Loading feed...' } return ( <> {data.map((feedItem) => { if (feedItem.type === 'GRAPH') { return <GraphFeedItem key={feedItem.id} feedItem={feedItem} /> } return <StandardFeedItem key={feedItem.id} feedItem={feedItem} /> })} </> ) } // GraphFeedItem.tsx function GraphFeedItem({ feedItem }) { const { data, isPending } = useQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) ... } // This lazy loads the GraphFeedItem component, meaning // it wont start loading until something renders it const GraphFeedItem = React.lazy(() => import('./GraphFeedItem')) function Feed() { const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: getFeed, }) if (isPending) { return 'Loading feed...' } return ( <> {data.map((feedItem) => { if (feedItem.type === 'GRAPH') { return <GraphFeedItem key={feedItem.id} feedItem={feedItem} /> } return <StandardFeedItem key={feedItem.id} feedItem={feedItem} /> })} </> ) } // GraphFeedItem.tsx function GraphFeedItem({ feedItem }) { const { data, isPending } = useQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) ... } This example has a double waterfall, looking like this: 1. |> getFeed() 2. |> JS for <GraphFeedItem> 3. |> getGraphDataById() 1. |> getFeed() 2. |> JS for <GraphFeedItem> 3. |> getGraphDataById() But that's just looking at the code from the example, if we consider what the first page load of this page looks like, we actually have to complete 5 round trips to the server before we can render the graph! 1. |> Markup 2. |> JS for <Feed> 3. |> getFeed() 4. |> JS for <GraphFeedItem> 5. |> getGraphDataById() 1. |> Markup 2. |> JS for <Feed> 3. |> getFeed() 4. |> JS for <GraphFeedItem> 5. |> getGraphDataById() Note that this looks a bit different when server rendering, we will explore that further in the Server Rendering & Hydration guide. Also note that it's not uncommon for the route that contains <Feed> to also be code split, which could add yet another hop. In the code split case, it might actually help to hoist the getGraphDataById query to the <Feed> component and make it conditional, or add a conditional prefetch. That query could then be fetched in parallel with the code, turning the example part into this: 1. |> getFeed() 2. |> getGraphDataById() 2. |> JS for <GraphFeedItem> 1. |> getFeed() 2. |> getGraphDataById() 2. |> JS for <GraphFeedItem> This is very much a tradeoff however. You are now including the data fetching code for getGraphDataById in the same bundle as <Feed>, so evaluate what is best for your case. Read more about how to do this in the Prefetching & Router Integration guide. > The tradeoff between: > > * Include all data fetching code in the main bundle, even if we seldom use it > * Put the data fetching code in the code split bundle, but with a request waterfall > > is not great and has been one of the motivations for Server Components. With Server Components, it's possible to avoid both, read more about how this applies to React Query in the Advanced Server Rendering guide. ## Summary and takeaways Request Waterfalls are a very common and complex performance concern with many tradeoffs. There are many ways to accidentally introduce them into your application: * Adding a query to a child, not realizing a parent already has a query * Adding a query to a parent, not realizing a child already has a query * Moving a component with descendants that has a query to a new parent with an ancestor that has a query * Etc.. Because of this accidental complexity, it pays off to be mindful of waterfalls and regularly examine your application looking for them (a good way is to examine the Network tab every now and then!). You don't necessarily have to flatten them all to have good performance, but keep an eye out for the high impact ones. In the next guide, we'll look at more ways to flatten waterfalls, by leveraging Prefetching & Router Integration. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/prefetching When you know or suspect that a certain piece of data will be needed, you can use prefetching to populate the cache with that data ahead of time, leading to a faster experience. There are a few different prefetching patterns: 1. In event handlers 2. In components 3. Via router integration 4. During Server Rendering (another form of router integration) In this guide, we'll take a look at the first three, while the fourth will be covered in depth in the Server Rendering & Hydration guide and the Advanced Server Rendering guide. One specific use of prefetching is to avoid Request Waterfalls, for an in-depth background and explanation of those, see the Performance & Request Waterfalls guide. ## prefetchQuery & prefetchInfiniteQuery Before jumping into the different specific prefetch patterns, let's look at the prefetchQuery and prefetchInfiniteQuery functions. First a few basics: * Out of the box, these functions use the default staleTime configured for the queryClient to determine whether existing data in the cache is fresh or needs to be fetched again * You can also pass a specific staleTime like this: prefetchQuery({ queryKey: \['todos'\], queryFn: fn, staleTime: 5000 }) * This staleTime is only used for the prefetch, you still need to set it for any useQuery call as well * If you want to ignore staleTime and instead always return data if it's available in the cache, you can use the ensureQueryData function. * Tip: If you are prefetching on the server, set a default staleTime higher than 0 for that queryClient to avoid having to pass in a specific staleTime to each prefetch call * If no instances of useQuery appear for a prefetched query, it will be deleted and garbage collected after the time specified in gcTime * These functions returns Promise<void> and thus never return query data. If that's something you need, use fetchQuery/fetchInfiniteQuery instead. * The prefetch functions never throws errors because they usually try to fetch again in a useQuery which is a nice graceful fallback. If you need to catch errors, use fetchQuery/fetchInfiniteQuery instead. This is how you use prefetchQuery: tsx const prefetchTodos = async () => { // The results of this query will be cached like a normal query await queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos, }) } const prefetchTodos = async () => { // The results of this query will be cached like a normal query await queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos, }) } Infinite Queries can be prefetched like regular Queries. Per default, only the first page of the Query will be prefetched and will be stored under the given QueryKey. If you want to prefetch more than one page, you can use the pages option, in which case you also have to provide a getNextPageParam function: tsx const prefetchProjects = async () => { // The results of this query will be cached like a normal query await queryClient.prefetchInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, pages: 3, // prefetch the first 3 pages }) } const prefetchProjects = async () => { // The results of this query will be cached like a normal query await queryClient.prefetchInfiniteQuery({ queryKey: ['projects'], queryFn: fetchProjects, initialPageParam: 0, getNextPageParam: (lastPage, pages) => lastPage.nextCursor, pages: 3, // prefetch the first 3 pages }) } Next, let's look at how you can use these and other ways to prefetch in different situations. ## Prefetch in event handlers A straightforward form of prefetching is doing it when the user interacts with something. In this example we'll use queryClient.prefetchQuery to start a prefetch on onMouseEnter or onFocus. tsx function ShowDetailsButton() { const queryClient = useQueryClient() const prefetch = () => { queryClient.prefetchQuery({ queryKey: ['details'], queryFn: getDetailsData, // Prefetch only fires when data is older than the staleTime, // so in a case like this you definitely want to set one staleTime: 60000, }) } return ( <button onMouseEnter={prefetch} onFocus={prefetch} onClick={...}> Show Details </button> ) } function ShowDetailsButton() { const queryClient = useQueryClient() const prefetch = () => { queryClient.prefetchQuery({ queryKey: ['details'], queryFn: getDetailsData, // Prefetch only fires when data is older than the staleTime, // so in a case like this you definitely want to set one staleTime: 60000, }) } return ( <button onMouseEnter={prefetch} onFocus={prefetch} onClick={...}> Show Details </button> ) } ## Prefetch in components Prefetching during the component lifecycle is useful when we know some child or descendant will need a particular piece of data, but we can't render that until some other query has finished loading. Let's borrow an example from the Request Waterfall guide to explain: tsx function Article({ id }) { const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) if (isPending) { return 'Loading article...' } return ( <> <ArticleHeader articleData={articleData} /> <ArticleBody articleData={articleData} /> <Comments id={id} /> </> ) } function Comments({ id }) { const { data, isPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) ... } function Article({ id }) { const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) if (isPending) { return 'Loading article...' } return ( <> <ArticleHeader articleData={articleData} /> <ArticleBody articleData={articleData} /> <Comments id={id} /> </> ) } function Comments({ id }) { const { data, isPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) ... } This results in a request waterfall looking like this: 1. |> getArticleById() 2. |> getArticleCommentsById() 1. |> getArticleById() 2. |> getArticleCommentsById() As mentioned in that guide, one way to flatten this waterfall and improve performance is to hoist the getArticleCommentsById query to the parent and pass down the result as a prop, but what if this is not feasible or desirable, for example when the components are unrelated and have multiple levels between them? In that case, we can instead prefetch the query in the parent. The simplest way to do this is to use a query but ignore the result: tsx function Article({ id }) { const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) // Prefetch useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, // Optional optimization to avoid rerenders when this query changes: notifyOnChangeProps: [], }) if (isPending) { return 'Loading article...' } return ( <> <ArticleHeader articleData={articleData} /> <ArticleBody articleData={articleData} /> <Comments id={id} /> </> ) } function Comments({ id }) { const { data, isPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) ... } function Article({ id }) { const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: getArticleById, }) // Prefetch useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, // Optional optimization to avoid rerenders when this query changes: notifyOnChangeProps: [], }) if (isPending) { return 'Loading article...' } return ( <> <ArticleHeader articleData={articleData} /> <ArticleBody articleData={articleData} /> <Comments id={id} /> </> ) } function Comments({ id }) { const { data, isPending } = useQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) ... } This starts fetching 'article-comments' immediately and flattens the waterfall: 1. |> getArticleById() 1. |> getArticleCommentsById() 1. |> getArticleById() 1. |> getArticleCommentsById() If you want to prefetch together with Suspense, you will have to do things a bit differently. You can't use useSuspenseQueries to prefetch, since the prefetch would block the component from rendering. You also can not use useQuery for the prefetch, because that wouldn't start the prefetch until after suspenseful query had resolved. For this scenario, you can use the usePrefetchQuery or the usePrefetchInfiniteQuery hooks available in the library. You can now use useSuspenseQuery in the component that actually needs the data. You _might_ want to wrap this later component in its own <Suspense> boundary so the "secondary" query we are prefetching does not block rendering of the "primary" data. tsx function ArticleLayout({ id }) { usePrefetchQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) return ( <Suspense fallback="Loading article"> <Article id={id} /> </Suspense> ) } function Article({ id }) { const { data: articleData, isPending } = useSuspenseQuery({ queryKey: ['article', id], queryFn: getArticleById, }) ... } function ArticleLayout({ id }) { usePrefetchQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) return ( <Suspense fallback="Loading article"> <Article id={id} /> </Suspense> ) } function Article({ id }) { const { data: articleData, isPending } = useSuspenseQuery({ queryKey: ['article', id], queryFn: getArticleById, }) ... } Another way is to prefetch inside of the query function. This makes sense if you know that every time an article is fetched it's very likely comments will also be needed. For this, we'll use queryClient.prefetchQuery: tsx const queryClient = useQueryClient() const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: (...args) => { queryClient.prefetchQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) return getArticleById(...args) }, }) const queryClient = useQueryClient() const { data: articleData, isPending } = useQuery({ queryKey: ['article', id], queryFn: (...args) => { queryClient.prefetchQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) return getArticleById(...args) }, }) Prefetching in an effect also works, but note that if you are using useSuspenseQuery in the same component, this effect wont run until _after_ the query finishes which might not be what you want. tsx const queryClient = useQueryClient() useEffect(() => { queryClient.prefetchQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) }, [queryClient, id]) const queryClient = useQueryClient() useEffect(() => { queryClient.prefetchQuery({ queryKey: ['article-comments', id], queryFn: getArticleCommentsById, }) }, [queryClient, id]) To recap, if you want to prefetch a query during the component lifecycle, there are a few different ways to do it, pick the one that suits your situation best: * Prefetch before a suspense boundary using usePrefetchQuery or usePrefetchInfiniteQuery hooks * Use useQuery or useSuspenseQueries and ignore the result * Prefetch inside the query function * Prefetch in an effect Let's look at a slightly more advanced case next. ### Dependent Queries & Code Splitting Sometimes we want to prefetch conditionally, based on the result of another fetch. Consider this example borrowed from the Performance & Request Waterfalls guide: tsx // This lazy loads the GraphFeedItem component, meaning // it wont start loading until something renders it const GraphFeedItem = React.lazy(() => import('./GraphFeedItem')) function Feed() { const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: getFeed, }) if (isPending) { return 'Loading feed...' } return ( <> {data.map((feedItem) => { if (feedItem.type === 'GRAPH') { return <GraphFeedItem key={feedItem.id} feedItem={feedItem} /> } return <StandardFeedItem key={feedItem.id} feedItem={feedItem} /> })} </> ) } // GraphFeedItem.tsx function GraphFeedItem({ feedItem }) { const { data, isPending } = useQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) ... } // This lazy loads the GraphFeedItem component, meaning // it wont start loading until something renders it const GraphFeedItem = React.lazy(() => import('./GraphFeedItem')) function Feed() { const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: getFeed, }) if (isPending) { return 'Loading feed...' } return ( <> {data.map((feedItem) => { if (feedItem.type === 'GRAPH') { return <GraphFeedItem key={feedItem.id} feedItem={feedItem} /> } return <StandardFeedItem key={feedItem.id} feedItem={feedItem} /> })} </> ) } // GraphFeedItem.tsx function GraphFeedItem({ feedItem }) { const { data, isPending } = useQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) ... } As noted over in that guide, this example leads to the following double request waterfall: 1. |> getFeed() 2. |> JS for <GraphFeedItem> 3. |> getGraphDataById() 1. |> getFeed() 2. |> JS for <GraphFeedItem> 3. |> getGraphDataById() If we can not restructure our API so getFeed() also returns the getGraphDataById() data when necessary, there is no way to get rid of the getFeed->getGraphDataById waterfall, but by leveraging conditional prefetching, we can at least load the code and data in parallel. Just like described above, there are multiple ways to do this, but for this example, we'll do it in the query function: tsx function Feed() { const queryClient = useQueryClient() const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: async (...args) => { const feed = await getFeed(...args) for (const feedItem of feed) { if (feedItem.type === 'GRAPH') { queryClient.prefetchQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) } } return feed } }) ... } function Feed() { const queryClient = useQueryClient() const { data, isPending } = useQuery({ queryKey: ['feed'], queryFn: async (...args) => { const feed = await getFeed(...args) for (const feedItem of feed) { if (feedItem.type === 'GRAPH') { queryClient.prefetchQuery({ queryKey: ['graph', feedItem.id], queryFn: getGraphDataById, }) } } return feed } }) ... } This would load the code and data in parallel: 1. |> getFeed() 2. |> JS for <GraphFeedItem> 2. |> getGraphDataById() 1. |> getFeed() 2. |> JS for <GraphFeedItem> 2. |> getGraphDataById() There is a tradeoff however, in that the code for getGraphDataById is now included in the parent bundle instead of in JS for <GraphFeedItem> so you'll need to determine what's the best performance tradeoff on a case by case basis. If GraphFeedItem are likely, it's probably worth to include the code in the parent. If they are exceedingly rare, it's probably not. ## Router Integration Because data fetching in the component tree itself can easily lead to request waterfalls and the different fixes for that can be cumbersome as they accumulate throughout the application, an attractive way to do prefetching is integrating it at the router level. In this approach, you explicitly declare for each _route_ what data is going to be needed for that component tree, ahead of time. Because Server Rendering has traditionally needed all data to be loaded before rendering starts, this has been the dominating approach for SSR'd apps for a long time. This is still a common approach and you can read more about it in the Server Rendering & Hydration guide. For now, let's focus on the client side case and look at an example of how you can make this work with Tanstack Router. These examples leave out a lot of setup and boilerplate to stay concise, you can check out a full React Query example over in the Tanstack Router docs. When integrating at the router level, you can choose to either _block_ rendering of that route until all data is present, or you can start a prefetch but not await the result. That way, you can start rendering the route as soon as possible. You can also mix these two approaches and await some critical data, but start rendering before all the secondary data has finished loading. In this example, we'll configure an /article route to not render until the article data has finished loading, as well as start prefetching comments as soon as possible, but not block rendering the route if comments haven't finished loading yet. tsx const queryClient = new QueryClient() const routerContext = new RouterContext() const rootRoute = routerContext.createRootRoute({ component: () => { ... } }) const articleRoute = new Route({ getParentRoute: () => rootRoute, path: 'article', beforeLoad: () => { return { articleQueryOptions: { queryKey: ['article'], queryFn: fetchArticle }, commentsQueryOptions: { queryKey: ['comments'], queryFn: fetchComments }, } }, loader: async ({ context: { queryClient }, routeContext: { articleQueryOptions, commentsQueryOptions }, }) => { // Fetch comments asap, but don't block queryClient.prefetchQuery(commentsQueryOptions) // Don't render the route at all until article has been fetched await queryClient.prefetchQuery(articleQueryOptions) }, component: ({ useRouteContext }) => { const { articleQueryOptions, commentsQueryOptions } = useRouteContext() const articleQuery = useQuery(articleQueryOptions) const commentsQuery = useQuery(commentsQueryOptions) return ( ... ) }, errorComponent: () => 'Oh crap!', }) const queryClient = new QueryClient() const routerContext = new RouterContext() const rootRoute = routerContext.createRootRoute({ component: () => { ... } }) const articleRoute = new Route({ getParentRoute: () => rootRoute, path: 'article', beforeLoad: () => { return { articleQueryOptions: { queryKey: ['article'], queryFn: fetchArticle }, commentsQueryOptions: { queryKey: ['comments'], queryFn: fetchComments }, } }, loader: async ({ context: { queryClient }, routeContext: { articleQueryOptions, commentsQueryOptions }, }) => { // Fetch comments asap, but don't block queryClient.prefetchQuery(commentsQueryOptions) // Don't render the route at all until article has been fetched await queryClient.prefetchQuery(articleQueryOptions) }, component: ({ useRouteContext }) => { const { articleQueryOptions, commentsQueryOptions } = useRouteContext() const articleQuery = useQuery(articleQueryOptions) const commentsQuery = useQuery(commentsQueryOptions) return ( ... ) }, errorComponent: () => 'Oh crap!', }) Integration with other routers is also possible, see the React Router example for another demonstration. ## Manually Priming a Query If you already have the data for your query synchronously available, you don't need to prefetch it. You can just use the Query Client's setQueryData method to directly add or update a query's cached result by key. tsx queryClient.setQueryData(['todos'], todos) queryClient.setQueryData(['todos'], todos) ## Further reading For a deep-dive on how to get data into your Query Cache before you fetch, have a look at #17: Seeding the Query Cache from the Community Resources. Integrating with Server Side routers and frameworks is very similar to what we just saw, with the addition that the data has to passed from the server to the client to be hydrated into the cache there. To learn how, continue on to the Server Rendering & Hydration guide. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/ssr In this guide you'll learn how to use React Query with server rendering. See the guide on Prefetching & Router Integration for some background. You might also want to check out the Performance & Request Waterfalls guide before that. For advanced server rendering patterns, such as streaming, Server Components and the new Next.js app router, see the Advanced Server Rendering guide. If you just want to see some code, you can skip ahead to the Full Next.js pages router example or the Full Remix example below. ## Server Rendering & React Query So what is server rendering anyway? The rest of this guide will assume you are familiar with the concept, but let's spend some time to look at how it relates to React Query. Server rendering is the act of generating the initial html on the server, so that the user has some content to look at as soon as the page loads. This can happen on demand when a page is requested (SSR). It can also happen ahead of time either because a previous request was cached, or at build time (SSG). If you've read the Request Waterfalls guide, you might remember this: 1. |-> Markup (without content) 2. |-> JS 3. |-> Query 1. |-> Markup (without content) 2. |-> JS 3. |-> Query With a client rendered application, these are the minimum 3 server roundtrips you will need to make before getting any content on the screen for the user. One way of viewing server rendering is that it turns the above into this: 1. |-> Markup (with content AND initial data) 2. |-> JS 1. |-> Markup (with content AND initial data) 2. |-> JS As soon as **1.** is complete, the user can see the content and when **2.** finishes, the page is interactive and clickable. Because the markup also contains the initial data we need, step **3.** does not need to run on the client at all, at least until you want to revalidate the data for some reason. This is all from the clients perspective. On the server, we need to **prefetch** that data before we generate/render the markup, we need to **dehydrate** that data into a serializable format we can embed in the markup, and on the client we need to **hydrate** that data into a React Query cache so we can avoid doing a new fetch on the client. Read on to learn how to implement these three steps with React Query. ## A quick note on Suspense This guide uses the regular useQuery API. While we don't necessarily recommend it, it is possible to replace this with useSuspenseQuery instead **as long as you always prefetch all your queries**. The upside is that you get to use <Suspense> for loading states on the client. If you do forget to prefetch a query when you are using useSuspenseQuery, the consequences will depend on the framework you are using. In some cases, the data will Suspend and get fetched on the server but never be hydrated to the client, where it will fetch again. In these cases you will get a markup hydration mismatch, because the server and the client tried to render different things. ## Initial setup The first steps of using React Query is always to create a queryClient and wrap the application in a <QueryClientProvider>. When doing server rendering, it's important to create the queryClient instance **inside of your app**, in React state (an instance ref works fine too). **This ensures that data is not shared between different users and requests**, while still only creating the queryClient once per component lifecycle. Next.js pages router: tsx // _app.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' // NEVER DO THIS: // const queryClient = new QueryClient() // // Creating the queryClient at the file root level makes the cache shared // between all requests and means _all_ data gets passed to _all_ users. // Besides being bad for performance, this also leaks any sensitive data. export default function MyApp({ Component, pageProps }) { // Instead do this, which ensures each request has its own cache: const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> ) } // _app.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' // NEVER DO THIS: // const queryClient = new QueryClient() // // Creating the queryClient at the file root level makes the cache shared // between all requests and means _all_ data gets passed to _all_ users. // Besides being bad for performance, this also leaks any sensitive data. export default function MyApp({ Component, pageProps }) { // Instead do this, which ensures each request has its own cache: const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> ) } Remix: tsx // app/root.tsx import { Outlet } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp() { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Outlet /> </QueryClientProvider> ) } // app/root.tsx import { Outlet } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp() { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Outlet /> </QueryClientProvider> ) } ## Get started fast with initialData The quickest way to get started is to not involve React Query at all when it comes to prefetching and not use the dehydrate/hydrate APIs. What you do instead is passing the raw data in as the initialData option to useQuery. Let's look at an example using Next.js pages router, using getServerSideProps. tsx export async function getServerSideProps() { const posts = await getPosts() return { props: { posts } } } function Posts(props) { const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: props.posts, }) // ... } export async function getServerSideProps() { const posts = await getPosts() return { props: { posts } } } function Posts(props) { const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: props.posts, }) // ... } This also works with getStaticProps or even the older getInitialProps and the same pattern can be applied in any other framework that has equivalent functions. This is what the same example looks like with Remix: tsx export async function loader() { const posts = await getPosts() return json({ posts }) } function Posts() { const { posts } = useLoaderData<typeof loader>() const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: posts, }) // ... } export async function loader() { const posts = await getPosts() return json({ posts }) } function Posts() { const { posts } = useLoaderData<typeof loader>() const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: posts, }) // ... } The setup is minimal and this can be a quick solution for some cases, but there are a **few tradeoffs to consider** when compared to the full approach: * If you are calling useQuery in a component deeper down in the tree you need to pass the initialData down to that point * If you are calling useQuery with the same query in multiple locations, passing initialData to only one of them can be brittle and break when your app changes since. If you remove or move the component that has the useQuery with initialData, the more deeply nested useQuery might no longer have any data. Passing initialData to **all** queries that needs it can also be cumbersome. * There is no way to know at what time the query was fetched on the server, so dataUpdatedAt and determining if the query needs refetching is based on when the page loaded instead * If there is already data in the cache for a query, initialData will never overwrite this data, **even if the new data is fresher than the old one**. * To understand why this is especially bad, consider the getServerSideProps example above. If you navigate back and forth to a page several times, getServerSideProps would get called each time and fetch new data, but because we are using the initialData option, the client cache and data would never be updated. Setting up the full hydration solution is straightforward and does not have these drawbacks, this will be the focus for the rest of the documentation. ## Using the Hydration APIs With just a little more setup, you can use a queryClient to prefetch queries during a preload phase, pass a serialized version of that queryClient to the rendering part of the app and reuse it there. This avoids the drawbacks above. Feel free to skip ahead for full Next.js pages router and Remix examples, but at a general level these are the extra steps: * In the framework loader function, create a const queryClient = new QueryClient(options) * In the loader function, do await queryClient.prefetchQuery(...) for each query you want to prefetch * You want to use await Promise.all(...) to fetch the queries in parallel when possible * It's fine to have queries that aren't prefetched. These wont be server rendered, instead they will be fetched on the client after the application is interactive. This can be great for content that are shown only after user interaction, or is far down on the page to avoid blocking more critical content. * From the loader, return dehydrate(queryClient), note that the exact syntax to return this differs between frameworks * Wrap your tree with <HydrationBoundary state={dehydratedState}> where dehydratedState comes from the framework loader. How you get dehydratedState also differs between frameworks. * This can be done for each route, or at the top of the application to avoid boilerplate, see examples > An interesting detail is that there are actually _three_ queryClients involved. The framework loaders are a form of "preloading" phase that happens before rendering, and this phase has its own queryClient that does the prefetching. The dehydrated result of this phase gets passed to **both** the server rendering process **and** the client rendering process which each has its own queryClient. This ensures they both start with the same data so they can return the same markup. > Server Components are another form of "preloading" phase, that can also "preload" (pre-render) parts of a React component tree. Read more in the Advanced Server Rendering guide. ### Full Next.js pages router example > For app router documentation, see the Advanced Server Rendering guide. Initial setup: tsx // _app.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> ) } // _app.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> ) } In each route: tsx // pages/posts.tsx import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' // This could also be getServerSideProps export async function getStaticProps() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return { props: { dehydratedState: dehydrate(queryClient), }, } } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } // pages/posts.tsx import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' // This could also be getServerSideProps export async function getStaticProps() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return { props: { dehydratedState: dehydrate(queryClient), }, } } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } ### Full Remix example Initial setup: tsx // app/root.tsx import { Outlet } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp() { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Outlet /> </QueryClientProvider> ) } // app/root.tsx import { Outlet } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp() { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Outlet /> </QueryClientProvider> ) } In each route, note that it's fine to do this in nested routes too: tsx // app/routes/posts.tsx import { json } from '@remix-run/node' import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' export async function loader() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return json({ dehydratedState: dehydrate(queryClient) }) } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute() { const { dehydratedState } = useLoaderData<typeof loader>() return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } // app/routes/posts.tsx import { json } from '@remix-run/node' import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' export async function loader() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return json({ dehydratedState: dehydrate(queryClient) }) } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute() { const { dehydratedState } = useLoaderData<typeof loader>() return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } ## Optional - Remove boilerplate Having this part in every route might seem like a lot of boilerplate: tsx export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } While there is nothing wrong with this approach, if you want to get rid of this boilerplate, here's how you can modify your setup in Next.js: tsx // _app.tsx import { HydrationBoundary, QueryClient, QueryClientProvider, } from '@tanstack/react-query' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState(() => new QueryClient()) return ( <QueryClientProvider client={queryClient}> <HydrationBoundary state={pageProps.dehydratedState}> <Component {...pageProps} /> </HydrationBoundary> </QueryClientProvider> ) } // pages/posts.tsx // Remove PostsRoute with the HydrationBoundary and instead export Posts directly: export default function Posts() { ... } // _app.tsx import { HydrationBoundary, QueryClient, QueryClientProvider, } from '@tanstack/react-query' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState(() => new QueryClient()) return ( <QueryClientProvider client={queryClient}> <HydrationBoundary state={pageProps.dehydratedState}> <Component {...pageProps} /> </HydrationBoundary> </QueryClientProvider> ) } // pages/posts.tsx // Remove PostsRoute with the HydrationBoundary and instead export Posts directly: export default function Posts() { ... } With Remix, this is a little bit more involved, we recommend checking out the use-dehydrated-state package. ## Prefetching dependent queries Over in the Prefetching guide we learned how to prefetch dependent queries, but how do we do this in framework loaders? Consider the following code, taken from the Dependent Queries guide: tsx // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) How would we prefetch this so it can be server rendered? Here's an example: tsx // For Remix, rename this to loader instead export async function getServerSideProps() { const queryClient = new QueryClient() const user = await queryClient.fetchQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) if (user?.userId) { await queryClient.prefetchQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, }) } // For Remix: // return json({ dehydratedState: dehydrate(queryClient) }) return { props: { dehydratedState: dehydrate(queryClient) } } } // For Remix, rename this to loader instead export async function getServerSideProps() { const queryClient = new QueryClient() const user = await queryClient.fetchQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) if (user?.userId) { await queryClient.prefetchQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, }) } // For Remix: // return json({ dehydratedState: dehydrate(queryClient) }) return { props: { dehydratedState: dehydrate(queryClient) } } } This can get more complex of course, but since these loader functions are just JavaScript, you can use the full power of the language to build your logic. Make sure you prefetch all queries that you want to be server rendered. ## Error handling React Query defaults to a graceful degradation strategy. This means: * queryClient.prefetchQuery(...) never throws errors * dehydrate(...) only includes successful queries, not failed ones This will lead to any failed queries being retried on the client and that the server rendered output will include loading states instead of the full content. While a good default, sometimes this is not what you want. When critical content is missing, you might want to respond with a 404 or 500 status code depending on the situation. For these cases, use queryClient.fetchQuery(...) instead, which will throw errors when it fails, letting you handle things in a suitable way. tsx let result try { result = await queryClient.fetchQuery(...) } catch (error) { // Handle the error, refer to your framework documentation } // You might also want to check and handle any invalid `result` here let result try { result = await queryClient.fetchQuery(...) } catch (error) { // Handle the error, refer to your framework documentation } // You might also want to check and handle any invalid `result` here If you for some reason want to include failed queries in the dehydrated state to avoid retries, you can use the option shouldDehydrateQuery to override the default function and implement your own logic: tsx dehydrate(queryClient, { shouldDehydrateQuery: (query) => { // This will include all queries, including failed ones, // but you can also implement your own logic by inspecting `query` return true }, }) dehydrate(queryClient, { shouldDehydrateQuery: (query) => { // This will include all queries, including failed ones, // but you can also implement your own logic by inspecting `query` return true }, }) ## Serialization When doing return { props: { dehydratedState: dehydrate(queryClient) } } in Next.js, or return json({ dehydratedState: dehydrate(queryClient) }) in Remix, what happens is that the dehydratedState representation of the queryClient is serialized by the framework so it can be embedded into the markup and transported to the client. By default, these frameworks only supports returning things that are safely serializable/parsable, and therefore does not support undefined, Error, Date, Map, Set, BigInt, Infinity, NaN, \-0, regular expressions etc. This also means that you can not return any of these things from your queries. If returning these values is something you want, check out superjson or similar packages. If you are using a custom SSR setup, you need to take care of this step yourself. Your first instinct might be to use JSON.stringify(dehydratedState), but because this doesn't escape things like <script>alert('Oh no..')</script> by default, this can easily lead to **XSS-vulnerabilities** in your application. superjson also **does not** escape values and is unsafe to use by itself in a custom SSR setup (unless you add an extra step for escaping the output). Instead we recommend using a library like Serialize JavaScript or devalue which are both safe against XSS injections out of the box. ## A note about request waterfalls In the Performance & Request Waterfalls guide we mentioned we would revisit how server rendering changes one of the more complex nested waterfalls. Check back for the specific code example, but as a refresher, we have a code split <GraphFeedItem> component inside a <Feed> component. This only renders if the feed contains a graph item and both of these components fetches their own data. With client rendering, this leads to the following request waterfall: 1. |> Markup (without content) 2. |> JS for <Feed> 3. |> getFeed() 4. |> JS for <GraphFeedItem> 5. |> getGraphDataById() 1. |> Markup (without content) 2. |> JS for <Feed> 3. |> getFeed() 4. |> JS for <GraphFeedItem> 5. |> getGraphDataById() The nice thing about server rendering is that we can turn the above into: 1. |> Markup (with content AND initial data) 2. |> JS for <Feed> 2. |> JS for <GraphFeedItem> 1. |> Markup (with content AND initial data) 2. |> JS for <Feed> 2. |> JS for <GraphFeedItem> Note that the queries are no longer fetched on the client, instead their data was included in the markup. The reason we can now load the JS in parallel is that since <GraphFeedItem> was rendered on the server we know that we are going to need this JS on the client as well and can insert a script-tag for this chunk in the markup. On the server, we would still have this request waterfall: 1. |> getFeed() 2. |> getGraphDataById() 1. |> getFeed() 2. |> getGraphDataById() We simply can not know before we have fetched the feed if we also need to fetch graph data, they are dependent queries. Because this happens on the server where latency is generally both lower and more stable, this often isn't such a big deal. Amazing, we've mostly flattened our waterfalls! There's a catch though. Let's call this page the /feed page, and let's pretend we also have another page like /posts. If we type in www.example.com/feed directly in the url bar and hit enter, we get all these great server rendering benefits, BUT, if we instead type in www.example.com/posts and then **click a link** to /feed, we're back to to this: 1. |> JS for <Feed> 2. |> getFeed() 3. |> JS for <GraphFeedItem> 4. |> getGraphDataById() 1. |> JS for <Feed> 2. |> getFeed() 3. |> JS for <GraphFeedItem> 4. |> getGraphDataById() This is because with SPA's, server rendering only works for the initial page load, not for any subsequent navigation. Modern frameworks often try to solve this by fetching the initial code and data in parallel, so if you were using Next.js or Remix with the prefetching patterns we outlined in this guide, including how to prefetch dependent queries, it would actually look like this instead: 1. |> JS for <Feed> 1. |> getFeed() + getGraphDataById() 2. |> JS for <GraphFeedItem> 1. |> JS for <Feed> 1. |> getFeed() + getGraphDataById() 2. |> JS for <GraphFeedItem> This is much better, but if we want to improve this further we can flatten this to a single roundtrip with Server Components. Learn how in the Advanced Server Rendering guide. ## Tips, Tricks and Caveats ### Staleness is measured from when the query was fetched on the server A query is considered stale depending on when it was dataUpdatedAt. A caveat here is that the server needs to have the correct time for this to work properly, but UTC time is used, so timezones do not factor into this. Because staleTime defaults to 0, queries will be refetched in the background on page load by default. You might want to use a higher staleTime to avoid this double fetching, especially if you don't cache your markup. This refetching of stale queries is a perfect match when caching markup in a CDN! You can set the cache time of the page itself decently high to avoid having to re-render pages on the server, but configure the staleTime of the queries lower to make sure data is refetched in the background as soon as a user visits the page. Maybe you want to cache the pages for a week, but refetch the data automatically on page load if it's older than a day? ### High memory consumption on server In case you are creating the QueryClient for every request, React Query creates the isolated cache for this client, which is preserved in memory for the gcTime period. That may lead to high memory consumption on server in case of high number of requests during that period. On the server, gcTime defaults to Infinity which disables manual garbage collection and will automatically clear memory once a request has finished. If you are explicitly setting a non-Infinity gcTime then you will be responsible for clearing the cache early. Avoid setting gcTime to 0 as it may result in a hydration error. This occurs because the Hydration Boundary places necessary data into the cache for rendering, but if the garbage collector removes the data before the rendering completes, issues may arise. If you require a shorter gcTime, we recommend setting it to 2 \* 1000 to allow sufficient time for the app to reference the data. To clear the cache after it is not needed and to lower memory consumption, you can add a call to queryClient.clear() after the request is handled and dehydrated state has been sent to the client. Alternatively, you can set a smaller gcTime. ### Caveat for Next.js rewrites There's a catch if you're using Next.js' rewrites feature together with Automatic Static Optimization or getStaticProps: It will cause a second hydration by React Query. That's because Next.js needs to ensure that they parse the rewrites on the client and collect any params after hydration so that they can be provided in router.query. The result is missing referential equality for all the hydration data, which for example triggers wherever your data is used as props of components or in the dependency array of useEffects/useMemos. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr Welcome to the Advanced Server Rendering guide, where you will learn all about using React Query with streaming, Server Components and the Next.js app router. You might want to read the Server Rendering & Hydration guide before this one as it teaches the basics for using React Query with SSR, and Performance & Request Waterfalls as well as Prefetching & Router Integration also contains valuable background. Before we start, let's note that while the initialData approach outlined in the SSR guide also works with Server Components, we'll focus this guide on the hydration APIs. ## Server Components & Next.js app router We won't cover Server Components in depth here, but the short version is that they are components that are guaranteed to _only_ run on the server, both for the initial page view and **also on page transitions**. This is similar to how Next.js getServerSideProps/getStaticProps and Remix loader works, as these also always run on the server but while those can only return data, Server Components can do a lot more. The data part is central to React Query however, so let's focus on that. How do we take what we learned in the Server Rendering guide about passing data prefetched in framework loaders to the app and apply that to Server Components and the Next.js app router? The best way to start thinking about this is to consider Server Components as "just" another framework loader. ### A quick note on terminology So far in these guides, we've been talking about the _server_ and the _client_. It's important to note that confusingly enough this does not match 1-1 with _Server Components_ and _Client Components_. Server Components are guaranteed to only run on the server, but Client Components can actually run in both places. The reason for this is that they can also render during the initial _server rendering_ pass. One way to think of this is that even though Server Components also _render_, they happen during a "loader phase" (always happens on the server), while Client Components run during the "application phase". That application can run both on the server during SSR, and in for example a browser. Where exactly that application runs and if it runs during SSR or not might differ between frameworks. ### Initial setup The first step of any React Query setup is always to create a queryClient and wrap your application in a QueryClientProvider. With Server Components, this looks mostly the same across frameworks, one difference being the filename conventions: tsx // In Next.js, this file would be called: app/providers.tsx 'use client' // Since QueryClientProvider relies on useContext under the hood, we have to put 'use client' on top import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export default function Providers({ children }: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> ) } // In Next.js, this file would be called: app/providers.tsx 'use client' // Since QueryClientProvider relies on useContext under the hood, we have to put 'use client' on top import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export default function Providers({ children }: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> ) } tsx // In Next.js, this file would be called: app/layout.tsx import Providers from './providers' export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <head /> <body> <Providers>{children}</Providers> </body> </html> ) } // In Next.js, this file would be called: app/layout.tsx import Providers from './providers' export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <head /> <body> <Providers>{children}</Providers> </body> </html> ) } This part is pretty similar to what we did in the SSR guide, we just need to split things up into two different files. ### Prefetching and de/hydrating data Let's next look at how to actually prefetch data and dehydrate and hydrate it. This is what it looked like using the **Next.js pages router**: tsx // pages/posts.tsx import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' // This could also be getServerSideProps export async function getStaticProps() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return { props: { dehydratedState: dehydrate(queryClient), }, } } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way // // Note that we are using useQuery here instead of useSuspenseQuery. // Because this data has already been prefetched, there is no need to // ever suspend in the component itself. If we forget or remove the // prefetch, this will instead fetch the data on the client, while // using useSuspenseQuery would have had worse side effects. const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } // pages/posts.tsx import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' // This could also be getServerSideProps export async function getStaticProps() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return { props: { dehydratedState: dehydrate(queryClient), }, } } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way // // Note that we are using useQuery here instead of useSuspenseQuery. // Because this data has already been prefetched, there is no need to // ever suspend in the component itself. If we forget or remove the // prefetch, this will instead fetch the data on the client, while // using useSuspenseQuery would have had worse side effects. const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } Converting this to the app router actually looks pretty similar, we just need to move things around a bit. First, we'll create a Server Component to do the prefetching part: tsx // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Posts from './posts' export default async function PostsPage() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( // Neat! Serialization is now as easy as passing props. // HydrationBoundary is a Client Component, so hydration will happen there. <HydrationBoundary state={dehydrate(queryClient)}> <Posts /> </HydrationBoundary> ) } // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Posts from './posts' export default async function PostsPage() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( // Neat! Serialization is now as easy as passing props. // HydrationBoundary is a Client Component, so hydration will happen there. <HydrationBoundary state={dehydrate(queryClient)}> <Posts /> </HydrationBoundary> ) } Next, we'll look at what the Client Component part looks like: tsx // app/posts/posts.tsx 'use client' export default function Posts() { // This useQuery could just as well happen in some deeper // child to <Posts>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: () => getPosts(), }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix. const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } // app/posts/posts.tsx 'use client' export default function Posts() { // This useQuery could just as well happen in some deeper // child to <Posts>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: () => getPosts(), }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix. const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } One neat thing about the examples above is that the only thing that is Next.js-specific here are the file names, everything else would look the same in any other framework that supports Server Components. In the SSR guide, we noted that you could get rid of the boilerplate of having <HydrationBoundary> in every route. This is not possible with Server Components. > NOTE: If you encounter a type error while using async Server Components with TypeScript versions lower than 5.1.3 and @types/react versions lower than 18.2.8, it is recommended to update to the latest versions of both. Alternatively, you can use the temporary workaround of adding {/\* @ts-expect-error Server Component \*/} when calling this component inside another. For more information, see Async Server Component TypeScript Error in the Next.js 13 docs. > NOTE: If you encounter an error Only plain objects, and a few built-ins, can be passed to Server Actions. Classes or null prototypes are not supported. make sure that you're **not** passing to queryFn a function reference, instead call the function because queryFn args has a bunch of properties and not all of it would be serializable. see Server Action only works when queryFn isn't a reference. ### Nesting Server Components A nice thing about Server Components is that they can be nested and exist on many levels in the React tree, making it possible to prefetch data closer to where it's actually used instead of only at the top of the application (just like Remix loaders). This can be as simple as a Server Component rendering another Server Component (we'll leave the Client Components out in this example for brevity): tsx // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Posts from './posts' import CommentsServerComponent from './comments-server' export default async function PostsPage() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( <HydrationBoundary state={dehydrate(queryClient)}> <Posts /> <CommentsServerComponent /> </HydrationBoundary> ) } // app/posts/comments-server.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Comments from './comments' export default async function CommentsServerComponent() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) return ( <HydrationBoundary state={dehydrate(queryClient)}> <Comments /> </HydrationBoundary> ) } // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Posts from './posts' import CommentsServerComponent from './comments-server' export default async function PostsPage() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( <HydrationBoundary state={dehydrate(queryClient)}> <Posts /> <CommentsServerComponent /> </HydrationBoundary> ) } // app/posts/comments-server.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Comments from './comments' export default async function CommentsServerComponent() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) return ( <HydrationBoundary state={dehydrate(queryClient)}> <Comments /> </HydrationBoundary> ) } As you can see, it's perfectly fine to use <HydrationBoundary> in multiple places, and create and dehydrate multiple queryClient for prefetching. Note that because we are awaiting getPosts before rendering CommentsServerComponent this would lead to a server side waterfall: 1. |> getPosts() 2. |> getComments() 1. |> getPosts() 2. |> getComments() If the server latency to the data is low, this might not be a huge issue, but is still worth pointing out. In Next.js, besides prefetching data in page.tsx, you can also do it in layout.tsx, and in parallel routes. Because these are all part of the routing, Next.js knows how to fetch them all in parallel. So if CommentsServerComponent above was instead expressed as a parallel route, the waterfall would be flattened automatically. As more frameworks start supporting Server Components, they might have other routing conventions. Read your framework docs for details. ### Alternative: Use a single queryClient for prefetching In the example above, we create a new queryClient for each Server Component that fetches data. This is the recommended approach, but if you want to, you can alternatively create a single one that is reused across all Server Components: tsx // app/getQueryClient.tsx import { QueryClient } from '@tanstack/react-query' import { cache } from 'react' // cache() is scoped per request, so we don't leak data between requests const getQueryClient = cache(() => new QueryClient()) export default getQueryClient // app/getQueryClient.tsx import { QueryClient } from '@tanstack/react-query' import { cache } from 'react' // cache() is scoped per request, so we don't leak data between requests const getQueryClient = cache(() => new QueryClient()) export default getQueryClient The benefit of this is that you can call getQueryClient() to get a hold of this client anywhere that gets called from a Server Component, including utility functions. The downside is that every time you call dehydrate(getQueryClient()), you serialize _the entire_ queryClient, including queries that have already been serialized before and are unrelated to the current Server Component which is unnecessary overhead. Next.js already dedupes requests that utilize fetch(), but if you are using something else in your queryFn, or if you use a framework that does _not_ dedupe these requests automatically, using a single queryClient as described above might make sense, despite the duplicated serialization. > As a future improvement, we might look into creating a dehydrateNew() function (name pending) that only dehydrate queries that are _new_ since the last call to dehydrateNew(). Feel free to get in touch if this sounds interesting and like something you want to help out with! ### Data ownership and revalidation With Server Components, it's important to think about data ownership and revalidation. To explain why, let's look at a modified example from above: tsx // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Posts from './posts' export default async function PostsPage() { const queryClient = new QueryClient() // Note we are now using fetchQuery() const posts = await queryClient.fetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( <HydrationBoundary state={dehydrate(queryClient)}> {/* This is the new part */} <div>Nr of posts: {posts.length}</div> <Posts /> </HydrationBoundary> ) } // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import Posts from './posts' export default async function PostsPage() { const queryClient = new QueryClient() // Note we are now using fetchQuery() const posts = await queryClient.fetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( <HydrationBoundary state={dehydrate(queryClient)}> {/* This is the new part */} <div>Nr of posts: {posts.length}</div> <Posts /> </HydrationBoundary> ) } We are now rendering data from the getPosts query both in a Server Component and in a Client Component. This will be fine for the initial page render, but what happens when the query revalidates on the client for some reason when staleTime has been passed? React Query has no idea of how to _revalidate the Server Component_, so if it refetches the data on the client, causing React to rerender the list of posts, the Nr of posts: {posts.length} will end up out of sync. This is fine if you set staleTime: Infinity, so that React Query never revalidates, but this is probably not what you want if you are using React Query in the first place. Using React Query with Server Components makes most sense if: * You have an app using React Query and want to migrate to Server Components without rewriting all the data fetching * You want a familiar programming paradigm, but want to still sprinkle in the benefits of Server Components where it makes most sense * You have some use case that React Query covers, but that your framework of choice does not cover It's hard to give general advice on when it makes sense to pair React Query with Server Components and not. **If you are just starting out with a new Server Components app, we suggest you start out with any tools for data fetching your framework provides you with and avoid bringing in React Query until you actually need it.** This might be never, and that's fine, use the right tool for the job! If you do use it, a good rule of thumb is to avoid queryClient.fetchQuery unless you need to catch errors. If you do use it, don't render its result on the server or pass the result to another component, even a Client Component one. From the React Query perspective, treat Server Components as a place to prefetch data, nothing more. Of course, it's fine to have Server Components own some data, and Client Components own other, just make sure those two realities don't get out of sync. ## Streaming with Server Components The Next.js app router automatically streams any part of the application that is ready to be displayed to the browser as soon as possible, so finished content can be displayed immediately without waiting for still pending content. It does this along <Suspense> boundary lines. Note that if you create a file loading.tsx, this automatically creates a <Suspense> boundary behind the scenes. With the prefetching patterns described above, React Query is perfectly compatible with this form of streaming. As the data for each Suspense boundary resolves, Next.js can render and stream the finished content to the browser. This works even if you are using useQuery as outlined above because the suspending actually happens when you await the prefetch. As of React Query v5.40.0, you don't have to await all prefetches for this to work, as pending Queries can also be dehydrated and sent to the client. This lets you kick off prefetches as early as possible without letting them block an entire Suspense boundary, and streams the _data_ to the client as the query finishes. This can be useful for example if you want to prefetch some content that is only visible after some user interaction, or say if you want to await and render the first page of an infinite query, but start prefetching page 2 without blocking rendering. To make this work, we have to instruct the queryClient to also dehydrate pending Queries. We can do this globally, or by passing that option directly to dehydrate. We will also need to move the getQueryClient() function out of our app/providers.tsx file as we want to use it in our server component and our client provider. tsx // app/get-query-client.ts import { isServer, QueryClient, defaultShouldDehydrateQuery, } from '@tanstack/react-query' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { staleTime: 60 * 1000, }, dehydrate: { // include pending queries in dehydration shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === 'pending', shouldRedactErrors: (error) => { // We should not catch Next.js server errors // as that's how Next.js detects dynamic pages // so we cannot redact them. // Next.js also automatically redacts errors for us // with better digests. return false }, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined export function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } // app/get-query-client.ts import { isServer, QueryClient, defaultShouldDehydrateQuery, } from '@tanstack/react-query' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { staleTime: 60 * 1000, }, dehydrate: { // include pending queries in dehydration shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === 'pending', shouldRedactErrors: (error) => { // We should not catch Next.js server errors // as that's how Next.js detects dynamic pages // so we cannot redact them. // Next.js also automatically redacts errors for us // with better digests. return false }, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined export function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } > Note: This works in NextJs and Server Components because React can serialize Promises over the wire when you pass them down to Client Components. Then, all we need to do is provide a HydrationBoundary, but we don't need to await prefetches anymore: tsx // app/posts/page.tsx import { dehydrate, HydrationBoundary } from '@tanstack/react-query' import { getQueryClient } from './get-query-client' import Posts from './posts' // the function doesn't need to be `async` because we don't `await` anything export default function PostsPage() { const queryClient = getQueryClient() // look ma, no await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( <HydrationBoundary state={dehydrate(queryClient)}> <Posts /> </HydrationBoundary> ) } // app/posts/page.tsx import { dehydrate, HydrationBoundary } from '@tanstack/react-query' import { getQueryClient } from './get-query-client' import Posts from './posts' // the function doesn't need to be `async` because we don't `await` anything export default function PostsPage() { const queryClient = getQueryClient() // look ma, no await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return ( <HydrationBoundary state={dehydrate(queryClient)}> <Posts /> </HydrationBoundary> ) } On the client, the Promise will be put into the QueryCache for us. That means we can now call useSuspenseQuery inside the Posts component to "use" that Promise (which was created on the Server): tsx // app/posts/posts.tsx 'use client' export default function Posts() { const { data } = useSuspenseQuery({ queryKey: ['posts'], queryFn: getPosts }) // ... } // app/posts/posts.tsx 'use client' export default function Posts() { const { data } = useSuspenseQuery({ queryKey: ['posts'], queryFn: getPosts }) // ... } > Note that you could also useQuery instead of useSuspenseQuery, and the Promise would still be picked up correctly. However, NextJs won't suspend in that case and the component will render in the pending status, which also opts out of server rendering the content. If you're using non-JSON data types and serialize the query results on the server, you can specify the dehydrate.serializeData and hydrate.deserializeData options to serialize and deserialize the data on each side of the boundary to ensure the data in the cache is the same format both on the server and the client: tsx // app/get-query-client.ts import { QueryClient, defaultShouldDehydrateQuery } from '@tanstack/react-query' import { deserialize, serialize } from './transformer' function makeQueryClient() { return new QueryClient({ defaultOptions: { // ... hydrate: { deserializeData: deserialize, }, dehydrate: { serializeData: serialize, }, }, }) } // ... // app/get-query-client.ts import { QueryClient, defaultShouldDehydrateQuery } from '@tanstack/react-query' import { deserialize, serialize } from './transformer' function makeQueryClient() { return new QueryClient({ defaultOptions: { // ... hydrate: { deserializeData: deserialize, }, dehydrate: { serializeData: serialize, }, }, }) } // ... tsx // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import { getQueryClient } from './get-query-client' import { serialize } from './transformer' import Posts from './posts' export default function PostsPage() { const queryClient = getQueryClient() // look ma, no await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: () => getPosts().then(serialize), // <-- serialize the data on the server }) return ( <HydrationBoundary state={dehydrate(queryClient)}> <Posts /> </HydrationBoundary> ) } // app/posts/page.tsx import { dehydrate, HydrationBoundary, QueryClient, } from '@tanstack/react-query' import { getQueryClient } from './get-query-client' import { serialize } from './transformer' import Posts from './posts' export default function PostsPage() { const queryClient = getQueryClient() // look ma, no await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: () => getPosts().then(serialize), // <-- serialize the data on the server }) return ( <HydrationBoundary state={dehydrate(queryClient)}> <Posts /> </HydrationBoundary> ) } tsx // app/posts/posts.tsx 'use client' export default function Posts() { const { data } = useSuspenseQuery({ queryKey: ['posts'], queryFn: getPosts }) // ... } // app/posts/posts.tsx 'use client' export default function Posts() { const { data } = useSuspenseQuery({ queryKey: ['posts'], queryFn: getPosts }) // ... } Now, your getPosts function can return e.g. Temporal datetime objects and the data will be serialized and deserialized on the client, assuming your transformer can serialize and deserialize those data types. For more information, check out the Next.js App with Prefetching Example. ## Experimental streaming without prefetching in Next.js While we recommend the prefetching solution detailed above because it flattens request waterfalls both on the initial page load **and** any subsequent page navigation, there is an experimental way to skip prefetching altogether and still have streaming SSR work: @tanstack/react-query-next-experimental This package will allow you to fetch data on the server (in a Client Component) by just calling useSuspenseQuery in your component. Results will then be streamed from the server to the client as SuspenseBoundaries resolve. If you call useSuspenseQuery without wrapping it in a <Suspense> boundary, the HTML response won't start until the fetch resolves. This can be when you want depending on the situation, but keep in mind that this will hurt your TTFB. To achieve this, wrap your app in the ReactQueryStreamedHydration component: tsx // app/providers.tsx 'use client' import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import * as React from 'react' import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export function Providers(props: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}> <ReactQueryStreamedHydration> {props.children} </ReactQueryStreamedHydration> </QueryClientProvider> ) } // app/providers.tsx 'use client' import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import * as React from 'react' import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export function Providers(props: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}> <ReactQueryStreamedHydration> {props.children} </ReactQueryStreamedHydration> </QueryClientProvider> ) } For more information, check out the NextJs Suspense Streaming Example. The big upside is that you no longer need to prefetch queries manually to have SSR work, and it even still streams in the result! This gives you phenomenal DX and lower code complexity. The downside is easiest to explain if we look back at the complex request waterfall example in the Performance & Request Waterfalls guide. Server Components with prefetching effectively eliminates the request waterfalls both for the initial page load **and** any subsequent navigation. This prefetch-less approach however will only flatten the waterfalls on the initial page load but ends up the same deep waterfall as the original example on page navigations: 1. |> JS for <Feed> 2. |> getFeed() 3. |> JS for <GraphFeedItem> 4. |> getGraphDataById() 1. |> JS for <Feed> 2. |> getFeed() 3. |> JS for <GraphFeedItem> 4. |> getGraphDataById() This is even worse than with getServerSideProps/getStaticProps, since with those we could at least parallelize data- and code-fetching. If you value DX/iteration/shipping speed with low code complexity over performance, don't have deeply nested queries, or are on top of your request waterfalls with parallel fetching using tools like useSuspenseQueries, this can be a good tradeoff. > It might be possible to combine the two approaches, but even we haven't tried that out yet. If you do try this, please report back your findings, or even update these docs with some tips! ## Final words Server Components and streaming are still fairly new concepts and we are still figuring out how React Query fits in and what improvements we can make to the API. We welcome suggestions, feedback and bug reports! Similarly, it would be impossible to teach all the intricacies of this new paradigm all in one guide, on the first try. If you are missing some piece of information here or have suggestions on how to improve this content, also get in touch, or even better, click the "Edit on GitHub" button below and help us out. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/caching > Please thoroughly read the Important Defaults before reading this guide ## Basic Example This caching example illustrates the story and lifecycle of: * Query Instances with and without cache data * Background Refetching * Inactive Queries * Garbage Collection Let's assume we are using the default gcTime of **5 minutes** and the default staleTime of 0. * A new instance of useQuery({ queryKey: \['todos'\], queryFn: fetchTodos }) mounts. * Since no other queries have been made with the \['todos'\] query key, this query will show a hard loading state and make a network request to fetch the data. * When the network request has completed, the returned data will be cached under the \['todos'\] key. * The hook will mark the data as stale after the configured staleTime (defaults to 0, or immediately). * A second instance of useQuery({ queryKey: \['todos'\], queryFn: fetchTodos }) mounts elsewhere. * Since the cache already has data for the \['todos'\] key from the first query, that data is immediately returned from the cache. * The new instance triggers a new network request using its query function. * Note that regardless of whether both fetchTodos query functions are identical or not, both queries' status are updated (including isFetching, isPending, and other related values) because they have the same query key. * When the request completes successfully, the cache's data under the \['todos'\] key is updated with the new data, and both instances are updated with the new data. * Both instances of the useQuery({ queryKey: \['todos'\], queryFn: fetchTodos }) query are unmounted and no longer in use. * Since there are no more active instances of this query, a garbage collection timeout is set using gcTime to delete and garbage collect the query (defaults to **5 minutes**). * Before the cache timeout has completed, another instance of useQuery({ queryKey: \['todos'\], queryFn: fetchTodos }) mounts. The query immediately returns the available cached data while the fetchTodos function is being run in the background. When it completes successfully, it will populate the cache with fresh data. * The final instance of useQuery({ queryKey: \['todos'\], queryFn: fetchTodos }) unmounts. * No more instances of useQuery({ queryKey: \['todos'\], queryFn: fetchTodos }) appear within **5 minutes**. * The cached data under the \['todos'\] key is deleted and garbage collected. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/render-optimizations React Query applies a couple of optimizations automatically to ensure that your components only re-render when they actually need to. This is done by the following means: ## structural sharing React Query uses a technique called "structural sharing" to ensure that as many references as possible will be kept intact between re-renders. If data is fetched over the network, usually, you'll get a completely new reference by json parsing the response. However, React Query will keep the original reference if _nothing_ changed in the data. If a subset changed, React Query will keep the unchanged parts and only replace the changed parts. > Note: This optimization only works if the queryFn returns JSON compatible data. You can turn it off by setting structuralSharing: false globally or on a per-query basis, or you can implement your own structural sharing by passing a function to it. ### referential identity The top level object returned from useQuery, useInfiniteQuery, useMutation and the Array returned from useQueries is **not referentially stable**. It will be a new reference on every render. However, the data properties returned from these hooks will be as stable as possible. ## tracked properties React Query will only trigger a re-render if one of the properties returned from useQuery is actually "used". This is done by using custom getters. This avoids a lot of unnecessary re-renders, e.g. because properties like isFetching or isStale might change often, but are not used in the component. You can customize this feature by setting notifyOnChangeProps manually globally or on a per-query basis. If you want to turn that feature off, you can set notifyOnChangeProps: 'all'. > Note: Custom getters are invoked by accessing a property, either via destructuring or by accessing it directly. If you use object rest destructuring, you will disable this optimization. We have a lint rule to guard against this pitfall. ## select You can use the select option to select a subset of the data that your component should subscribe to. This is useful for highly optimized data transformations or to avoid unnecessary re-renders. js export const useTodos = (select) => { return useQuery({ queryKey: ['todos'], queryFn: fetchTodos, select, }) } export const useTodoCount = () => { return useTodos((data) => data.length) } export const useTodos = (select) => { return useQuery({ queryKey: ['todos'], queryFn: fetchTodos, select, }) } export const useTodoCount = () => { return useTodos((data) => data.length) } A component using the useTodoCount custom hook will only re-render if the length of the todos changes. It will **not** re-render if e.g. the name of a todo changed. > Note: select operates on successfully cached data and is not the appropriate place to throw errors. The source of truth for errors is the queryFn, and a select function that returns an error results in data being undefined and isSuccess being true. We recommend handling errors in the queryFn if you wish to have a query fail on incorrect data, or outside of the query hook if you have a error case not related to caching. ### memoization The select function will only re-run if: * the select function itself changed referentially * data changed This means that an inlined select function, as shown above, will run on every render. To avoid this, you can wrap the select function in useCallback, or extract it to a stable function reference if it doesn't have any dependencies: js // wrapped in useCallback export const useTodoCount = () => { return useTodos(useCallback((data) => data.length, [])) } // wrapped in useCallback export const useTodoCount = () => { return useTodos(useCallback((data) => data.length, [])) } js // extracted to a stable function reference const selectTodoCount = (data) => data.length export const useTodoCount = () => { return useTodos(selectTodoCount) } // extracted to a stable function reference const selectTodoCount = (data) => data.length export const useTodoCount = () => { return useTodos(selectTodoCount) } ## Further Reading For an in-depth guide about these topics, read React Query Render Optimizations from the Community Resources. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/default-query-function If you find yourself wishing for whatever reason that you could just share the same query function for your entire app and just use query keys to identify what it should fetch, you can do that by providing a **default query function** to TanStack Query: tsx // Define a default query function that will receive the query key const defaultQueryFn = async ({ queryKey }) => { const { data } = await axios.get( `https://jsonplaceholder.typicode.com${queryKey[0]}`, ) return data } // provide the default query function to your app with defaultOptions const queryClient = new QueryClient({ defaultOptions: { queries: { queryFn: defaultQueryFn, }, }, }) function App() { return ( <QueryClientProvider client={queryClient}> <YourApp /> </QueryClientProvider> ) } // All you have to do now is pass a key! function Posts() { const { status, data, error, isFetching } = useQuery({ queryKey: ['/posts'] }) // ... } // You can even leave out the queryFn and just go straight into options function Post({ postId }) { const { status, data, error, isFetching } = useQuery({ queryKey: [`/posts/${postId}`], enabled: !!postId, }) // ... } // Define a default query function that will receive the query key const defaultQueryFn = async ({ queryKey }) => { const { data } = await axios.get( `https://jsonplaceholder.typicode.com${queryKey[0]}`, ) return data } // provide the default query function to your app with defaultOptions const queryClient = new QueryClient({ defaultOptions: { queries: { queryFn: defaultQueryFn, }, }, }) function App() { return ( <QueryClientProvider client={queryClient}> <YourApp /> </QueryClientProvider> ) } // All you have to do now is pass a key! function Posts() { const { status, data, error, isFetching } = useQuery({ queryKey: ['/posts'] }) // ... } // You can even leave out the queryFn and just go straight into options function Post({ postId }) { const { status, data, error, isFetching } = useQuery({ queryKey: [`/posts/${postId}`], enabled: !!postId, }) // ... } If you ever want to override the default queryFn, you can just provide your own like you normally would. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/suspense React Query can also be used with React's Suspense for Data Fetching APIs. For this, we have dedicated hooks: * useSuspenseQuery * useSuspenseInfiniteQuery * useSuspenseQueries * Additionally, you can use the useQuery().promise and React.use() (Experimental) When using suspense mode, status states and error objects are not needed and are then replaced by usage of the React.Suspense component (including the use of the fallback prop and React error boundaries for catching errors). Please read the Resetting Error Boundaries and look at the Suspense Example for more information on how to set up suspense mode. If you want mutations to propagate errors to the nearest error boundary (similar to queries), you can set the throwOnError option to true as well. Enabling suspense mode for a query: tsx import { useSuspenseQuery } from '@tanstack/react-query' const { data } = useSuspenseQuery({ queryKey, queryFn }) import { useSuspenseQuery } from '@tanstack/react-query' const { data } = useSuspenseQuery({ queryKey, queryFn }) This works nicely in TypeScript, because data is guaranteed to be defined (as errors and loading states are handled by Suspense- and ErrorBoundaries). On the flip side, you therefore can't conditionally enable / disable the Query. This generally shouldn't be necessary for dependent Queries because with suspense, all your Queries inside one component are fetched in serial. placeholderData also doesn't exist for this Query. To prevent the UI from being replaced by a fallback during an update, wrap your updates that change the QueryKey into startTransition. ### throwOnError default Not all errors are thrown to the nearest Error Boundary per default - we're only throwing errors if there is no other data to show. That means if a Query ever successfully got data in the cache, the component will render, even if data is stale. Thus, the default for throwOnError is: throwOnError: (error, query) => typeof query.state.data === 'undefined' throwOnError: (error, query) => typeof query.state.data === 'undefined' Since you can't change throwOnError (because it would allow for data to become potentially undefined), you have to throw errors manually if you want all errors to be handled by Error Boundaries: tsx import { useSuspenseQuery } from '@tanstack/react-query' const { data, error, isFetching } = useSuspenseQuery({ queryKey, queryFn }) if (error && !isFetching) { throw error } // continue rendering data import { useSuspenseQuery } from '@tanstack/react-query' const { data, error, isFetching } = useSuspenseQuery({ queryKey, queryFn }) if (error && !isFetching) { throw error } // continue rendering data ## Resetting Error Boundaries Whether you are using **suspense** or **throwOnError** in your queries, you will need a way to let queries know that you want to try again when re-rendering after some error occurred. Query errors can be reset with the QueryErrorResetBoundary component or with the useQueryErrorResetBoundary hook. When using the component it will reset any query errors within the boundaries of the component: tsx import { QueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => ( <QueryErrorResetBoundary> {({ reset }) => ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> There was an error! <Button onClick={() => resetErrorBoundary()}>Try again</Button> </div> )} > <Page /> </ErrorBoundary> )} </QueryErrorResetBoundary> ) import { QueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => ( <QueryErrorResetBoundary> {({ reset }) => ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> There was an error! <Button onClick={() => resetErrorBoundary()}>Try again</Button> </div> )} > <Page /> </ErrorBoundary> )} </QueryErrorResetBoundary> ) When using the hook it will reset any query errors within the closest QueryErrorResetBoundary. If there is no boundary defined it will reset them globally: tsx import { useQueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => { const { reset } = useQueryErrorResetBoundary() return ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> There was an error! <Button onClick={() => resetErrorBoundary()}>Try again</Button> </div> )} > <Page /> </ErrorBoundary> ) } import { useQueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => { const { reset } = useQueryErrorResetBoundary() return ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> There was an error! <Button onClick={() => resetErrorBoundary()}>Try again</Button> </div> )} > <Page /> </ErrorBoundary> ) } ## Fetch-on-render vs Render-as-you-fetch Out of the box, React Query in suspense mode works really well as a **Fetch-on-render** solution with no additional configuration. This means that when your components attempt to mount, they will trigger query fetching and suspend, but only once you have imported them and mounted them. If you want to take it to the next level and implement a **Render-as-you-fetch** model, we recommend implementing Prefetching on routing callbacks and/or user interactions events to start loading queries before they are mounted and hopefully even before you start importing or mounting their parent components. ## Suspense on the Server with streaming If you are using NextJs, you can use our **experimental** integration for Suspense on the Server: @tanstack/react-query-next-experimental. This package will allow you to fetch data on the server (in a client component) by just calling useSuspenseQuery in your component. Results will then be streamed from the server to the client as SuspenseBoundaries resolve. To achieve this, wrap your app in the ReactQueryStreamedHydration component: tsx // app/providers.tsx 'use client' import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import * as React from 'react' import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export function Providers(props: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}> <ReactQueryStreamedHydration> {props.children} </ReactQueryStreamedHydration> </QueryClientProvider> ) } // app/providers.tsx 'use client' import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import * as React from 'react' import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export function Providers(props: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}> <ReactQueryStreamedHydration> {props.children} </ReactQueryStreamedHydration> </QueryClientProvider> ) } For more information, check out the NextJs Suspense Streaming Example and the Advanced Rendering & Hydration guide. ## Using useQuery().promise and React.use() (Experimental) > To enable this feature, you need to set the experimental\_prefetchInRender option to true when creating your QueryClient **Example code:** tsx const queryClient = new QueryClient({ defaultOptions: { queries: { experimental_prefetchInRender: true, }, }, }) const queryClient = new QueryClient({ defaultOptions: { queries: { experimental_prefetchInRender: true, }, }, }) **Usage:** tsx import React from 'react' import { useQuery } from '@tanstack/react-query' import { fetchTodos, type Todo } from './api' function TodoList({ query }: { query: UseQueryResult<Todo[]> }) { const data = React.use(query.promise) return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) } export function App() { const query = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) return ( <> <h1>Todos</h1> <React.Suspense fallback={<div>Loading...</div>}> <TodoList query={query} /> </React.Suspense> </> ) } import React from 'react' import { useQuery } from '@tanstack/react-query' import { fetchTodos, type Todo } from './api' function TodoList({ query }: { query: UseQueryResult<Todo[]> }) { const data = React.use(query.promise) return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) } export function App() { const query = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) return ( <> <h1>Todos</h1> <React.Suspense fallback={<div>Loading...</div>}> <TodoList query={query} /> </React.Suspense> </> ) } --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/network-mode/#network-mode-online TanStack Query provides three different network modes to distinguish how Queries and Mutations should behave if you have no network connection. This mode can be set for each Query / Mutation individually, or globally via the query / mutation defaults. Since TanStack Query is most often used for data fetching in combination with data fetching libraries, the default network mode is online. ## Network Mode: online In this mode, Queries and Mutations will not fire unless you have network connection. This is the default mode. If a fetch is initiated for a query, it will always stay in the state (pending, error, success) it is in if the fetch cannot be made because there is no network connection. However, a fetchStatus is exposed additionally. This can be either: * fetching: The queryFn is really executing - a request is in-flight. * paused: The query is not executing - it is paused until you have connection again * idle: The query is not fetching and not paused The flags isFetching and isPaused are derived from this state and exposed for convenience. > Keep in mind that it might not be enough to check for pending state to show a loading spinner. Queries can be in state: 'pending', but fetchStatus: 'paused' if they are mounting for the first time, and you have no network connection. If a query runs because you are online, but you go offline while the fetch is still happening, TanStack Query will also pause the retry mechanism. Paused queries will then continue to run once you re-gain network connection. This is independent of refetchOnReconnect (which also defaults to true in this mode), because it is not a refetch, but rather a continue. If the query has been cancelled in the meantime, it will not continue. ## Network Mode: always In this mode, TanStack Query will always fetch and ignore the online / offline state. This is likely the mode you want to choose if you use TanStack Query in an environment where you don't need an active network connection for your Queries to work - e.g. if you just read from AsyncStorage, or if you just want to return Promise.resolve(5) from your queryFn. * Queries will never be paused because you have no network connection. * Retries will also not pause - your Query will go to error state if it fails. * refetchOnReconnect defaults to false in this mode, because reconnecting to the network is not a good indicator anymore that stale queries should be refetched. You can still turn it on if you want. ## Network Mode: offlineFirst This mode is the middle ground between the first two options, where TanStack Query will run the queryFn once, but then pause retries. This is very handy if you have a serviceWorker that intercepts a request for caching like in an offline-first PWA, or if you use HTTP caching via the Cache-Control header. In those situations, the first fetch might succeed because it comes from an offline storage / cache. However, if there is a cache miss, the network request will go out and fail, in which case this mode behaves like an online query - pausing retries. ## Devtools The TanStack Query Devtools will show Queries in a paused state if they would be fetching, but there is no network connection. There is also a toggle button to _Mock offline behavior_. Please note that this button will _not_ actually mess with your network connection (you can do that in the browser devtools), but it will set the OnlineManager in an offline state. ## Signature * networkMode: 'online' | 'always' | 'offlineFirst' * optional * defaults to 'online' --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/disabling-queries/#typesafe-disabling-of-queries-using-skiptoken If you ever want to disable a query from automatically running, you can use the enabled = false option. The enabled option also accepts a callback that returns a boolean. When enabled is false: * If the query has cached data, then the query will be initialized in the status === 'success' or isSuccess state. * If the query does not have cached data, then the query will start in the status === 'pending' and fetchStatus === 'idle' state. * The query will not automatically fetch on mount. * The query will not automatically refetch in the background. * The query will ignore query client invalidateQueries and refetchQueries calls that would normally result in the query refetching. * refetch returned from useQuery can be used to manually trigger the query to fetch. However, it will not work with skipToken. > TypeScript users may prefer to use skipToken as an alternative to enabled = false. tsx function Todos() { const { isLoading, isError, data, error, refetch, isFetching } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, enabled: false, }) return ( <div> <button onClick={() => refetch()}>Fetch Todos</button> {data ? ( <> <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> </> ) : isError ? ( <span>Error: {error.message}</span> ) : isLoading ? ( <span>Loading...</span> ) : ( <span>Not ready ...</span> )} <div>{isFetching ? 'Fetching...' : null}</div> </div> ) } function Todos() { const { isLoading, isError, data, error, refetch, isFetching } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, enabled: false, }) return ( <div> <button onClick={() => refetch()}>Fetch Todos</button> {data ? ( <> <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> </> ) : isError ? ( <span>Error: {error.message}</span> ) : isLoading ? ( <span>Loading...</span> ) : ( <span>Not ready ...</span> )} <div>{isFetching ? 'Fetching...' : null}</div> </div> ) } Permanently disabling a query opts out of many great features that TanStack Query has to offer (like background refetches), and it's also not the idiomatic way. It takes you from the declarative approach (defining dependencies when your query should run) into an imperative mode (fetch whenever I click here). It is also not possible to pass parameters to refetch. Oftentimes, all you want is a lazy query that defers the initial fetch: ## Lazy Queries The enabled option can not only be used to permanently disable a query, but also to enable / disable it at a later time. A good example would be a filter form where you only want to fire off the first request once the user has entered a filter value: tsx function Todos() { const [filter, setFilter] = React.useState('') const { data } = useQuery({ queryKey: ['todos', filter], queryFn: () => fetchTodos(filter), // ⬇️ disabled as long as the filter is empty enabled: !!filter, }) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data} />} </div> ) } function Todos() { const [filter, setFilter] = React.useState('') const { data } = useQuery({ queryKey: ['todos', filter], queryFn: () => fetchTodos(filter), // ⬇️ disabled as long as the filter is empty enabled: !!filter, }) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data} />} </div> ) } ### isLoading (Previously: isInitialLoading) Lazy queries will be in status: 'pending' right from the start because pending means that there is no data yet. This is technically true, however, since we are not currently fetching any data (as the query is not _enabled_), it also means you likely cannot use this flag to show a loading spinner. If you are using disabled or lazy queries, you can use the isLoading flag instead. It's a derived flag that is computed from: isPending && isFetching so it will only be true if the query is currently fetching for the first time. ## Typesafe disabling of queries using skipToken If you are using TypeScript, you can use the skipToken to disable a query. This is useful when you want to disable a query based on a condition, but you still want to keep the query to be type safe. > IMPORTANT: refetch from useQuery will not work with skipToken. Other than that, skipToken works the same as enabled: false. tsx import { skipToken, useQuery } from '@tanstack/react-query' function Todos() { const [filter, setFilter] = React.useState<string | undefined>() const { data } = useQuery({ queryKey: ['todos', filter], // ⬇️ disabled as long as the filter is undefined or empty queryFn: filter ? () => fetchTodos(filter) : skipToken, }) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data} />} </div> ) } import { skipToken, useQuery } from '@tanstack/react-query' function Todos() { const [filter, setFilter] = React.useState<string | undefined>() const { data } = useQuery({ queryKey: ['todos', filter], // ⬇️ disabled as long as the filter is undefined or empty queryFn: filter ? () => fetchTodos(filter) : skipToken, }) return ( <div> // 🚀 applying the filter will enable and execute the query <FiltersForm onApply={setFilter} /> {data && <TodosTable data={data} />} </div> ) } --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/ssr/#full-nextjs-pages-router-example In this guide you'll learn how to use React Query with server rendering. See the guide on Prefetching & Router Integration for some background. You might also want to check out the Performance & Request Waterfalls guide before that. For advanced server rendering patterns, such as streaming, Server Components and the new Next.js app router, see the Advanced Server Rendering guide. If you just want to see some code, you can skip ahead to the Full Next.js pages router example or the Full Remix example below. ## Server Rendering & React Query So what is server rendering anyway? The rest of this guide will assume you are familiar with the concept, but let's spend some time to look at how it relates to React Query. Server rendering is the act of generating the initial html on the server, so that the user has some content to look at as soon as the page loads. This can happen on demand when a page is requested (SSR). It can also happen ahead of time either because a previous request was cached, or at build time (SSG). If you've read the Request Waterfalls guide, you might remember this: 1. |-> Markup (without content) 2. |-> JS 3. |-> Query 1. |-> Markup (without content) 2. |-> JS 3. |-> Query With a client rendered application, these are the minimum 3 server roundtrips you will need to make before getting any content on the screen for the user. One way of viewing server rendering is that it turns the above into this: 1. |-> Markup (with content AND initial data) 2. |-> JS 1. |-> Markup (with content AND initial data) 2. |-> JS As soon as **1.** is complete, the user can see the content and when **2.** finishes, the page is interactive and clickable. Because the markup also contains the initial data we need, step **3.** does not need to run on the client at all, at least until you want to revalidate the data for some reason. This is all from the clients perspective. On the server, we need to **prefetch** that data before we generate/render the markup, we need to **dehydrate** that data into a serializable format we can embed in the markup, and on the client we need to **hydrate** that data into a React Query cache so we can avoid doing a new fetch on the client. Read on to learn how to implement these three steps with React Query. ## A quick note on Suspense This guide uses the regular useQuery API. While we don't necessarily recommend it, it is possible to replace this with useSuspenseQuery instead **as long as you always prefetch all your queries**. The upside is that you get to use <Suspense> for loading states on the client. If you do forget to prefetch a query when you are using useSuspenseQuery, the consequences will depend on the framework you are using. In some cases, the data will Suspend and get fetched on the server but never be hydrated to the client, where it will fetch again. In these cases you will get a markup hydration mismatch, because the server and the client tried to render different things. ## Initial setup The first steps of using React Query is always to create a queryClient and wrap the application in a <QueryClientProvider>. When doing server rendering, it's important to create the queryClient instance **inside of your app**, in React state (an instance ref works fine too). **This ensures that data is not shared between different users and requests**, while still only creating the queryClient once per component lifecycle. Next.js pages router: tsx // _app.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' // NEVER DO THIS: // const queryClient = new QueryClient() // // Creating the queryClient at the file root level makes the cache shared // between all requests and means _all_ data gets passed to _all_ users. // Besides being bad for performance, this also leaks any sensitive data. export default function MyApp({ Component, pageProps }) { // Instead do this, which ensures each request has its own cache: const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> ) } // _app.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' // NEVER DO THIS: // const queryClient = new QueryClient() // // Creating the queryClient at the file root level makes the cache shared // between all requests and means _all_ data gets passed to _all_ users. // Besides being bad for performance, this also leaks any sensitive data. export default function MyApp({ Component, pageProps }) { // Instead do this, which ensures each request has its own cache: const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> ) } Remix: tsx // app/root.tsx import { Outlet } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp() { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Outlet /> </QueryClientProvider> ) } // app/root.tsx import { Outlet } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp() { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Outlet /> </QueryClientProvider> ) } ## Get started fast with initialData The quickest way to get started is to not involve React Query at all when it comes to prefetching and not use the dehydrate/hydrate APIs. What you do instead is passing the raw data in as the initialData option to useQuery. Let's look at an example using Next.js pages router, using getServerSideProps. tsx export async function getServerSideProps() { const posts = await getPosts() return { props: { posts } } } function Posts(props) { const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: props.posts, }) // ... } export async function getServerSideProps() { const posts = await getPosts() return { props: { posts } } } function Posts(props) { const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: props.posts, }) // ... } This also works with getStaticProps or even the older getInitialProps and the same pattern can be applied in any other framework that has equivalent functions. This is what the same example looks like with Remix: tsx export async function loader() { const posts = await getPosts() return json({ posts }) } function Posts() { const { posts } = useLoaderData<typeof loader>() const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: posts, }) // ... } export async function loader() { const posts = await getPosts() return json({ posts }) } function Posts() { const { posts } = useLoaderData<typeof loader>() const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: posts, }) // ... } The setup is minimal and this can be a quick solution for some cases, but there are a **few tradeoffs to consider** when compared to the full approach: * If you are calling useQuery in a component deeper down in the tree you need to pass the initialData down to that point * If you are calling useQuery with the same query in multiple locations, passing initialData to only one of them can be brittle and break when your app changes since. If you remove or move the component that has the useQuery with initialData, the more deeply nested useQuery might no longer have any data. Passing initialData to **all** queries that needs it can also be cumbersome. * There is no way to know at what time the query was fetched on the server, so dataUpdatedAt and determining if the query needs refetching is based on when the page loaded instead * If there is already data in the cache for a query, initialData will never overwrite this data, **even if the new data is fresher than the old one**. * To understand why this is especially bad, consider the getServerSideProps example above. If you navigate back and forth to a page several times, getServerSideProps would get called each time and fetch new data, but because we are using the initialData option, the client cache and data would never be updated. Setting up the full hydration solution is straightforward and does not have these drawbacks, this will be the focus for the rest of the documentation. ## Using the Hydration APIs With just a little more setup, you can use a queryClient to prefetch queries during a preload phase, pass a serialized version of that queryClient to the rendering part of the app and reuse it there. This avoids the drawbacks above. Feel free to skip ahead for full Next.js pages router and Remix examples, but at a general level these are the extra steps: * In the framework loader function, create a const queryClient = new QueryClient(options) * In the loader function, do await queryClient.prefetchQuery(...) for each query you want to prefetch * You want to use await Promise.all(...) to fetch the queries in parallel when possible * It's fine to have queries that aren't prefetched. These wont be server rendered, instead they will be fetched on the client after the application is interactive. This can be great for content that are shown only after user interaction, or is far down on the page to avoid blocking more critical content. * From the loader, return dehydrate(queryClient), note that the exact syntax to return this differs between frameworks * Wrap your tree with <HydrationBoundary state={dehydratedState}> where dehydratedState comes from the framework loader. How you get dehydratedState also differs between frameworks. * This can be done for each route, or at the top of the application to avoid boilerplate, see examples > An interesting detail is that there are actually _three_ queryClients involved. The framework loaders are a form of "preloading" phase that happens before rendering, and this phase has its own queryClient that does the prefetching. The dehydrated result of this phase gets passed to **both** the server rendering process **and** the client rendering process which each has its own queryClient. This ensures they both start with the same data so they can return the same markup. > Server Components are another form of "preloading" phase, that can also "preload" (pre-render) parts of a React component tree. Read more in the Advanced Server Rendering guide. ### Full Next.js pages router example > For app router documentation, see the Advanced Server Rendering guide. Initial setup: tsx // _app.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> ) } // _app.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> ) } In each route: tsx // pages/posts.tsx import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' // This could also be getServerSideProps export async function getStaticProps() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return { props: { dehydratedState: dehydrate(queryClient), }, } } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } // pages/posts.tsx import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' // This could also be getServerSideProps export async function getStaticProps() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return { props: { dehydratedState: dehydrate(queryClient), }, } } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } ### Full Remix example Initial setup: tsx // app/root.tsx import { Outlet } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp() { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Outlet /> </QueryClientProvider> ) } // app/root.tsx import { Outlet } from '@remix-run/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' export default function MyApp() { const [queryClient] = React.useState( () => new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }), ) return ( <QueryClientProvider client={queryClient}> <Outlet /> </QueryClientProvider> ) } In each route, note that it's fine to do this in nested routes too: tsx // app/routes/posts.tsx import { json } from '@remix-run/node' import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' export async function loader() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return json({ dehydratedState: dehydrate(queryClient) }) } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute() { const { dehydratedState } = useLoaderData<typeof loader>() return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } // app/routes/posts.tsx import { json } from '@remix-run/node' import { dehydrate, HydrationBoundary, QueryClient, useQuery, } from '@tanstack/react-query' export async function loader() { const queryClient = new QueryClient() await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts, }) return json({ dehydratedState: dehydrate(queryClient) }) } function Posts() { // This useQuery could just as well happen in some deeper child to // the <PostsRoute>, data will be available immediately either way const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // This query was not prefetched on the server and will not start // fetching until on the client, both patterns are fine to mix const { data: commentsData } = useQuery({ queryKey: ['posts-comments'], queryFn: getComments, }) // ... } export default function PostsRoute() { const { dehydratedState } = useLoaderData<typeof loader>() return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } ## Optional - Remove boilerplate Having this part in every route might seem like a lot of boilerplate: tsx export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } export default function PostsRoute({ dehydratedState }) { return ( <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary> ) } While there is nothing wrong with this approach, if you want to get rid of this boilerplate, here's how you can modify your setup in Next.js: tsx // _app.tsx import { HydrationBoundary, QueryClient, QueryClientProvider, } from '@tanstack/react-query' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState(() => new QueryClient()) return ( <QueryClientProvider client={queryClient}> <HydrationBoundary state={pageProps.dehydratedState}> <Component {...pageProps} /> </HydrationBoundary> </QueryClientProvider> ) } // pages/posts.tsx // Remove PostsRoute with the HydrationBoundary and instead export Posts directly: export default function Posts() { ... } // _app.tsx import { HydrationBoundary, QueryClient, QueryClientProvider, } from '@tanstack/react-query' export default function MyApp({ Component, pageProps }) { const [queryClient] = React.useState(() => new QueryClient()) return ( <QueryClientProvider client={queryClient}> <HydrationBoundary state={pageProps.dehydratedState}> <Component {...pageProps} /> </HydrationBoundary> </QueryClientProvider> ) } // pages/posts.tsx // Remove PostsRoute with the HydrationBoundary and instead export Posts directly: export default function Posts() { ... } With Remix, this is a little bit more involved, we recommend checking out the use-dehydrated-state package. ## Prefetching dependent queries Over in the Prefetching guide we learned how to prefetch dependent queries, but how do we do this in framework loaders? Consider the following code, taken from the Dependent Queries guide: tsx // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) // Get the user const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) const userId = user?.id // Then get the user's projects const { status, fetchStatus, data: projects, } = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, // The query will not execute until the userId exists enabled: !!userId, }) How would we prefetch this so it can be server rendered? Here's an example: tsx // For Remix, rename this to loader instead export async function getServerSideProps() { const queryClient = new QueryClient() const user = await queryClient.fetchQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) if (user?.userId) { await queryClient.prefetchQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, }) } // For Remix: // return json({ dehydratedState: dehydrate(queryClient) }) return { props: { dehydratedState: dehydrate(queryClient) } } } // For Remix, rename this to loader instead export async function getServerSideProps() { const queryClient = new QueryClient() const user = await queryClient.fetchQuery({ queryKey: ['user', email], queryFn: getUserByEmail, }) if (user?.userId) { await queryClient.prefetchQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser, }) } // For Remix: // return json({ dehydratedState: dehydrate(queryClient) }) return { props: { dehydratedState: dehydrate(queryClient) } } } This can get more complex of course, but since these loader functions are just JavaScript, you can use the full power of the language to build your logic. Make sure you prefetch all queries that you want to be server rendered. ## Error handling React Query defaults to a graceful degradation strategy. This means: * queryClient.prefetchQuery(...) never throws errors * dehydrate(...) only includes successful queries, not failed ones This will lead to any failed queries being retried on the client and that the server rendered output will include loading states instead of the full content. While a good default, sometimes this is not what you want. When critical content is missing, you might want to respond with a 404 or 500 status code depending on the situation. For these cases, use queryClient.fetchQuery(...) instead, which will throw errors when it fails, letting you handle things in a suitable way. tsx let result try { result = await queryClient.fetchQuery(...) } catch (error) { // Handle the error, refer to your framework documentation } // You might also want to check and handle any invalid `result` here let result try { result = await queryClient.fetchQuery(...) } catch (error) { // Handle the error, refer to your framework documentation } // You might also want to check and handle any invalid `result` here If you for some reason want to include failed queries in the dehydrated state to avoid retries, you can use the option shouldDehydrateQuery to override the default function and implement your own logic: tsx dehydrate(queryClient, { shouldDehydrateQuery: (query) => { // This will include all queries, including failed ones, // but you can also implement your own logic by inspecting `query` return true }, }) dehydrate(queryClient, { shouldDehydrateQuery: (query) => { // This will include all queries, including failed ones, // but you can also implement your own logic by inspecting `query` return true }, }) ## Serialization When doing return { props: { dehydratedState: dehydrate(queryClient) } } in Next.js, or return json({ dehydratedState: dehydrate(queryClient) }) in Remix, what happens is that the dehydratedState representation of the queryClient is serialized by the framework so it can be embedded into the markup and transported to the client. By default, these frameworks only supports returning things that are safely serializable/parsable, and therefore does not support undefined, Error, Date, Map, Set, BigInt, Infinity, NaN, \-0, regular expressions etc. This also means that you can not return any of these things from your queries. If returning these values is something you want, check out superjson or similar packages. If you are using a custom SSR setup, you need to take care of this step yourself. Your first instinct might be to use JSON.stringify(dehydratedState), but because this doesn't escape things like <script>alert('Oh no..')</script> by default, this can easily lead to **XSS-vulnerabilities** in your application. superjson also **does not** escape values and is unsafe to use by itself in a custom SSR setup (unless you add an extra step for escaping the output). Instead we recommend using a library like Serialize JavaScript or devalue which are both safe against XSS injections out of the box. ## A note about request waterfalls In the Performance & Request Waterfalls guide we mentioned we would revisit how server rendering changes one of the more complex nested waterfalls. Check back for the specific code example, but as a refresher, we have a code split <GraphFeedItem> component inside a <Feed> component. This only renders if the feed contains a graph item and both of these components fetches their own data. With client rendering, this leads to the following request waterfall: 1. |> Markup (without content) 2. |> JS for <Feed> 3. |> getFeed() 4. |> JS for <GraphFeedItem> 5. |> getGraphDataById() 1. |> Markup (without content) 2. |> JS for <Feed> 3. |> getFeed() 4. |> JS for <GraphFeedItem> 5. |> getGraphDataById() The nice thing about server rendering is that we can turn the above into: 1. |> Markup (with content AND initial data) 2. |> JS for <Feed> 2. |> JS for <GraphFeedItem> 1. |> Markup (with content AND initial data) 2. |> JS for <Feed> 2. |> JS for <GraphFeedItem> Note that the queries are no longer fetched on the client, instead their data was included in the markup. The reason we can now load the JS in parallel is that since <GraphFeedItem> was rendered on the server we know that we are going to need this JS on the client as well and can insert a script-tag for this chunk in the markup. On the server, we would still have this request waterfall: 1. |> getFeed() 2. |> getGraphDataById() 1. |> getFeed() 2. |> getGraphDataById() We simply can not know before we have fetched the feed if we also need to fetch graph data, they are dependent queries. Because this happens on the server where latency is generally both lower and more stable, this often isn't such a big deal. Amazing, we've mostly flattened our waterfalls! There's a catch though. Let's call this page the /feed page, and let's pretend we also have another page like /posts. If we type in www.example.com/feed directly in the url bar and hit enter, we get all these great server rendering benefits, BUT, if we instead type in www.example.com/posts and then **click a link** to /feed, we're back to to this: 1. |> JS for <Feed> 2. |> getFeed() 3. |> JS for <GraphFeedItem> 4. |> getGraphDataById() 1. |> JS for <Feed> 2. |> getFeed() 3. |> JS for <GraphFeedItem> 4. |> getGraphDataById() This is because with SPA's, server rendering only works for the initial page load, not for any subsequent navigation. Modern frameworks often try to solve this by fetching the initial code and data in parallel, so if you were using Next.js or Remix with the prefetching patterns we outlined in this guide, including how to prefetch dependent queries, it would actually look like this instead: 1. |> JS for <Feed> 1. |> getFeed() + getGraphDataById() 2. |> JS for <GraphFeedItem> 1. |> JS for <Feed> 1. |> getFeed() + getGraphDataById() 2. |> JS for <GraphFeedItem> This is much better, but if we want to improve this further we can flatten this to a single roundtrip with Server Components. Learn how in the Advanced Server Rendering guide. ## Tips, Tricks and Caveats ### Staleness is measured from when the query was fetched on the server A query is considered stale depending on when it was dataUpdatedAt. A caveat here is that the server needs to have the correct time for this to work properly, but UTC time is used, so timezones do not factor into this. Because staleTime defaults to 0, queries will be refetched in the background on page load by default. You might want to use a higher staleTime to avoid this double fetching, especially if you don't cache your markup. This refetching of stale queries is a perfect match when caching markup in a CDN! You can set the cache time of the page itself decently high to avoid having to re-render pages on the server, but configure the staleTime of the queries lower to make sure data is refetched in the background as soon as a user visits the page. Maybe you want to cache the pages for a week, but refetch the data automatically on page load if it's older than a day? ### High memory consumption on server In case you are creating the QueryClient for every request, React Query creates the isolated cache for this client, which is preserved in memory for the gcTime period. That may lead to high memory consumption on server in case of high number of requests during that period. On the server, gcTime defaults to Infinity which disables manual garbage collection and will automatically clear memory once a request has finished. If you are explicitly setting a non-Infinity gcTime then you will be responsible for clearing the cache early. Avoid setting gcTime to 0 as it may result in a hydration error. This occurs because the Hydration Boundary places necessary data into the cache for rendering, but if the garbage collector removes the data before the rendering completes, issues may arise. If you require a shorter gcTime, we recommend setting it to 2 \* 1000 to allow sufficient time for the app to reference the data. To clear the cache after it is not needed and to lower memory consumption, you can add a call to queryClient.clear() after the request is handled and dehydrated state has been sent to the client. Alternatively, you can set a smaller gcTime. ### Caveat for Next.js rewrites There's a catch if you're using Next.js' rewrites feature together with Automatic Static Optimization or getStaticProps: It will cause a second hydration by React Query. That's because Next.js needs to ensure that they parse the rewrites on the client and collect any params after hydration so that they can be provided in router.query. The result is missing referential equality for all the hydration data, which for example triggers wherever your data is used as props of components or in the dependency array of useEffects/useMemos. --- ## Page: https://tanstack.com/query/latest/docs/framework/react/guides/suspense/#resetting-error-boundaries React Query can also be used with React's Suspense for Data Fetching APIs. For this, we have dedicated hooks: * useSuspenseQuery * useSuspenseInfiniteQuery * useSuspenseQueries * Additionally, you can use the useQuery().promise and React.use() (Experimental) When using suspense mode, status states and error objects are not needed and are then replaced by usage of the React.Suspense component (including the use of the fallback prop and React error boundaries for catching errors). Please read the Resetting Error Boundaries and look at the Suspense Example for more information on how to set up suspense mode. If you want mutations to propagate errors to the nearest error boundary (similar to queries), you can set the throwOnError option to true as well. Enabling suspense mode for a query: tsx import { useSuspenseQuery } from '@tanstack/react-query' const { data } = useSuspenseQuery({ queryKey, queryFn }) import { useSuspenseQuery } from '@tanstack/react-query' const { data } = useSuspenseQuery({ queryKey, queryFn }) This works nicely in TypeScript, because data is guaranteed to be defined (as errors and loading states are handled by Suspense- and ErrorBoundaries). On the flip side, you therefore can't conditionally enable / disable the Query. This generally shouldn't be necessary for dependent Queries because with suspense, all your Queries inside one component are fetched in serial. placeholderData also doesn't exist for this Query. To prevent the UI from being replaced by a fallback during an update, wrap your updates that change the QueryKey into startTransition. ### throwOnError default Not all errors are thrown to the nearest Error Boundary per default - we're only throwing errors if there is no other data to show. That means if a Query ever successfully got data in the cache, the component will render, even if data is stale. Thus, the default for throwOnError is: throwOnError: (error, query) => typeof query.state.data === 'undefined' throwOnError: (error, query) => typeof query.state.data === 'undefined' Since you can't change throwOnError (because it would allow for data to become potentially undefined), you have to throw errors manually if you want all errors to be handled by Error Boundaries: tsx import { useSuspenseQuery } from '@tanstack/react-query' const { data, error, isFetching } = useSuspenseQuery({ queryKey, queryFn }) if (error && !isFetching) { throw error } // continue rendering data import { useSuspenseQuery } from '@tanstack/react-query' const { data, error, isFetching } = useSuspenseQuery({ queryKey, queryFn }) if (error && !isFetching) { throw error } // continue rendering data ## Resetting Error Boundaries Whether you are using **suspense** or **throwOnError** in your queries, you will need a way to let queries know that you want to try again when re-rendering after some error occurred. Query errors can be reset with the QueryErrorResetBoundary component or with the useQueryErrorResetBoundary hook. When using the component it will reset any query errors within the boundaries of the component: tsx import { QueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => ( <QueryErrorResetBoundary> {({ reset }) => ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> There was an error! <Button onClick={() => resetErrorBoundary()}>Try again</Button> </div> )} > <Page /> </ErrorBoundary> )} </QueryErrorResetBoundary> ) import { QueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => ( <QueryErrorResetBoundary> {({ reset }) => ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> There was an error! <Button onClick={() => resetErrorBoundary()}>Try again</Button> </div> )} > <Page /> </ErrorBoundary> )} </QueryErrorResetBoundary> ) When using the hook it will reset any query errors within the closest QueryErrorResetBoundary. If there is no boundary defined it will reset them globally: tsx import { useQueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => { const { reset } = useQueryErrorResetBoundary() return ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> There was an error! <Button onClick={() => resetErrorBoundary()}>Try again</Button> </div> )} > <Page /> </ErrorBoundary> ) } import { useQueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' const App = () => { const { reset } = useQueryErrorResetBoundary() return ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> There was an error! <Button onClick={() => resetErrorBoundary()}>Try again</Button> </div> )} > <Page /> </ErrorBoundary> ) } ## Fetch-on-render vs Render-as-you-fetch Out of the box, React Query in suspense mode works really well as a **Fetch-on-render** solution with no additional configuration. This means that when your components attempt to mount, they will trigger query fetching and suspend, but only once you have imported them and mounted them. If you want to take it to the next level and implement a **Render-as-you-fetch** model, we recommend implementing Prefetching on routing callbacks and/or user interactions events to start loading queries before they are mounted and hopefully even before you start importing or mounting their parent components. ## Suspense on the Server with streaming If you are using NextJs, you can use our **experimental** integration for Suspense on the Server: @tanstack/react-query-next-experimental. This package will allow you to fetch data on the server (in a client component) by just calling useSuspenseQuery in your component. Results will then be streamed from the server to the client as SuspenseBoundaries resolve. To achieve this, wrap your app in the ReactQueryStreamedHydration component: tsx // app/providers.tsx 'use client' import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import * as React from 'react' import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export function Providers(props: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}> <ReactQueryStreamedHydration> {props.children} </ReactQueryStreamedHydration> </QueryClientProvider> ) } // app/providers.tsx 'use client' import { isServer, QueryClient, QueryClientProvider, } from '@tanstack/react-query' import * as React from 'react' import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental' function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 60 * 1000, }, }, }) } let browserQueryClient: QueryClient | undefined = undefined function getQueryClient() { if (isServer) { // Server: always make a new query client return makeQueryClient() } else { // Browser: make a new query client if we don't already have one // This is very important, so we don't re-make a new client if React // suspends during the initial render. This may not be needed if we // have a suspense boundary BELOW the creation of the query client if (!browserQueryClient) browserQueryClient = makeQueryClient() return browserQueryClient } } export function Providers(props: { children: React.ReactNode }) { // NOTE: Avoid useState when initializing the query client if you don't // have a suspense boundary between this and the code that may // suspend because React will throw away the client on the initial // render if it suspends and there is no boundary const queryClient = getQueryClient() return ( <QueryClientProvider client={queryClient}> <ReactQueryStreamedHydration> {props.children} </ReactQueryStreamedHydration> </QueryClientProvider> ) } For more information, check out the NextJs Suspense Streaming Example and the Advanced Rendering & Hydration guide. ## Using useQuery().promise and React.use() (Experimental) > To enable this feature, you need to set the experimental\_prefetchInRender option to true when creating your QueryClient **Example code:** tsx const queryClient = new QueryClient({ defaultOptions: { queries: { experimental_prefetchInRender: true, }, }, }) const queryClient = new QueryClient({ defaultOptions: { queries: { experimental_prefetchInRender: true, }, }, }) **Usage:** tsx import React from 'react' import { useQuery } from '@tanstack/react-query' import { fetchTodos, type Todo } from './api' function TodoList({ query }: { query: UseQueryResult<Todo[]> }) { const data = React.use(query.promise) return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) } export function App() { const query = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) return ( <> <h1>Todos</h1> <React.Suspense fallback={<div>Loading...</div>}> <TodoList query={query} /> </React.Suspense> </> ) } import React from 'react' import { useQuery } from '@tanstack/react-query' import { fetchTodos, type Todo } from './api' function TodoList({ query }: { query: UseQueryResult<Todo[]> }) { const data = React.use(query.promise) return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ) } export function App() { const query = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) return ( <> <h1>Todos</h1> <React.Suspense fallback={<div>Loading...</div>}> <TodoList query={query} /> </React.Suspense> </> ) }