@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.:

  1. Redux. Transform and place all data returned by the APIs into a global application store.

  2. React Context. Transform and place all data returned by the APIs into component-based context.

  3. React State. Transform and place all data returned by the APIs into component-based state.

  4. Composition.

  5. 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

API Reference

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

API Reference

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)

Resources

General

  • Tanstack Query:

  • Tanstack React Query Overview:

  • TkDodo’s Blog about React Query (very insightful):

  • Installation:

  • Quick start:

  • Devtools:

  • Important defaults:

  • Query Keys:

  • Query Key Factory:

  • @tanstack/eslint-plugin-query:

  • Does @tanstack/react-query replace Redux?

  • Course:

Open edX-specific

  • Prior art in code:

    • frontend-app-learner-portal-enterprise

      • useCheckSubsidyAccessPolicyRedeemability (source)

        • Uses useQuery.

      • useStatefulEnroll (source)

        • Uses useQuery and useMutation as dependent queries.

      • useRedemptionStatus (source)

        • Uses queryClient.invalidateQueries()