@tanstack/react-query: An overview
Introduction
The micro-frontend (MFE) applications throughout the Open edX platform rely on APIs exposed by the backend services. When an MFE gets response data back from an API request, there are a few common approaches to where these data are typically stored and accessed throughout MFEs today, e.g.:
Redux. Transform and place all data returned by the APIs into a global application store.
React Context. Transform and place all data returned by the APIs into component-based context.
React State. Transform and place all data returned by the APIs into component-based state.
Composition.
Blend of all the above.
Historically, using Redux came with a fair amount of repetitive boilerplate that would make working with it a bit cumbersome, though many of the concerns around boilerplate were mitigated through the use of @reduxjs/toolkit
.
As vanilla React began to support more Redux-like features natively (e.g., useContext
, useReducer
), relying on Redux for the patterns of avoiding prop drilling and working with reducers & actions became less of a necessity. However, Context has some caveats around performance and re-rendering that require careful attention (i.e., if your component subscribes to a single property within a Context provider, your component will re-render on any change to the context. There are some workarounds for this, e.g. use-context-selector
[source]).
Over time, even newer paradigms and tools have been created to further improve data and state management within React applications, requiring less overall boilerplate. For example, @tanstack/react-query
(the focus of this document) was created to be purpose-built for working with asynchronous server state rather than client state, which libraries like Redux are more well-suited for.
For a more in-depth comparison between Redux and @tanstack/react-query
, see https://tanstack.com/query/v4/docs/react/comparison.
Client vs. server state
One of the biggest distinctions @tanstack/react-query
makes is between client and server state for web applications:
Server state represents data fetched from an external resource (e.g., API data from a Django micro-service).
Client state represents client-side only state (e.g., form input values as user fills out of a form).
As @tanstack/react-query
calls out, server state has a distinct set of concerns when compared to client state:
Data are persisted in externally to the application
Asynchronous
Can potentially become “out of date”
@tanstack/react-query
helps deal with these challenges in a more built-in, intuitive, and lightweight way compared to Redux (where we use a client-side state management library for server state):
Caching
Deduping multiple requests
Updating “out of date” data in the background
Performance optimizations and memoized query results.
Here are the claims from @tanstack/react-query
:
“Help you remove many lines of complicated and misunderstood code from your application and replace with just a handful of lines of React 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”
Getting started
@tanstack/react-query
ships with a provider component and 2 primary React hooks that your MFE application will interact with:
QueryClientProvider
useQuery
useMutation
Also, @tanstack/react-query
ships with important defaults (“aggressive but sane”) that are worth familiarizing yourself with. Some examples:
Retry any failed request up to 3 times
Any promise that throws an error.
Client-side cache duration is set to 5 minutes.
Queries will automatically be retried when the browser window is re-focused.
QueryClientProvider
The first step when using @tanstack/react-query
is to wrap your entire application with the QueryClientProvider
component, passing it a queryClient
object.
This provider ensures all queries/mutations through the application has access to the query client.
The queryClient
used in the QueryClientProvider
also houses the common, shared configuration for @tanstack/react-query
applicable to all queries throughout the application.
Code example
import {
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
const queryClient = new QueryClient({
queries: {
retry: true, // optional: you may disable automatic query retries for all queries or on a per-query basis.
},
})
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>
)
}
useQuery
This hook is used for retrieving data (e.g., GET
requests). It returns all metadata about the query, including its current state:
const {
data,
error,
isLoading,
isFetching,
isInitialLoading,
status,
refetch,
// ... see API reference for full list of properties
} = useQuery({ queryKey, queryFn });
// Previously...
const [isLoading, setIsLoading] = useState()
const [error, setError] = useState()
const [data, setData] = useState()
There are (usually) 2 required parameters with useQuery
:
queryKey
Unique to the query.
Must be an array and serializable.
Dependency array (like
useMemo
)May be used to pass arguments to the
queryFn
.Hashed deterministically (i.e., order of the keys in objects within
queryKey
does not make a difference).More details may be found here.
queryFn
Any function that returns a promise. The returned promise should either resolve the data or throw an error.
For data fetching from an API, this function is where we’d call
getAuthenticatedHttpClient().get(...)
.More details may be found here.
All query options that may be customized globally via QueryClientProvider
may also be passed on a per-query basis.
Code example
function Example() {
const { isLoading, isFetching, isInitialLoading, error, data } = useQuery({
queryKey: ['repoData'],
queryFn: () =>
// Example API call with `fetch`
return fetch('https://api.github.com/repos/tannerlinsley/react-query').then(
(res) => res.json(),
),
// Example API call with `getAuthenticatedHttpClient`
return getAuthenticatedHttpClient().get(...);
})
if (isLoading) { 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>
)
}
useMutation
This hook is used for retrieving data (e.g., POST
requests).
TBD
Code example
Invalidating the query cache
By default, @tanstack/react-query
will cache resolved query data for 5 minutes on the client before it’ll re-fetch the data from the external service. However, in certain cases, you may need to invalidate the query cache yourself. The cache invalidation is done based on the queryKey
.
For example, say there is a query that returns a list of todos. The user submits a form to add a new todo. The original query won’t be aware of the newly created todo until the query returning the list of todos is invalidated.
See the documentation for more details.
Code example
Best practices
We can learn from the broader JS community in terms of adopting best practices around the use of @tanstack/react-query
.
(Note: non-exhaustive)
Create custom React hooks
Always wrap useQuery
with a custom React hook (source):
“You can keep the actual data fetching out of the ui, but co-located with your
useQuery
call.”“You can keep all usages of one query key (and potentially type definitions) in one file.”
“If you need to tweak some settings or add some data transformation, you can do that in one place.”
Query Keys
Keep query keys next to their respective queries, co-located in a relevant feature directory.
Query key factories. Query keys are often re-used outside of a single
useQuery
call. To ensure the use of query keys is not error prone, it’s recommend to utilize a mechanism like a query key factory to organize the query keys in a re-usable, scalable way.
Testing
https://tkdodo.eu/blog/testing-react-query
Other neat features of @tanstack/react-query
(Note: non-exhaustive)
Prefetching.
Auto Refetching / Polling
Pagination
Load more & Infinite scroll
Resources
General
Tanstack Query: https://tanstack.com/query/latest
Tanstack React Query Overview: https://tanstack.com/query/v4/docs/react/overview
TkDodo’s Blog about React Query (very insightful): https://tanstack.com/query/latest/docs/react/community/tkdodos-blog
Installation: https://tanstack.com/query/v4/docs/react/installation
Quick start: https://tanstack.com/query/v4/docs/react/quick-start
Important defaults: https://tanstack.com/query/v4/docs/react/guides/important-defaults
Query Keys: https://tanstack.com/query/v4/docs/react/guides/query-keys
Query Key Factory: https://tanstack.com/query/latest/docs/react/community/lukemorales-query-key-factory
@tanstack/eslint-plugin-query
: https://tanstack.com/query/latest/docs/react/eslint/eslint-plugin-queryDoes
@tanstack/react-query
replace Redux? https://tanstack.com/query/latest/docs/react/guides/does-this-replace-client-state
Open edX-specific