Inside React Query
— ReactJs , React Query , TypeScript , JavaScript — 4 min read
- #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
- #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
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
.
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
:
1import { QueryClient, QueryClientProvider } from '@tanstack/react-query'2
3// ⬇️ this creates the client4const queryClient = new QueryClient()5
6function App() {7 return (8 // ⬇️ this distributes the client9 <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
.
A vessel that holds the cache
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
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
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.