100% found this document useful (1 vote)
51 views

Inside React Query - TkDodo's Blog

an article about react query library
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
100% found this document useful (1 vote)
51 views

Inside React Query - TkDodo's Blog

an article about react query library
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 20

8/1/24, 7:09 PM Inside React Query | TkDodo's blog

TkDodo's blog
Blog Tags Sponsors Rss Twitter Github

Inside React Query


03.12.2022 — ReactJs, React Query, TypeScript, JavaScript — 4 min read

Photo by Tim Mossholder


#1: Practical React Query
#2: React Query Data Transformations
#3: React Query Render Optimizations
#4: Status Checks in React Query
#5: Testing React Query
#6: React Query and TypeScript
#7: Using WebSockets with React Query
#8: Effective React Query Keys
#8a: Leveraging the Query Function Context
https://tkdodo.eu/blog/inside-react-query 1/20
8/1/24, 7:09 PM Inside React Query | TkDodo's blog

#9: Placeholder and Initial Data in React Query


#10: React Query as a State Manager
#11: React Query Error Handling
#12: Mastering Mutations in React Query
#13: Offline React Query
#14: React Query and Forms
#15: React Query FAQs
#16: React Query meets React Router
#17: Seeding the Query Cache
#18: Inside React Query
#19: Type-safe React Query
#20: You Might Not Need React Query
#21: Thinking in React Query
#22: React Query and React Context
#23: Why You Want React Query
#24: The Query Options API
#25: Automatic Query Invalidation after Mutations

React component &


prop analytics
Measure usage,
optimize your
codebase, drive
adoption. Start free
Ads by EthicalAds

한국어 日本語 Add translation

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

To understand the architecture, we have to start at the beginning:


The QueryClient

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 }

The QueryClientProvider uses React Context to distribute the QueryClient


throughout the entire application. The client itself is a stable value - it's created once (make
sure you don't inadvertently re-create it too often), so this is a perfect case for using
Context. It will not make your app re-render - it just gives you access to this client via
useQueryClient .

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

The complete picture

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:

the component mounts, it calls useQuery , which creates an Observer .


that Observer subscribes to the Query , which lives in the QueryCache .
that subscription might trigger the creation of the Query (if it doesn't yet exist), or it
might trigger a background refetch if data is deemed stale.
starting a fetch changes the state of the Query , so the Observer will be informed
about that.
The Observer will then run some optimizations and potentially notify the component
about the update, which can then render the new state.
after the Query has finished running, it will inform the Observer about that as well.
Please note that this is just one of many potential flows. Ideally, data would be in the cache
already when the component mounts - you can read more about that in #17: Seeding the
Query Cache.
What's the same for all flows is that most of the logic happens outside of React (or Solid or
Vue), and that every update from the state machine is propagated to the Observer , who
then decides if the component should also be informed.
I hope it's now a bit clearer how React Query works internally. As always, feel free to reach
out to me on twitter if you have any questions, or just leave a comment below. ⬇️
Like the monospace font in the code blocks?
Check out monolisa.dev

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

22 comments · 25 replies – powered by giscus Oldest Newest

Write Preview
Sign in to comment

Sign in with GitHub

devagrawal09 Dec 4, 2022


Great write-up! I was looking for something exactly like this
1 ❤️ 4 0 replies

yuvalshimoni Dec 5, 2022


Hi great blog, thanks!
Small side question, why do you using 'useState' here instead of 'use useMemo'?
const [observer] = React.useState(
() =>
new Observer<TQueryFnData, TError, TData, TQueryData, TQueryKey>(
queryClient,
defaultedOptions,
),
)

1 6 replies

Show 1 previous reply


Pelv Dec 5, 2022
https://beta.reactjs.org/apis/react/useMemo#should-you-add-usememo-everywhere

https://tkdodo.eu/blog/inside-react-query 10/20
8/1/24, 7:09 PM Inside React Query | TkDodo's blog

yuvalshimoni Dec 5, 2022


It's not answered.. I trying to understand why memo with useState

Pelv Dec 5, 2022 edited


To ensure that the observer never change.
UseMemo is useful for perf optimisation, it's not a "real memorisation". It allows you to skip
some re-render, not all.

ivanjeremic Dec 5, 2022


also when you pass a function to useState it is for more expensive initial state
👍2

TkDodo Dec 6, 2022 Owner


To ensure that the observer never change.
This is the correct answer. We want to create one instance of the Observer for the lifetime of
the component. I do have a separate article on why useState is the best way to achieve that:
https://tkdodo.eu/blog/use-state-for-one-time-initializations
❤️ 2

yuvalshimoni Dec 5, 2022


Can you explain a bit about notifyManager?
1 1 reply

TkDodo Dec 7, 2022 Owner


The notifyManager is a mechanism for us to batch certain updates together. If multiple calls
would trigger an update in the same microtask, we generally defer the update to produce fewer
re-renders.

praveenkumar66 Dec 5, 2022


Two months back I texted you in twitter asking about this. Now it is here. Thanks for your
commitment. Great blog.
1 👍1 🚀1 0 replies
https://tkdodo.eu/blog/inside-react-query 11/20
8/1/24, 7:09 PM Inside React Query | TkDodo's blog

samuellawrentz Dec 6, 2022


Yo thanks for making it look so simple!! This is an awesome library.
1 ❤️ 1 0 replies

ghost Dec 7, 2022


Thanks so much for the explanation. Btw. what program did you use to create the charts? They are
beautiful and I'd like to create similar ones =)
1 ❤️ 1 1 reply

TkDodo Dec 7, 2022 Owner


It's called excalidraw and it's open source. You can use it here: https://excalidraw.com/
❤️ 1

j0j032 Dec 7, 2022


Thank you Dominik! always good to be backstage 😎
1 🎉1 0 replies

meteor91 May 25, 2023


Hi! Can you explain how you force react to rerender, when data is loaded or when refetch called?
1 1 reply

TkDodo May 26, 2023 Owner


before v4, it was just a forceUpdate :
const [, forceUpdate] = React.useState(0)

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(),
)

raphaeljcm Jul 12, 2023


That was a great article! After using RQ a lot for the past months, It is nice to understand how it
works under the hood. Thanks a lot!
1 ❤️ 1 0 replies

StyleShit Jul 30, 2023


Great article! 🚀
So the QueryObserver is responsible for avoiding re-renders (or actually "not notifying") when the
response is the same?
If so, can you please point me to the area in the code where it happens?
Thanks 🙏
2 👀1 6 replies

Show 1 previous reply


StyleShit Jul 31, 2023
I see that there is shallow comparison of the data, but doesn't the data reference always
change on each refetch even if the response is identical? (e.g. you JSON.parse() the same
string)
If so, how does this comparison work? I think I'm missing something here

TkDodo Jul 31, 2023 Owner edited


structuralSharing , a "smart, deep-equal" copying, happens on Query level:

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

you can read more about it here: https://tkdodo.eu/blog/react-query-render-


optimizations#structural-sharing

StyleShit Jul 31, 2023


Oh, interesting!
So just to make sure I got it right, this is the flow?
1. Got new result from the queryFn
2. Create a smart deep copy of the result
3. Put it in the cache
4. Observer will run (and eventually call queryObserver.updateResult( ... ) )
5. Shallowly compare the prev & next data (since we keep references using structural sharing
for both cache & selectors)
5.1. If the data has changed - notify listeners
5.2. Otherwise - do nothing

TkDodo Aug 1, 2023 Owner


pretty much, yeah. What the observer will do additionally is keeping track of which fields of the
result is actually used by the component. So if you use const { data, status } =
useQuery(...) , you will be notified of changes to the data and status field. If you also use
isFetching , then your component will receive more updates, as this prop changes more often.
This is know as "tracked queries", and is the default since v4: https://tkdodo.eu/blog/react-
query-render-optimizations#tracked-queries

StyleShit Aug 1, 2023


Just looked at the PR and it's really cool! (and smart with its simplicity)
Thanks for all of your work, you're awesome 🙏
❤️ 1

woohm402 Sep 8, 2023


Hi, thank you for writing a great article :)
What I understood is: react-query is just an adapter of react and query-core . When calling
useQuery , QueryObserver is created, and then query-core subscribes / calls queryFn itself.

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();

if (someConditionCalculatedFromPrevRefCurrentValue) queryFn(); // <- called while rende


}

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

TkDodo Sep 8, 2023 Owner


render functions need to be pure. triggering a side effect like data fetching in them is
technically against the rules of react. This can be troublesome when combined with suspense
or concurrent features, where rendering can be interrupted. We also need an active observer
first, and that subscription happens in useSyncExternalStore , which has similar timings to
useEffect .

👍 3 ❤️ 1

woohm402 Sep 8, 2023


Ah... I see. Thanks! 🙏
👍1

trongtai37 Sep 21, 2023


It's awesome!
1 ❤️ 1 0 replies

https://tkdodo.eu/blog/inside-react-query 15/20
8/1/24, 7:09 PM Inside React Query | TkDodo's blog

debater-coder Sep 24, 2023


How does it know which fields are being tracked?
1 1 reply

TkDodo Sep 24, 2023 Owner


We use Object.defineProperty with custom getters and track which field gets accessed on the
result returned from useQuery

mechaniccoder Oct 25, 2023


Thanks for the great article!
1 🎉1 0 replies

idiglove Nov 29, 2023


I love it! Thanks for this. I have a question, how do you handle clean ups on useMutation? Do you use
fetch API in the background, how do you handle abort controllers?
1 1 reply

TkDodo Dec 1, 2023 Owner


mutations can't be aborted because they have side-effects on the server

dovranJorayev Nov 30, 2023


I love it! Thanks for this. I have a question, how do you handle clean ups on useMutation? Do
you use fetch API in the background, how do you handle abort controllers?
I guess you mean unsubcribe callbacks like in useEffect, then i guess generally it is unsafe to cancel
mutations, and it is indeterminated what will happen on cancel, does data changed on the external
storage or not. For this reason it is a page in docs about query cancelation and is not about mutation
cancelation
And if you question about, how making a request happens inside useMutation, you are welcome to
explore the source code of it 😄
It is interesting part of open source i think
1 0 replies
https://tkdodo.eu/blog/inside-react-query 16/20
8/1/24, 7:09 PM Inside React Query | TkDodo's blog

mechaniccoder Dec 2, 2023


Great explanation of architecture!
1 ❤️ 1 0 replies

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

TkDodo Mar 5 Owner


Where is the React Query cache stored?
In memory, in the queryClient . When you do new QueryClient() , this is actually:
const queryClient = new QueryClient({
queryCache: new QueryCache(),
mutationCache: new MutationCache()
})

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>
);
};```

TkDodo Mar 7 Owner


the provider is just react context. The question is where is the ReactQueryClient coming
from...
👍2

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

TkDodo Apr 22 Owner


Why not use something like WeakMap, which works well with garbage collection?
This design pre-dates my involvement, so I can only guess. WeakMap will automatically garbage
collect when there is no reference anymore. What we do is start a timer and then only remove if
there is no reference after the timer has elapsed.
How do you handle storing large objects in memory and manage that effectively?
Not sure what you mean. We don't do manual memory management 😅

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

if I understood the problem correctly for duplicate request?


is there any solution to avoid multiple duplicate refetch and also refetch when user visit the page
again?
React query version: 3.39.3
React: 17.0.2
1 1 reply

TkDodo Jun 19 Owner


My recommendation would be set a staleTime that is small enough to dedupe the requests in
i dl hi l d f l h i
© 2024 by TkDodo's blog. All rights reserved.
Theme by LekoArts

https://tkdodo.eu/blog/inside-react-query 20/20

You might also like