Inside React Query - TkDodo's Blog
Inside React Query - TkDodo's Blog
TkDodo's blog
Blog Tags Sponsors Rss Twitter Github
I've been asked a lot lately how React Query works internally. How does it know when to
re-render? How does it de-duplicate things? How come it's framework-agnostic?
These are all very good questions - so let's take a look under the hood of our beloved
async state management library and analyze what really happens when you call
useQuery .
https://tkdodo.eu/blog/inside-react-query 2/20
8/1/24, 7:09 PM Inside React Query | TkDodo's blog
It all starts with a QueryClient . That's the class you create an instance of, likely at the
start of your application, and then make available everywhere via the
QueryClientProvider :
query-client-provider
JS Copy
1 import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
2
3 // ⬇️this creates the client
4 const queryClient = new QueryClient()
5
6 function App() {
7 return (
8 // ⬇️this distributes the client
9 <QueryClientProvider client={queryClient}>
10 <RestOfYourApp />
11 </QueryClientProvider>
12 )
13 }
https://tkdodo.eu/blog/inside-react-query 3/20
A vessel that holds the cache
8/1/24, 7:09 PM Inside React Query | TkDodo's blog
It might not be well known, but the QueryClient itself doesn't really do much. It's a
container for the QueryCache and the MutationCache , which are automatically created
when you create a new QueryClient .
It also holds some defaults that you can set for all your queries and mutations, and it
provides convenience methods to work with the caches. In most situations, you will not
interact with the cache directly - you will access it through the QueryClient .
QueryCache
Alright, so the client lets us work with the cache - what is the cache?
Simply put - the QueryCache is an in-memory object where the keys are a stably
serialized version of your queryKeys (called a queryKeyHash) and the values are an
instance of the Query class.
I think it's important to understand that React Query, per default, only stores data in-
memory and nowhere else. If you reload your browser page, the cache is gone. Have a look
at the persisters if you want to write the cache to an external storage like localstorage.
Query
https://tkdodo.eu/blog/inside-react-query 4/20
8/1/24, 7:09 PM Inside React Query | TkDodo's blog
The cache has queries, and a Query is where most of the logic is happening. It not only
contains all the information about a query (its data, status field or meta information like
when the last fetch happened), it also executes the query function and contains the retry,
cancellation and de-duplication logic.
It has an internal state machine to make sure we don't wind up in impossible states. For
example, if a query function should be triggered while we are already fetching, that fetch
can be de-duplicated. If a query is cancelled, it goes back to its previous state.
Most importantly, the query knows who's interested in the query data, and it can inform
those Observers about all changes.
QueryObserver
https://tkdodo.eu/blog/inside-react-query 5/20
8/1/24, 7:09 PM Inside React Query | TkDodo's blog
Observers are the glue between the Query and the components that want to use it. An
Observer is created when you call useQuery , and it is always subscribed to exactly one
query. That's why you have to pass a queryKey to useQuery . 😉
The Observer does a bit more though - it's where most of the optimizations happen. The
Observer knows which properties of the Query a component is using, so it doesn't have
to notify it of unrelated changes. As an example, if you only use the data field, the
component doesn't have to re-render if isFetching is changing on a background refetch.
Even more - each Observer can have a select option, where you can decide which parts
of the data field you are interested in. I've written about this optimization before in #2:
React Query Data Transformations. Most of the timers, like ones for staleTime or interval
fetching, also happen on the observer-level.
Active and inactive Queries
A Query without an Observer is called an inactive query. It's still in the cache, but it's not
being used by any component. If you take a look at the React Query Devtools, you will see
that inactive queries are greyed out. The number on the left side indicates the number of
Observers that are subscribed to the query.
https://tkdodo.eu/blog/inside-react-query 6/20
8/1/24, 7:09 PM Inside React Query | TkDodo's blog
Putting it all together, we can see that most of the logic lives inside the framework-
agnostic Query Core: QueryClient , QueryCache , Query and QueryObserver are all
there.
That's why it's fairly straightforward to create an adapter for a new framework. You
basically need a way to create an Observer , subscribe to it, and re-render your
component if the Observer is notified. The useQuery adapters for react and solid each
have around 100 lines of code only.
https://tkdodo.eu/blog/inside-react-query 7/20
From a component perspective
8/1/24, 7:09 PM Inside React Query | TkDodo's blog
Lastly, let's look at the flow from another angle - starting with a component:
https://tkdodo.eu/blog/inside-react-query 8/20
8/1/24, 7:09 PM Inside React Query | TkDodo's blog
https://tkdodo.eu/blog/inside-react-query 9/20
8/1/24, 7:09 PM Inside React Query | TkDodo's blog
91 reactions
👍 28 🎉 19 ❤️ 27 🚀 17
Write Preview
Sign in to comment
1 6 replies
https://tkdodo.eu/blog/inside-react-query 10/20
8/1/24, 7:09 PM Inside React Query | TkDodo's blog
forceUpdate(x => x + 1)
now, it's useSyncExternalStore , where you can implement a subscribe callback that will re-
render your component and return a snapshot:
React.useSyncExternalStore(
React.useCallback(
(onStoreChange) >
https://tkdodo.eu/blog/inside-react-query 12/20
8/1/24, 7:09 PM Inside React Query | TkDodo's blog
(onStoreChange) =>
isRestoring
? () => undefined
: observer.subscribe(notifyManager.batchCalls(onStoreChange)),
[observer, isRestoring],
),
() => observer.getCurrentResult(),
() => observer.getCurrentResult(),
)
https://github.com/TanStack/query/blob/792e0b24368b1650fdd904c9e4c85cdcf7118867/
packages/query-core/src/utils.ts#L424-L437
https://github.com/TanStack/query/blob/792e0b24368b1650fdd904c9e4c85cdcf7118867/
packages/query core/src/queryts#L201
https://tkdodo.eu/blog/inside-react-query 13/20
8/1/24, 7:09 PM
packages/query-core/src/query.ts#L201 Inside React Query | TkDodo's blog
I found that when using useQuery without any options like suspense , the queryFn is triggered at the
same level of useEffect , because this useEffect triggers setOptions() and this calls queryFn .
I think it could be on render phase instead of effect phase. Because useEffect is called after
https://tkdodo.eu/blog/inside-react-query 14/20
8/1/24, 7:09 PM
p p
Inside React Query | TkDodo's blog
rendering is done, "triggered in useEffect" vs "triggered while rendering" will have a performance
difference (yes, it will be small - about 10ms, though. 😅 ).
like this way (briefly writed)
// as is
// of course, indeed - here are many many more complex steps & options
const useQuery = (queryFn) => {
useEffect(() => {
queryFn(); // <- called while effect phase
}, [queryFn]);
}
// to be
// something like this would be better in performance
const useQuery = (queryFn) => {
const prevRef = useRef();
This would be a terrible code in plain react, but react-query uses its own observer, so this pattern
seems not that wierd.
So my question is: is this behavior intended despite this can lead to a little poor performance?
Again, thank you for a great article and library! I love tanstack query :)
1 ❤️ 1 2 replies
👍 3 ❤️ 1
https://tkdodo.eu/blog/inside-react-query 15/20
8/1/24, 7:09 PM Inside React Query | TkDodo's blog
alexl-shopware Feb 15
Very well written and clear. I'm impressed. I'm a dev with 6 years experience but still have a hard time
grasping architectures and technical details. You laid it out masterfully and was easy to follow along.
1 ❤️ 1 0 replies
morijenab Mar 5
Hi,
Great blog thanks!
I have a question and hope here is the right place to ask.
What is the main question:
Where is the React Query cache stored?
more context:
We are using micro front end structure more specifically( Single Spa package).
Each applications has its own cache provider, when i switch between different applications the
application is gone ( tested by useeffect cleanup function inside App level).
What is strange to me?
when switching btw different applications:
1-Application A mounted (react query cache is empty).
2-Application B mounted and Application A unmounted.
3-Application B unmounted and Application A mounted(now here before api calls finish the react
query cache is exacllty like the last time it was mounted
Each application is a separate project being mounted/unmounted under the single spa management.
1 3 replies
https://tkdodo.eu/blog/inside-react-query 17/20
8/1/24, 7:09 PM Inside React Query | TkDodo's blog
the client holds the cache, and the client lives in memory.
morijenab Mar 5
Thanks for your response
Lets consider this structure
this is the highest level in the project and as you can see the queryClient wrap the entire App.
if i switch the application i will see the console unMounted . here is the question what happen to
the QueryClientProvider after this phase?
if it lives in the memory i expect this should be gone. however after i switch back to this app i
can see the QueryClientProvider has the last data.
export const Index = () => {
useEffect(() => {
return () => {
console.log('unMounted!');
};
}, []);
return (
<CacheProvider value={customCache}>
<ThemeProvider theme={defaultTheme}>
<QueryClientProvider client={ReactQueryClient}>
<I18nextProvider i18n={i18n}>
<App />
</I18nextProvider>
</QueryClientProvider>
</ThemeProvider>
</CacheProvider>
);
};```
pooooriya Apr 16
Hi, thanks for your explanation!
I have a question:
Why do you store the whole object inside the QueryCache class and within a property? Why not use
something like WeakMap, which works well with garbage collection?
In the codebase, I saw a garbage collection timer that removes the whole object when an observer is
https://tkdodo.eu/blog/inside-react-query 18/20
8/1/24, 7:09 PM Inside React Query | TkDodo's blog
removed (I think when the component unmounts). How do you handle storing large objects in
memory and manage that effectively?
I have a lot of questions about storing data inside class properties versus other options. I noticed the
useSWR codebase uses WeakMap internally, and seeing only a class handle all this logic made me
wonder about your approach.
2 1 reply
harsh-lamba Jun 19
Hi Great blogs, Thanks for the explanation.
But I have query along use case in our system and wanted your input in it:
Use Case:
We have hook (for instance useStatus) that wraps the react query. This hook is being used in parent
child hierarchy . In parent component, this hook is being used and it is also being used in child and
grand-child as well. But Grand child is being rendered asynchronously so we see two duplicate
request on network.
Reason behind: Why do we have duplicate request on Network tab?
Since components were rendered in different lifecycle, that is the reason we have two different
network call. (Stale time:0 [Which is our requirement]). And we are facing these problems with a lot
of components.
Following are the approaches that we thought to avoid these multiple request:
Use of refetchOnMount: false
This solution is doing background fetch when we come back from Route A to Route B (Already data
was cached and stale).
Use of refetchOnMount: false + cacheTime: 1s
This solution will do background fetch but will also render the component (which we don't want).
May be we could put some staleTime: 1s:
With this we might be able to overcome with multiple duplicate request problem.
Also possibly we could avoid such instances by passing props from parent to child to grand child
component but don't want to be a part of props drilling.
https://tkdodo.eu/blog/inside-react-query 19/20
Could you provide your feedback for following:
8/1/24, 7:09 PM Inside React Query | TkDodo's blog
https://tkdodo.eu/blog/inside-react-query 20/20