<VR>

  • Home
  • Talks
  • Podcast
  • Blog

How react query works

Published on Feb 22, 2022

🛫3 mins to read

  • react query
  • react
  • data fetching
  • technical
  • articles
  • code

One of the best talks I’ve seen on the concept of React and hooks is Shawn Wang’s Getting Closure on React Hooks. In the talk, he reimplements the concept of hooks from scratch to show how it works internally. By breaking it down this way, you can grasp that a fairly complex concept is essentially an abstraction and a matter of convenience. Once you understand it you can reason about code a lot easier.

I attempted to do something similar here with React Query. React query is a cool library for data fetching and in-memory caching on the browser. It has a lot of niceties like prefetching data, background refresh etc. which solve a lot of common problems with SPAs like over-fetching data or a client having stale data. Although I didn’t follow the API 1:1, I think this exercise helps understand the concepts clearly. Let’s get started!

APIs that send JSON

Think of some API that returns JSON. I love using JSONPlaceholder for exercises like these as it has a wide spread of data types and relations.

https://jsonplaceholder.typicode.com/todos/1

It returns data like the following:

{ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }

Typically, you would have a method that wraps the calling of this API. All this method does is abstract away the pieces where you unpack the response from the fetch request.

const fetchData = async () => { const res = await fetch('https://jsonplaceholder.typicode.com/todos/1'); const data = await res.json(); return data; }; // You would call it like below: const data = await fetchData();

Add decorations to the data

Instead of simply returning the data, let’s decorate it with status variables like success and error.

When data fetch is successful:

  • success is true
  • error is false
  • data is whatever is returned from the API
const fetchData = async () => { const res = await fetch('https://jsonplaceholder.typicode.com/todos/1'); const data = await res.json(); return { data, error: false success: true }; }

Add error-handling

In the error scenario, the values for success and error are flipped and data is null. Let’s wrap the whole async data call in a try/catch block as well so we can handle both scenarios. Note: The idea of returning null for data in failure scenarios is a departure from how React query actually handles but this is a reduced use-case.

const fetchData = async () => { try { const res = await fetch('https://jsonplaceholder.typicode.com/todos/1'); const data = await res.json(); return { data, error: false, success: true, }; } catch (err) { return { data: null, error: `${err}`, success: false, }; } };

Let’s make a helper method that will call the API for us

This function returns a Promise which can internally resolve to one of the two scenarios above: success or failure. The key thing to note here is instead of defaulting to fetch data from the same endpoint, we are asking the caller of this function to pass in the data fetching method. This way, our API is an abstraction around all fetch calls instead of just that one fetch call.

const apiCaller = (dataFetchingFunction) => { return new Promise(async (resolve, reject) => { try { const data = await dataFetchingFunction(); resolve({ data, error: false, success: true, }); } catch (err) { reject({ data: null, error: `${err}`, success: false, }); } }); };

Now, we can use that fetchData method from before to call our apiCaller utility like below:

const { data, error, success } = await apiCaller(fetchData);

Callout from the use of await , this signature looks similar to what you would receive from React Query. However, it’s missing a key functionality - ability to store results in a cache object and retrieve it from cache before actually making a call.

Let’s build the cache layer

This can be any global variable and we can use the Object data structure itself as it has constant lookup O(1). All it needs is a key to store data for each API call. Most times you can reuse the URL itself as the cache key but it could also be any unique key.

{ 'UNIQUE-CACHE-KEY': { data: "here" } }

Our example above becomes an object whose key is any unique key we choose, and value becomes the API response.

{ 'POST-1': { "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false } }

Now let’s build the functionality where we check cache before triggering an API call.

const cache = {}; const cachedApiCalls = (key, dataFetchingFunction) => { if (cache[key]) { // return data from cache } else { // call the API // store data in cache // return data } };

All this does is check cache to see if the key already exists. If it does, return data from the cache object. Else, make the call, store it in cache and then return the data. This way subsequent calls will only return data from the cache object. Let’s remove psuedo-code and fill up with actual code.

const cachedApiCalls = (key, dataFetchingFunction) => { if (cache[key]) { return new Promise(async (resolve, reject) => { resolve({ data: cache[key], error: false, success: true, }); }); } else { return new Promise(async (resolve, reject) => { try { // call the API const data = await dataFetchingFunction(); // store data in cache cache[key] = data; resolve({ data, error: false, success: true, }); } catch (err) { // Do not cache, if error reject({ data: null, error: `${err}`, success: false, }); } }); } };

Function signatures

Let’s compare this function signature to ones you would use with React Query. The key difference when using React Query is, it does not require using await keyword.

cachedApiCalls(cacheKey, fetcherFunction); const { data, error } = await cachedApiCalls(fetcherFunction, cacheKey); const { data, error } = useQuery('header', getHeader);

How this maps to React Query

  • When you create a new QueryClient, all it does is create that global cache object which is checked prior to making API calls
  • This example explains why you need to pass it a key along with a fetcher function. It is a very dumb cache object, meaning it only knows to check via key. If you pass the same response but with a different key, it’ll make another entry.
  • The fetcher function can be any async function - doesn’t necessarily have to be an API call
  • Enhancements to this article are in progress, but here’s a sneak peak
Enhancements (WIP) ## Imagine passing down props several layers deep <ComponentA user={user}> <ComponentB user={user}> <ComponentC user={user}> <ComponentD user={user}> <ComponentE user={user}></ComponentE> </ComponentD> </ComponentC> </ComponentB> </ComponentA> ## Let’s use context React context is a way to use “global variables” within a tree structure import React from 'react'; const userContext = React.createContext({ user: {} }); export { userContext };

Further things to think about

  • Cache eviction strategies
  • Adding a loading state
  • How to handle server side rendering
  • How to pre-fill cache
  • Background fetching, fetching on refocus etc.
  • Making it a React component
  • Query cancellation
  • Testing
  • Query retrying

Further reading

  • If you want to see an actual reimplementation, check out Tanner Linsley’s (creator of React Query’s) talk from Let's Build React Query in 150 Lines of Code! Highly recommend the talk which goes into more detail. The reason it’s more complex is because we don’t await a react query call. It implements subscriber pattern to update data when the promise resolves and the code is more complex.
  • Check out all the different pieces of data that is returned by React query in the official API docs. React query helps with distributed development where multiple people can build components that all handle their own data fetching but without overloading the backend servers or making multiple data calls to the same endpoint from the same page.

Built with passion...

React

Used mainly for the JSX templating, client-side libraries and job secruity.

Gatsby

To enable static site generation and optimize page load performance.

GraphQL

For data-fetching from multiple sources.

Contentful

CMS to store all data that is powering this website except for blogs, which are markdown files.

Netlify

For static site hosting, handling form submissions and having CI/CD integrated with Github.

Images

From unsplash when they are not my own.

..and other fun technologies. All code is hosted on Github as a private repository. Development is done on VS-Code. If you are interested, take a look at my preferred dev machine setup. Fueled by coffee and lo-fi beats. Current active version is v2.12.1.

</VR>