DEV Community

Cover image for Data Fetching from an API using React Query - useQuery Hook Explained in Plain English
Abeinemukama Vicent
Abeinemukama Vicent

Posted on

Data Fetching from an API using React Query - useQuery Hook Explained in Plain English

Often described as the missing data fetching library for react, react query makes fetching, caching, updating and synchronizing server state in react applications easier than ever imagined.
React query provides a custom hook, useQuery for all this without touching any global state.

Why React Query

If you ever used default options in react such as useEffect hook to fetch data from an api end point, you in one way or the other scratched your head trying to figure out how some of these aspects react query brings to the table could be achieved with less effort. In this article, we are going to diagonise everything you need to know to get confident using react query as your next data fetching library.
The article is specific to data fetching that is getting data from an api endpoint to efficiently understand the concept. Other capabilities of react query such as updating server data, deleting server data and posting data to the server will be discussed in the coming articles.

Prerequisites

Before you read this article, you should have fundamental knowledge of JavaScript such as arrow functions, array and object destructuring, asynchronous and synchronous behavior among others. If youre not confident enough with these concepts, you can reminisce your self here. Additionally, you need intermediate knowledge of react 16.8 or higher. I assume zero knowledge of data fetching in react so we are going to diagonise this concept from scratch.
However, if you ever used other data fetching libraries such as redux toolkit or used hooks provided by react itself, its a big plus reading this article.

What are some issues with data fetching in Effects?

If you write your data fetching code in the classic style, call fetch and set state inside useEffect, there are a few problems you will most likely encounter:

  • No content in initial HTML. If you fetch all data in effects, this likely means that you're not using server rendering, so the initial HTML is an empty shell, and the user doesn't see anything until all JS loads which can significantly slow down your application. If you do use server rendering or try to adopt it later, you'll notice that your effects don't run on the server so you're not able to render any useful content on the server. By the point you realize this, it might be late to change your app's architecture.

  • Race conditions. If you don't write a cleanup function that ignores stale responses, you'll likely introduce bugs when responses arrive in a different order.

  • No instant Back button. If the user navigates to another page and then clicks Back, by default, you don't have the data to show to them. This is because, by default, there is no cache, and the previous page component will have already unmounted. This means that when the user presses Back, they see a spinner instead of the place where they left off. This kind of user experience makes it easy to tell if something is written as a client-side single-page app, and not in a good way.

  • Slow navigations between screens. If you have parent and child components both doing fetching in effects, then the child component can't even start fetching until the parent component finishes fetching. These types of performance problems are very common in single-page apps and cause a lot more slowness than "excessive re-rendering" that people tend to focus on.

In this article, I will be using react without any template such as typescript but the upcoming aticles on this subject will be with a typescript template.
Phew! enough, lets get hands dirty by diagonising a code sample for further understanding how react query comes in handy to the rescue.

I am starting with creating a new react app using vite.

npx create-vite react-query-fetch
Enter fullscreen mode Exit fullscreen mode

In my case, I called the project react-query-fetch
Select react from the drop-down and then JavaScript.
Lets now install project dependencies with the following command:

npm install
Enter fullscreen mode Exit fullscreen mode

We can now run our project with the command:

npm run dev
Enter fullscreen mode Exit fullscreen mode

We need two extra dependencies in our project; react-query for data fetching and axios for making HTTP requests. Lets install them with the following command:

npm install react-query axios
Enter fullscreen mode Exit fullscreen mode

We need to wrap the entry point of our application with the QueryClientProvider to give our app access to the awesome hooks provided by react-query. To achieve this, lets replace the code in src/main.jsx with the following code:

import React from 'react'
import ReactDOM from 'react-dom/client'
import { QueryClient, QueryClientProvider } from 'react-query'
import App from './App'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <QueryClientProvider client={new QueryClient}>
    <App />
  </QueryClientProvider>
)
Enter fullscreen mode Exit fullscreen mode

We shall do our data fetching from an online API, jsonplaceholder, inside src/app.jsx.
Lets first clean up the default code inside src/app.jsx to leave it look this:

import './App.css'

function App() {
  // Data fetching will take place here..
  return (
    <div className="App">
      {/* JSX here.. */}
    </div>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

Lets start with a basic fetch from users endpoint and display their phone numbers in the browser.

import axios from 'axios'
import { useQuery } from 'react-query'
import './App.css'

function App() {
  const getUsers =  async () => {
    const {data} =  await axios.get('https://jsonplaceholder.typicode.com/users')
    return  data;
  }

  const { isLoading, isError, error, data } = useQuery('users', getUsers)
  if (isLoading) {
    return <div>Loading..</div>
  }
  if (isError) {
    return <div>Errror, {error.message}</div>
  }
  return (
    <div className="App">
      {data &&  data.map((user, index) => (
        <div  key={index}>{user.phone}</div>
      ))}
    </div>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

Explanation

  • We import the useQuery hook from react-query to eliminate need for useEffect hook from React. If you are familiar with data fetching in React, you know that the useEffect can be used to fetch data and reload the page. The difference between react-query and the useEffect is that react-query will initially return the previously fetched data and then re-fetch.
  • We import axios from axios library for use in making HTTP requests. In our case, we are making a get request.
  • 'users' is called query key. react-query manages caching for us using query keys. This argument is required and can be a plain string, a variable or an array. In our case, its a plain string.
  • The getUsers function is an asynchronous one and returns data fetched from the end point. This argument is also required if no default query function has been defined in the entry point of the application. Incase of an error, useQuery hook will take care of it and in the next section of this article we will detail how this hook handles possible errors that may occur during data fetching. For this reason we didn't wrap our function in try-catch block.

In mycase, I wrote this function in the same file with the useQuery hook but we are not locked to this approach, you can have the function in a different file or folder and import it like you would for other files. This comes in handy for bigger projects.
In this basic example, we only have error, isFetching, isLoading and data.
Our, useQuery hook takes more arguments but in our example, we are taking two arguments that is a unique key to differentiate what is being fetched from the API and an asynchronous function where the fetching is done. The unique key is users and the function is getUsers.

  • The isLoading displays while the async-function runs, and an error is thrown when there is one. In our case there is no error and here is what we have after fetching: The hook returns an object with many properties but in our basic usage example we are returning error, isLoading, data and isError.

Advanced Usage of useQuery hook

In this section, we are going to diagonise advanced concepts including but not limited to error handling, refetching on window focus, refetching on connect and background fetching while fetching data with useQuery hook provided by react-query.
As ealier highlighted, useQuery hook takes quite a number of options, most of them being optional and returns an object with quite a number of properties, most of which are also optional but pick their react-query defaults if not specified. Refer to this page from the official TanStack Query documentation for background reading about all the options taken as arguments and the object properties returned by this hook.
In this article, we diagonise the most commonly passed arguments and object properties with a practical use case.

Error Handling

Handling errors is an integral part of working with data from asynchronous api calls, especially data fetching. We have to face it: Not all requests will be successful, and not all Promises will be fulfilled. This indicates that, not thinking about how we are going to handle our errors might negatively affect user experience of our applications.
Important points to note concerning error handling in react-query:

  • React query needs a rejected promise to handle errors for us correctly. Fortunately, this is what we get while working with axios(which we are using in our case). If you're using the fetch API or some other library that doesn't give a rejected Promise when an error is encountered during data fetching, I will recommend some background reading from the official react-query docs on this concept.

  • By default, react-query will try to refetch 3 times before it returns an error. In this illustration, I intentionally altered the api endpoint to fetch from a non-existent url and you will observe that there are three retries before the error is returned

  • In the basic example above, we handled error situations by checking for the isError boolean flag, which is derived from the status enum provided by react query and in our basic usage, it worked for us, but has some limitations in some scenarios. To mention but afew; it doesnot handle background errors(errors arising from background fetches) very well and also, it introduces more boilerplate code if you have to do this for every component that wants to use a query. The question in mind right now is; how do we go about this in react-query?

Before, lets first take a look at what react encrouges us to do regarding this error handling.
To catch run time errors and display a fallback ui, react has a concept called error boundaries which involves wrapping our components in Error Boundaries at any granularity we want, so that the rest of the UI will be unaffected by that error. This is nice but doesn't catch asynchronous errors. This is because, these errors do not occur during rendering.

To use error boundaries in react-query, the library internally catches the error for you and re-throws it in the next render cycle so that the error boundary can pick it up.
Pretty genius, right! and all we have to do is to add useErrorBoundary flag to our query and set it to true.

const usersObject = useQuery('users', getUsers, {
    useErrorBoundary: true
  }) 
Enter fullscreen mode Exit fullscreen mode

Additional tip: As you can see, destructing the object returned by react query is optional and we can possibly access properties of this object for example data by writing usersObject.data and all others in a similar way as shown:

const usersObject = useQuery("users", getUsers, { useErrorBoundary: true })

if(usersObject.isLoading){
 return <div>Loading...</div>
}

if(usersObject.data){
 return (
    {data.map((user, index) => (
        <p>{user.name}</p>
    ))}
)
}
Enter fullscreen mode Exit fullscreen mode

You can as well possibly customize which errors should go towards an error boundary and which errors should be handled locally. This can be achieved by specifying a function to useErrorBoundary. Lets take an example of a common situation where all server errors, 5xx are handled by error boundaries and other errors are handled locally:

const usersObject = useQuery('users', fetchUsers, {
  useErrorBoundary: error => error.response?.status >= 500
})
Enter fullscreen mode Exit fullscreen mode

This concept also works for mutations using useMutation hook and is quite helpful while doing form submissions. We shall diagonise this concept in the coming articles.

We can as well show error notifications to the user to create a better user experience. Lets use an imperative api, react-hot-toast to understand this concept.
To install this library, run the following command:

npm install react-hot-toast
Enter fullscreen mode Exit fullscreen mode

react-query optionally takes another callback function: onError to help us customise error handing the more as illustrated bellow:

import toast from 'react-hot-toast'
// Custom error handling function
 const handleError = (error: unknown, query: Query) => {
 // Do anything with the error here e.g conditionally render modals with error.message as body to improve your app's user experience
    toast.error(`Something went wrong: ${err.message}`)
    console.log(`An error occured, ${err.message}`)
    return<p>Error..</p>
  }

  const { data, error, isError, isLoading } = useQuery('users', getUsers, {
    onError: handleError,
    useErrorBoundary: error => error.response?.status >= 500
  })
Enter fullscreen mode Exit fullscreen mode

The onError function takes two arguments that is the error and the query itself so the function has access to the query's details including the queryKey just in case need for such information arises inside the function body, it's available at your disposal.
This function is called if an error occurs while executing the query to fetch a list of users from the API. The error object is passed as an argument to the onError function, which can be used to handle the error in some way, such as logging it in the console or displaying an error message to the user.

This is nice, but every observer will get to know that our fetch failed. In some scenarios, we may need to only notify the user once that our underlying fetch failed. In this this case, we will utilise react-query's *global callbacks.
When we created a new QueryClient that we passed in the QueryClientProvider in the entry file of our app, we implicitly created a QueryCache.
Global callbacks need to be provided when create a QueryCache. The implicit creation of the QueryCache can be customized too.

// main.jsx
const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: error => toast.error(`Something went wrong ${error.message}`)
  })
})
Enter fullscreen mode Exit fullscreen mode

Phew! we nailed it, the user will only see an error toast once for each query and this is exactly what we intended to achieve.
This is the best place to track errors as it will only run once per API call and won't be overidden including defaultOptions.

Wrapping up this section, we have discussed the three ways to track and monitor errors in react query that is;

  • The error property returned in the object by useQuery hook
  • Using error boundaries provided by react by setting useErrorBoundary to true in the object passed as a parameter to useQuery hook to enable react-query re-throw the catched error in the next render cycle for the Error Boundary to pick it.
  • Using the onError callback on the query itself or on the global QueryCache.

Refetching on window focus and on reconnect

By default, react-query will refetch new data in the background if there is some when you focus on the window or reconnect internet. This default behavior can however be customized by specifying refetchOnWindowFocus and refetchOnReconnect options in the object passed as an argument.

const { isLoading, isError, error, data } = useQuery({ 
    queryKey: 'users', 
    queryFn: getUsers, 
    refetchOnWindowFocus: false, //Defaults to true
    refetchOnReconnect: false //Defaults to true
})
Enter fullscreen mode Exit fullscreen mode

These two options can as well be disabled globally in the entry point of the application.

import React from 'react'
import ReactDOM from 'react-dom/client'
import { QueryClient, QueryClientProvider } from 'react-query'
import App from './App'
import './index.css'
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: true, 
      refetchOnReconnect: true 
    }
  }
}) 
ReactDOM.createRoot(document.getElementById('root')).render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
)
Enter fullscreen mode Exit fullscreen mode

Additional tip: refetchOnWindowFocus should be disabled on all the queries involving file uploads because react-query will try to refetch in the background upon closing the file upload modal. This abstracts the onChange function of this input whose type is file which may possibly create unexpected behaviour. However, in some situations, you might need to refetchOnWindowFocus on particular query except for this scenario of file upload. In this case you can use react-query's focusManager and customise it to your needs or reference this issue on github for background reading. You can find github repo for this article here

Conclusion

In conclusion, React Query is a powerful tool for managing server state and is configurable to each observer instance of a query with knobs and options to fit every use-case. This makes it exceptionally effective.
Thanks for reading.
Leave a comment below.
Lets Connect:
Github LinkedIn twitter BuyMeCoffee

Top comments (0)