Async data made simple with React Query

I recently had the chance to chat with Tanner Linsley about react-query. A library of custom hooks that solves async data fetching and caching within React apps. We added react-query to a simple blog post React app. The initial page of the app showed all of the posts from an API we previously setup. When a post was clicked on, we navigated to that individual post which eventually allowed us to edit it’s content.

Code

I put the code we worked on in the stream and what I use in this article on GitHub here 👇

https://github.com/twclark0/LWT-tanner-react-query

useQuery

useQuery is a custom hook. Which means that it is made up of many React hooks internally. One of many hooks it uses is useEffect, which makes sure that the component is mounted before sending a request. So when we implement useQuery, we don’t need to put it in any React hooks.

The first parameter to useQuery is a string and this is how the hook knows what to cache when data is returned. You want to make sure this is unique. Another optional way of creating this “cache key”, is to pass it an array of strings. react-query will combine them into one string.

As mentioned, you’ll want to make the cache key unique, however if you wanted to declare a separate useQuery hook somewhere else with the same cache key, it’ll still update the same. “Everything collides for good” - Tanner. Meaning react-query will combine the caches when data is loaded into it.

The second parameter is a function that does the actual fetch and this needs to return a promise. Also, if you are working with axios, you can attach a cancel method to the promise that gets returned. As necessary, react-query will use this cancel function to cancel requests to optimize for performance.

**Example 1 **

const { status, data: post, error, isFetching } = useQuery(
  ['post', activePostId],
  async () => {
    const postsData = await (
      await fetch(`${API_BASE_URL}/posts/${activePostId}`)
    ).json()

    return postsData
  }
)

**Example 2 **

const { status, data, error, isFetching } = useQuery('posts', async () => {
  const postsData = await (await fetch(`${API_BASE_URL}/posts`)).json()
  return postsData
})

As you can see we also get back some values that we are destructureing. We get back a status, data, error, and a isFetching to utalize within our app.

<h3>Posts {isFetching ? 'Updating...' : null}</h3>
<div>
  {status === 'loading' ? (
    <span>Loading...</span>
  ) : status === 'error' ? (
    <span>Error: {error.message}</span>
  ) : (
    // also status === 'success', but "else" logic works, too
    <ul>
      {[...data]
        .sort((a, b) => (a.title > b.title ? 1 : -1))
        .map((post) => (
          <div key={post.id}>
            <a href="#" onClick={() => setActivePostId(post.id)}>
              {post.title}
            </a>
          </div>
        ))}
    </ul>
  )}
</div>

What’s neat about isFetching, is every time we lose focus on our screen and then click back, our async fetch function will run again in the background. This will flip the isFetching bool from false to true and go check for new data. This is because react-query really wants to make sure that the data is always up to date. We don’t want any stale queries! We don’t get the hard loading state, just refreshing in the background. The best part is that you can disable this feature if you wanted. This is done by a config provider that can disable the fetching for every single query.

const { status, data, error, isFetching } = useQuery(
  'posts',
  async () => {
    const postsData = await (await fetch(`${API_BASE_URL}/posts`)).json()
    return postsData
  },
  {
    refetchOnWindowFocus: false,
  }
)

Questions? Everything is in the docs!

useMutation

This hook takes a function which will run when a mutation is needed. This provided function can be an async function that returns a required promise, (which is what an async function will do already). Notice the values that are destructured are the same as useQuery. These values can be easily added to our UX. We also get a function (called onSubmit below) returned to call when we want to mutate our data.

The real power of useMutation is the config object which is the second param to the hook. Notice we have, onSuccess, onMutate, onSettled, and onError in the example below. These are callback functions that will run according to the state of the mutation, whenever our mutation hook is called. Giving us more flexibility on what actually happens with our cache of data.

** Example 1 (All posts -> adding a new post)

const [onSubmit, { status, data, error }] = useMutation(
  async values => {
    await fetch(`${API_BASE_URL}/posts`, {
      method: 'POST',
      body: JSON.stringify(values),
      headers: {
        'Content-Type': 'application/json',
      },
    })
  },
  {
    onMutate: newPost => {
      const previousPosts = queryCache.getQueryData('posts')

      queryCache.setQueryData('posts', old => [...old, newPost])

      return () => queryCache.setQueryData('posts', previousPosts)
    },
    onError: (error, newPost, rollback) => {
      rollback()
    },
    onSettled: () => queryCache.refetchQueries('posts'),
  }
)

Notice that with onSettled, we use queryCache.refetchQueries('posts'). Which is how we access the cached data stored previously by our useQuery. So in our scenario above, we are reaching into our cache, looking for the cache key posts, and we are essentially telling it to refetch our posts useQuery when our useMutation has settled.

onSettled: () => queryCache.refetchQueries('posts')

Also, the useMutate callback function that lives on the config object, is called with the same arguments as the returned mutation function (see the async function that is passed as the first param to useMutation… it also becomes the destructured onSubmit). Now with those same variables, we can optimistically update the queryCache post list.

onMutate: newPost => {
  const previousPosts = queryCache.getQueryData('posts')
  queryCache.setQueryData('posts', old => [...old, newPost])
  return () => queryCache.setQueryData('posts', previousPosts)
}

When the onSubmit is called, we optimistically assume the updated post will work by updating the local query cache data with our data…Not waiting for the refetch called on onSettled to work first to update the cache. In the scenario that there is an error, the onError callback handles it. This function is given the error, the mutation item (in our case the newPost), and whatever is returned from the onMutate function. So if we do the onMutate right, we can snapshot the current state before the mutation, and pass that to the onError callback. Which will call that function if there is an error. Effectively, undoing the optimistic update, resetting it to the cache before the mutation was called.

onMutate: newPost => {
  const previousPosts = queryCache.getQueryData('posts')

  queryCache.setQueryData('posts', old => [...old, newPost])

  return () => queryCache.setQueryData('posts', previousPosts) // this return 👇
},
onError: (error, newPost, rollback /* gets here 👋*/) => {
  rollback()
}

Now in this scenario of mutating an individual post, the useMutation is pretty similar with just a couple differences.

** Example 2

const [onSubmit, { status, data, error }] = useMutation(
  async values => {
    const response = await fetch(`${API_BASE_URL}/posts/${values.id}`, {
      method: 'PATCH',
      body: JSON.stringify(values),
      headers: {
        'Content-Type': 'application/json',
      },
    })
  },
  {
    onMutate: newPost => {
      const previousPost = queryCache.getQueryData(['post', newPost.id])

      queryCache.setQueryData(['post', newPost.id], newPost)

      return () => queryCache.setQueryData[('post', newPost.id)], previousPost
    },
    onError: (error, newPost, rollback) => {
      rollback()
    },
    onSettled: (data, error, newPost) => {
      queryCache.refetchQueries('posts')
      queryCache.refetchQueries(['post', newPost.id])
    },
  }
)

For updating the individual post, notice that we are using the post cache key array with the current post id, as opposed to just the posts in the previous example. This is the same for pulling out post data from the /*...*/ queryCache.setQueryData[('post', newPost.id)] /**/. Then if there is an error, notice the values returned from onMutate. Again, this gets passed to the third argument of onError and is the current post from the cache.

Finally, onSettled needs to also refetch the individual post query as well as the all posts query. So in this scenario we need to access the third param which gives us the new mutated post.

onSettled: (data, error, newPost) => {
  queryCache.refetchQueries('posts')
  queryCache.refetchQueries(['post', newPost.id])
}

queryCache.getQueryData

One of the go to reasons for Redux is access to a global state. No one wants to prop drill components that are layers deep. Tanner said in our stream, useQuery is a subscription to the cache, but if you want to imperatively get something out of the cache, you just need to do queryCache.getQueryData and pass in the query cache key of the data you are trying to pull out. I.e queryCache.getQueryData('posts')


React Query Devtools

import { ReactQueryDevtools } from 'react-query-devtools'

//
;<ReactQueryDevtools />

By simply running this at the top of our tree, we will get the react query devtools. Once the app loads you will see a little icon in the bottom corner of your app. Inside of the devtools we can see how many instances of subscriptions we have at a time. It also shows the exact data for the query cache that we have… including the actual query object that react-query is using internally. As navigation happens throughout the app, the devtools will respond and show updated data. This is clearly seen when react-query does the window re-focus fetches.


Want more?

This barely scratches the surface on what you can do with react-query. There are other awesome hooks like, usePaginatedQuery, useInfiniteQuery, and ReactQueryConfigProvider. The documentation linked below has many sandbox examples and goes into SSR, suspense mode, and prefetching as well!


Additional Resources:

Share This:

I send out a newsletter... occasionally 🙂

If you like my content then you can sign up to be notified of upcoming stuff first. Most of my letters will be centered around frontend stuff, i.e JavaScript, CSS, React, testing stuff.. Though I'm also passionate about some soft skill stuff, interviews, SQL, and other backendy things.

No spam, I won't sell your info, and you can unsubscribe at any time.

Copyright © 2020 Tyler Clark