Painless Data Fetching With react-query

Haaris Chaudhry Development Technologies, React, Tutorial Leave a Comment

Hey everyone, my name is Haaris Chaudhry, and I’m a developer at Keyhole Software. Let me tell you about react-query!

In this blog, I’m going to give a quick introduction to a library for React called react-query. React-query provides specialized hooks that allow you to fetch and update data, which significantly reduces the complexity of hydrating and refreshing your components.

Creating the Project and Setting Up a Mock Database

For this demo, you’ll need to have Node.js installed. You’ll also probably want to have some recent development experience with React.

We’ll start by navigating to a folder of your choice and opening the command line. Create a new React project in this folder by executing the following command.

npx create-react-app rq-demo

Let’s move into this directory and install Axios, a library that facilitates interacting with restful APIs.

cd rq-demo && yarn add axios

We’ll need a mock backend API for this demo, which we can easily set up using JSON Server. Let’s install JSON Server globally.

npm install -g json-server

JSON Server requires a JSON file. It watches and treats this file as a database.

Using a text editor of your choice, open rq-demo and create a file named db.json in the root directory.

Our app will list a few NFL quarterbacks and their teams, so let’s populate db.json with the following JSON.

{
 "quarterbacks": [
   {
     "id": 1,
     "firstName": "Patrick",
     "lastName": "Mahomes",
     "teamId": 1
   },
   {
     "id": 2,
     "firstName": "Tom",
     "lastName": "Brady",
     "teamId": 2
   },
   {
     "id": 3,
     "firstName": "Josh",
     "lastName": "Allen",
     "teamId": 3
   },
   {
     "id": 4,
     "firstName": "Derek",
     "lastName": "Carr",
     "teamId": 4
   },
   {
     "id": 5,
     "firstName": "Aaron",
     "lastName": "Rodgers",
     "teamId": 5
   },
   {
     "firstName": "Kyler",
     "lastName": "Murray",
     "teamId": 6,
     "id": 6
   }
 ],
 "teams": [
   {
     "id": 1,
     "name": "Kansas City Chiefs"
   },
   {
     "id": 2,
     "name": "Tampa Bay Buccaneers"
   },
   {
     "id": 3,
     "name": "Buffalo Bills"
   },
   {
     "id": 4,
     "name": "Las Vegas Raiders"
   },
   {
     "id": 5,
     "name": "Green Bay Packers"
   },
   {
     "name": "Arizona Cardinals",
     "id": 6
   }
 ]
}

Next, using the terminal in the root of rq-demo, we can run JSON Server by executing the following command.

json-server --watch db.json -p 5000 --host 127.0.0.1

This command tells json-server to use db.json as a database, use port 5000, and use 127.0.0.1 (localhost) as the host.

With that complete, we’ve finished step one of our demo. Congratulations!

Now, it’s time to move into the next phase.

Creating the Initial React Components

Now, with our mock backend up and running, we’re ready to focus on the frontend.

Delete all files in the src directory leaving only App.js and index.js.

Index.js

Replace the contents of “index.js with the following code.

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
 
ReactDOM.render(
 <React.StrictMode>
   <App />
 </React.StrictMode>,
 document.getElementById('root')
);

App.js

Replace the contents of App.js with the following code.

import "./styles.css"
import Quarterbacks from "./Quarterbacks";
import Teams from "./Teams";
 
function App() {
 return (
   <div className="app-container">
     <div className="app-child-container">
       <Quarterbacks />
     </div>
     <div className="app-child-container">
       <Teams />
     </div>
   </div>
 );
}
 
export default App;

These components are fairly simple; the meat of our logic will be executed in the Quarterbacks and Teams components.

Quarterbacks.jsx

Create a new file named Quarterbacks.jsx in the root directory and insert the following code.

import React from "react";
import { useDataApi } from "./useDataApi";
import DataDisplay from "./DataDisplay";
import axios from "axios";
import AddQuarterbackForm from "./AddQuarterbackForm";
 
function Quarterbacks() {
 const [{ isLoading, isError, data }] = useDataApi("http://localhost:5000/quarterbacks", null);
 
 const handleFormSubmit = async ({firstName, lastName, teamName}) => {
   const {data: team} = await axios.post("http://localhost:5000/teams", {
     name: teamName
   })
   await axios.post("http://localhost:5000/quarterbacks", {
     firstName,
     lastName,
     teamId: team.id
   })
 }
 
 return (
   <div>
     <div>Quarterbacks</div>
     <AddQuarterbackForm onSubmit={handleFormSubmit} />
     <DataDisplay loading={isLoading} error={isError} data={data}/>
   </div>
 );
}
 
export default Quarterbacks;

The Quarterbacks component utilizes a specialized custom hook to fetch data.

It also contains a form component, AddQuarterbackForm, and passes a form submit callback to AddQuarterbackForm.

Finally, the DataDisplay component is used to display the fetched data in a formatted JSON form. These components should be created in separate files in the root directory. Let’s go over how to add each one!

AddQarterbackForm.jsx

The AddQuarterbackForm component is straightforward. It takes the input for three fields (first name, last name, and team name) and submits the results to the passed callback.

Here’s what it looks like.

import React, { useState } from "react";
 
const initialState = {
 firstName: "",
 lastName: "",
 teamName: ""
};
 
const isAnyFieldEmptyString = obj => {
 for (let prop in obj) {
   if (obj[prop] === "") return true;
 }
 return false;
};
 
function AddQuarterbacksForm ({ onSubmit }) {
 const [state, setState] = useState(initialState);
 
 const handleChange = field => ({ target: { value } }) => {
   setState(s => ({
     ...s,
     [field]: value
   }));
 };
 
 const clearForm = () => {
   setState({ ...initialState });
 };
 
 const handleSubmit = (e) => {
   e.preventDefault();
   onSubmit(state);
   clearForm();
 };
 
 return (
   <form onSubmit={handleSubmit}>
     <div>
       <label className="form-label">
         Player First Name:
         <input onChange={handleChange("firstName")} value={state.firstName}/>
       </label>
     </div>
     <div>
       <label className="form-label">
         Player Last Name:
         <input onChange={handleChange("lastName")} value={state.lastName}/>
       </label>
     </div>
     <div>
       <label className="form-label">
         Team Name:
         <input onChange={handleChange("teamName")} value={state.teamName}/>
       </label>
     </div>
     <div>
       <button type="submit" disabled={isAnyFieldEmptyString(state)}>Submit</button>
     </div>
   </form>
 );
}
 
export default AddQuarterbacksForm;

DataDisplay.jsx

The DataDisplay component displays the data passed back from the useDataApi custom hook (discussed below).

This component is super simple. The only reason it exists is so we don’t have to duplicate the display logic across both the Quarterbacks and Teams components.

import React from "react";
 
function DataDisplay({ data, error, loading }) {
 return (
   <>
     {loading && (
       <div>
         Loading...
       </div>
     )}
     {error && (
       <div>
         Error!
       </div>
     )}
     {data && (
       <div>
         <pre>{JSON.stringify(data, null, 2)}</pre>
       </div>
     )}
   </>
 );
}
 
export default DataDisplay

useDataApi.js

useDataApi is a hook that will immediately fetch data from the provided URL endpoint argument once its associated component has loaded. This hook provides information about the status of the fetch request that can be displayed to the user.

import { useEffect, useReducer, useState } from "react";
import axios from "axios";
 
const dataFetchReducer = (state, action) => {
 switch (action.type) {
   case 'FETCH_INIT':
     return {
       ...state,
       isLoading: true,
       isError: false
     };
   case 'FETCH_SUCCESS':
     return {
       ...state,
       isLoading: false,
       isError: false,
       data: action.payload,
     };
   case 'FETCH_FAILURE':
     return {
       ...state,
       isLoading: false,
       isError: true,
     };
   default:
     throw new Error();
 }
};
 
export const useDataApi = (initialUrl, initialData) => {
 const [url, setUrl] = useState(initialUrl);
 
 const [state, dispatch] = useReducer(dataFetchReducer, {
   isLoading: false,
   isError: false,
   data: initialData,
 });
 
 useEffect(() => {
   let didCancel = false;
 
   const fetchData = async () => {
     dispatch({ type: 'FETCH_INIT' });
 
     try {
       const result = await axios.get(url);
 
       if (!didCancel) {
         dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
       }
     } catch (error) {
       if (!didCancel) {
         dispatch({ type: 'FETCH_FAILURE' });
       }
     }
   };
 
   fetchData();
 
   return () => {
     didCancel = true;
   };
 }, [url]);
 
 return [state, setUrl];
};

useDataApi is somewhat complex, but if you’ve been coding in React for some time, then you’ve probably found yourself coming up with a similar abstracted data fetcher.

This particular custom hook was taken from an article by Robin Wieruch. I use something similar for my own projects!

It’s perfectly suited for fetching data. However, if you need to re-fetch data or notify one component that another component has fetched pertinent data, you’ll need to add some functionality.

One way to do this is by using a global state store like redux to maintain fetched data and have components subscribe to state changes and update accordingly. This can get out of hand very quickly, especially for large applications with several sources of data.

That’s where react-query comes in. React-query can help us reduce data fetching complexity.

Before we get into that, however, let’s finish off the first part of this demo and look at some of the shortcomings of our data-fetching model.

We have two more items to add to the project: the Teams component and our styles, both of which will be placed in the root directory.

Teams.jsx

The Teams component is straightforward. It’s very similar to the Quarterbacks component except that there is no form or form-submit handler.

import React from "react";
import { useDataApi } from "./useDataApi";
import DataDisplay from "./DataDisplay";
 
function Teams() {
 const [{ isLoading, isError, data }] = useDataApi("http://localhost:5000/teams", null);
 return (
   <div>
     <div>Teams</div>
     <DataDisplay loading={isLoading} error={isError} data={data}/>
   </div>
 );
}
 
export default Teams;

styles.css

The styles are very simple as well. They just help us place our two main components, Teams and Quarterbacks, in a side-by-side orientation.

.app-container {
   display: flex;
}
 
.app-child-container {
   flex-grow: 1
}
 
.form-label {
   display: inline-grid;
}

The Finished Demo

Your project directory structure should now look like this:

Open a command-line terminal window at the root of your project, and start the React project.

yarn start

Once the React app is up and running, you should be greeted with something like this:

Nothing fancy here. We just need to see what makes react-query so useful.

Let’s start by using the form and adding a new quarterback (using a new team name). I’m going to add Russell Wilson as part of the Seattle Seahawks.

This will create a new quarterback entry and a new team entry in our mock database. Once you add your quarterback and team, you’ll see that the components don’t automatically update without a browser refresh.

The data that you added is there – just checkout db.json or send a query with your browser to either the /quarterbacks route or the /teams route.

Getting Help From react-query

We need a way to inform components that they should refetch their data. We could use the context API, Redux, Mobx, Zustand, or some local custom logic, but that would be tedious and time-consuming. Instead, let’s see how react-query can help us with this problem.

Let’s first install react-query.

yarn add react-query

Next, we’ll need to add react-query through a provider so that the useQuery hook can be made available.

We’ll do that in index.js.

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { QueryClient, QueryClientProvider } from "react-query";
 
const queryClient = new QueryClient()
 
ReactDOM.render(
 <React.StrictMode>
   <QueryClientProvider client={queryClient}>
     <App/>
   </QueryClientProvider>
 </React.StrictMode>,
 document.getElementById("root")
);

Now, we’ll utilize the useQuery hook to fetch our data in Quarterbacks.jsx.

import React from "react";
import DataDisplay from "./DataDisplay";
import axios from "axios";
import AddQuarterbackForm from "./AddQuarterbackForm";
import { useDataApi } from "./useDataApi";
import { useQuery } from "react-query";
 
function Quarterbacks() {
  const [{ isLoading, isError, data }] = useDataApi("http://localhost:5000/quarterbacks", null);
 const { isLoading, data, isError, refetch } = useQuery('quarterbacks', () =>
   axios.get("http://localhost:5000/quarterbacks").then(res =>
     res.data
   ));
 
 const handleFormSubmit = async ({firstName, lastName, teamName}) => {
   const team = await axios.post("http://localhost:5000/teams", {
     name: teamName
   });
   await axios.post("http://localhost:5000/quarterbacks", {
     firstName,
     lastName,
     teamId: team.id
   });
   refetch();
 }
 
 return (
   <div>
     <div>Quarterbacks</div>
     <AddQuarterbackForm onSubmit={handleFormSubmit} />
     <DataDisplay loading={isLoading} error={isError} data={data}/>
   </div>
 );
}
 
export default Quarterbacks;

You’ll notice that the useQuery hook looks very similar to our useDataApi hook. useQuery accepts a “key” for our data and a function that will obtain our data. Our key in this case is quarterbacks. We’ll see how keys are useful in a moment.

useQuery comes with a lot more functionality than our custom hook, one example being the refetch function that will allow us to refetch our data. We’ll now get an immediate update to our component when we add a new quarterback and team.

However, you’ll notice that upon adding a new quarterback and team, our Teams component will not update. We need to somehow inform Teams that a new team has been added to the database. In the next section, we’ll talk through how to use react-query mutations to do this.

Creating A Mutation

Alright, the first step in creating a mutation is to modify Teams.jsx so that it now uses react-query to fetch teams instead of useDataApi.

import React from "react";
import useDataApi from "./useDataApi";
import DataDisplay from "./DataDisplay";
import { useQuery } from "react-query";
import axios from "axios";
 
function Teams() {
 const [{ isLoading, isError, data }] = useDataApi("http://localhost:5000/teams", null);
 const { isLoading, data, isError } = useQuery('teams', () =>
   axios.get("http://localhost:5000/teams").then(res =>
     res.data
   ));
 return (
   <div>
     <div>Teams</div>
     <DataDisplay loading={isLoading} error={isError} data={data}/>
   </div>
 );
}
 
export default Teams;

Let’s now create a mutation at the root of the project in a file named useAddQuarterbackAndTeam.js.

import { useMutation, useQueryClient } from "react-query";
import axios from "axios";
 
export function useAddQuarterbackAndTeam () {
 const queryClient = useQueryClient();
 
 const apiCall = (formData) =>
   axios.post("http://localhost:5000/teams", {
     name: formData.teamName
   }).then(res => res.data);
 
 return useMutation(apiCall, {
     onSuccess: async (team, submitData) => {
       await axios.post("http://localhost:5000/quarterbacks", {
         firstName: submitData.firstName,
         lastName: submitData.lastName,
         teamId: team.id
       });
 
       queryClient.invalidateQueries("teams");
       queryClient.invalidateQueries("quarterbacks");
     }
   }
 );
}

useMutation takes a function and an option’s object. In our case, we’re sending a request to post a new team to the teams endpoint, which we’ve assigned to a variable named apiCall.

The option’s object has a “success” property where we specify what needs to be done when apiCall is successful. You can also define what needs to be done when there’s an error, or what needs to be done regardless of the result of apiCall.

We’ve told useMutation that, when apiCall is successful, it needs to send another request, this time to the quarterbacks endpoint, so that we can add the quarterback with the correct teamId.

Recall that we’ve used “quarterbacks” and “teams” for the keys in the useQuery calls in the Quarterbacks and Teams components, respectively.

Calling queryClient.invalidateQueries with a key will notify any queries currently using that key that their data is old and they must refetch when they become active. In our case, we have two queries using the “quarterbacks” and “teams” keys and they’re both active, so they will refetch once our mutation successfully completes.

We’ll now modify Quarterbacks.jsx so that it looks like this:

import React from "react";
import DataDisplay from "./DataDisplay";
import axios from "axios";
import AddQuarterbackForm from "./AddQuarterbackForm";
import { useQuery } from "react-query";
import { useAddQuarterbackAndTeam } from "./useAddQuarterbackAndTeam";
 
function Quarterbacks () {
 const { isLoading, data, isError } = useQuery("quarterbacks", () =>
   axios.get("http://localhost:5000/quarterbacks").then(res =>
     res.data
   ));
 
const handleFormSubmit = async ({firstName, lastName, teamName}) => {
    const {data: team} = await axios.post("http://localhost:5000/teams", {
      name: teamName
    })
    await axios.post("http://localhost:5000/quarterbacks", {
      firstName,
      lastName,
      teamId: team.id
    })
  }
 
 
 const mutation = useAddQuarterbackAndTeam()
 
 const handleFormSubmit = formData => {
   mutation.mutate(formData)
 };
 
 return (
   <div>
     <div>Quarterbacks</div>
     <AddQuarterbackForm onSubmit={handleFormSubmit}/>
     <DataDisplay loading={isLoading} error={isError} data={data}/>
   </div>
 );
}
 
export default Quarterbacks;

To activate a mutation, simply call the mutate property and pass in the data that it needs. Now, when we add a new quarterback and team, both of our components will automatically update.

You can use mutations to encapsulate API calls that will modify data and will require rerenders of any dependent components. If you feel that another call to the backend is unnecessary, you can try optimistic updates in order to reduce the number of calls that you’re making to your application server.

In Conclusion…

This short demo barely scratches the surface of the world of capabilities react-query has. I highly suggest that you read the docs to gain a better understanding of how this wonderful library can simplify how you handle data in your React application.

Check out some of our other React-related blogs, all written by Keyhole devs. There are some great ones out there!

That’s it for now, happy coding!

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments