Using React Query to Effectively Handle Server Errors and Improve User Experience

Previously, we discussed how to replace your redux code by integrating React Query into your codebase and delegating data fetching and server state management. But in the everyday world, there might be errors from the API that limits what React query can do other than catching these errors and funneling it to you so you can handle it as best fit.

At the end of this blog post, you will learn how to handle errors from the server in a more user-friendly way.

Prerequisites

This blog post assumes that the reader has the following:

  • Basic knowledge of JavaScript

  • Basic knowledge of ReactJS

Setup Environment

We will use create-react-app to set up our react environment. To do this, run the commands below on your CLI.

npx create-react-app my-app
cd my-app

Install Dependencies

Now let's install and set up all libraries we need (React Query and React Hot Toast).

React Query: React query is a React library for fetching, caching, and updating asynchronous data in React applications. To install it, run the command below on your terminal:

npm install react-query

React Hot Toast: This enables you to add beautiful notifications to your React app to give your potential users a better visual guide of when there is an error in the app. To install the React Hot Toast, run the command below on your terminal:

npm install react-hot-toast

with these packages, let’s begin the development process.

Let’s start coding!

In your App.js file, we need to wrap our app with the Providers of React Query so that it can be used throughout the app. We also need to add the Toaster component which will allow the toast to be used everywhere in the app.

To do this, copy the code snippet below and paste it into the App.js file.

import { QueryClient, QueryClientProvider } from 'react-query';
import toast, { Toaster } from 'react-hot-toast';
import Home from './Home'
import React from 'react';

// Create a client
export const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            retry: false,
        },
    },
});

function App() {
  return (
    // Provide the client to your App
    <QueryClientProvider client={queryClient}>
      <Home />
      <Toaster />
    </QueryClientProvider>
  )
}

From the above code snippets

// Create a client
export const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            retry: false,
        },
    },
});

With React query, we create a new queryClient which will be used to interact with the React Query Cache. The default option has a key retry: false, which is used to tell react query to throw an error immediately after it receives one. If you set retry to a number, react query will retry the query according to the set number and only raise the error when it has retried that many times and still errors.

import React from 'react'
import { toast } from 'react-hot-toast'
import { useQuery } from 'react-query'

export const Home = () => {


  const fetchData = async () => {
    const options = {
  method: 'GET',
  headers: {'Content-Type': 'application/json', Prefer: 'code=500, dynamic=true'},
};
    try {
      let request = await fetch('https://stoplight.io/mocks/pipeline/pipelinev2-projects/111233856/clients?type=USER&limit=20&offset=1',options)
      if (request.status >= 200 && request.status < 300) {
        return request.json()
      } else {
        throw new Error("Something went wrong")
      }
    } catch (error) {

      throw new Error( error)
    }
  }

  useQuery(['test'], fetchData, {
    onSuccess(data) {
     //deal with the data here
    },
    onError(err) {
      toast.error(err.message)
    },
  })


  return (
    <div>
      <div>I am the home you seek</div>
    </div>
  )
}

From the code snippet above

const fetchData = async () => {
    const options = {
  method: 'GET',
  headers: {'Content-Type': 'application/json', Prefer: 'code=500, dynamic=true'},
};
    try {
      let request = await fetch('https://stoplight.io/mocks/pipeline/pipelinev2-projects/111233856/clients?type=USER&limit=20&offset=1',options)
      if (request.status >= 200 && request.status < 300) {
        return request.json()
      } else {
        throw new Error("Something went wrong")
      }
    } catch (error) {
      throw new Error( error)
    }
  }

This code fetches data from an endpoint that is bound to fail( because the aim of this blog is to help the user manage server errors in the frontend). because of the shape of the API we have to check the request status to see if it is successful then return the result of the request else we throw errors.

By throwing this error, React query knows that the request fails and then passes this error to its error result which can be assessed below

const {error} = useQuery(['test'], fetchData)

or the error can be passed to its onError callback as below

useQuery(['test'], fetchData, {
    onError(err) {
      toast.error(err.message)
    },
  })

we can choose any of both ways to handle the errors.

If you choose to separate concerns and move your data fetching logic to another file say a hook and you would want to handle showing the toast on that hook using the onError callback as above.

otherwise, you can use a useEffect, listen to the error result and display the toast based on the result. this is represented below

const {error} = useQuery(['test'], fetchData)

useEffect(() => {
if(error){
 toast.error(error.message)
}
}, [error])

Both methods above will help you in notifying the user when there is an error in your API call.

finally, there is some sort of error like network errors which might need that the user retries the API call, thankfully, React query has a refetch method that can be used to retry the API call, taking a snippet from previous code snippets

export const Home = () => {

const {error, data, refetch, loading} = useQuery(['test'], fetchData)


  return (
    <div>
     {loading && <p>loading ...</p>}

     {error && <div>
      <p>Something went wrong</p>
     <button onClick={refetch}> retry </button>
     </div>}

     {data && <div>I am the data you seek</div>}
    </div>
  )
}

The code snippet above gives us an example of how errors can be caught and displayed and the API call retried. we can notice how network errors are handled on Twitter where you have the option to click a button and retry fetching the data. we can achieve the same effect using the code above.

Conclusion

In conclusion, it is important that we handle errors on our UI as errors are inevitable. react query gives us the tool to effectively catch out errors and present them to the user as best fit.