3 Useful Patterns for React Query

Steve Hankin
4 min readFeb 16, 2021
Photo by Alex Loup on Unsplash

If you’re using ReactJS, there is a good chance that you access a data service that supports either REST or GraphQL

Interacting with a data layer presents numerous problems:

  • Duplicate requests (e.g. multiple components calling one endpoint)
  • Caching data
  • Updating stale data
  • Handling transitions from loading to success/error

I’ve been using React Query to wrap an Axios layer inside a Typescript React app and find it a real joy to work with, eliminating a lot of the usual boilerplate code that was both bloating the codebase and sometimes causing bugs due to inconsistencies in approach

The bulleted issues above are listed in the React Query documentation and a comparison of React Query vs other libraries is available here. If you’re not already using a library, I’d highly recommend spending half an hour looking at React Query

But out of the box I was encountering a few problems that weren’t immediately obvious from the documentation. I’ll discuss my solutions here and if you have alternatives I would be very interested to hear!

How React Query works

If you already use React Query, then you can skip this.

The React Query API declares a dependency on an asynchronous data source using a React Hook:

const myQuery = useQuery(key, asyncFetchFn)

The hook typically accepts a tuple specifying a key (which can be a simple string or an array) and an asynchronous function to fetch the data, but we’ll see another approach in the next section

#1 — Sharing queries across components

Let’s take a simple example where we’ll get the latest Bitcoin prices from the Coindesk API:

export const getPrices = async () => 
(await
fetch("https://api.coindesk.com/v1/bpi/currentprice.json")
).json();

This gives current prices in multiple currencies. If we wanted to use this data in different components (e.g. one component uses the prices for a table or chart and another component perhaps uses the timestamps) then we’d need a way to share the React Query call so that the first call will perform the GET and the second would benefit from the cache.

One way would be to make identical calls such as the following:

Component1.tsx:
const currentPrices = useQuery("currentPrices", getPrices)
Component2.tsx:
const currentPrices = useQuery("currentPrices", getPrices)

As long as both queries pass the same Query Key and Query Function, we’ll be able to leverage the React Query cache.

But having hardcoded text for the key is fragile. How about extracting a constant? We could do that and that would help protect against typos, but it still doesn’t give us the tight association between the key and the query function.

It might also be tempting to consider pushing the “useQuery()” into a shared function, but this will throw an error that the Rules of Hooks are being violated (specifically that hooks should be declared at the top-level of components)

This is where we can use the Query Object form.

First, let’s create a function that returns a Query Object:

export const getPricesQO = () => 
({
queryKey: "currentPrices",
queryFn: getPrices
});

Next, we can reference the QO in our components:

Component1.tsx
const currentPrices = useQuery(getPricesQO())
Component2.tsx
const currentPrices = useQuery(getPricesQO())

Perhaps we want to make the query cached depending on the currency? We could easily pass in a currency and make the query key an array as follows:

api.ts
export const getPricesQO = (ccy) =>
({
queryKey: ["currentPrices", ccy],
queryFn: getPrices(ccy)
});
Component1.tsx
const currentPrices = useQuery(getPricesQO(ccy))

#2 — Data transformation

Often we’ll need to transform the data structurally or extract subsets depending on the context. For example, I mentioned executing and caching the same query for different parameters. In this case, the query already provides multiple currencies in the same response

So how do we retrieve the currencies separately?

The first reaction might be to consider modifying the query object to accept the currency as a parameter (as I mentioned previously), but this has a downside; the different key will result in a cache-miss and the API being called again

Instead, we can use the “select” option which accepts a function that returns a data view.

For example, suppose we want to separately extract the GBP and Time data from the resolved query. We could define two new query objects that define select functions:

/**
* Selects only GBP from Query
*/
export const getGbpQO = (params?: {
enabled?: boolean;
}): QueryObserverOptions<BpiData, unknown, GBP> => ({
...pricesDefaults(params?.enabled),
select: (_) => _.bpi.GBP,
});
/**
* Selects only Time from Query
*/
export const getTimeQO = (params?: {
enabled?: boolean;
}): QueryObserverOptions<BpiData, unknown, Time> => ({
...pricesDefaults(params?.enabled),
select: (_) => _.time,
});

#3 — Conditionally suppressing queries

Often when components are bootstrapped, their key data might be undefined. In these situations, you would probably want to prevent the query from running at all, especially if the query relies on the missing key. Typically you’ll encounter this when a subcomponent needs something from the parent that isn’t available yet (imagine: Amazon Orders, where each order is a component mapped from a parent query that hasn’t completed yet)

Since React Query is implemented as hooks, it’s going to run as soon as the Component is rendered, whether you like it or not, but you don’t need to suddenly start wrapping the component in conditionals

Instead, there is a flag available in the Query Object called “enabled”

export const getPricesQO = (params?: {
enabled: boolean;
}): QueryObserverOptions<BpiData, Error> => ({
queryKey: "currentPrices",
queryFn: getPrices,
enabled: params?.enabled === true
});

Summary

  • Sharing queries across components — use Query Objects
  • Data transformation — the select option to the rescue!
  • Conditionally suppressing queries — try the “enabled” flag

Putting this together, I’ve created a Sandbox you can play with:

Hope this helps and I’m really interested in hearing your experiences with React Query and any hints and tips you have!

--

--

Steve Hankin

I’m a Software Engineer working with Financial Services in the UK