How To Manage Server State With React Query

Raphael Sani Enejo (SanRaph)
14 min readOct 24, 2022

Introduction

As an experienced React developer, there is no doubt you have faced state management issues and as a beginner, you will be facing Redux or Context API issues if you haven’t yet, this article is written so that you can decide whether you want Redux or React Query after comparing their benefits.

This article aims for a comprehensive presentation in a way that serves Novice and Ninja react developers alike, providing useful information to both worlds, we will be explaining in detail and simple terms what React Query is and then state why it is good for your projects so that you don’t change stack mid-way to avoid this experience.

What is the React Query?

React Query is a UX-driven library because it makes applications’ user experience feel faster, seamless, and more responsive.

React Query is comprised of a couple of hooks and other utilities dedicated to solving asynchronous server states, created by Tanner Linsley.

React Query is a small npm package you can use to fetch, cache, and update data in React and React Native applications, all without touching any “global state”.

Can we replace Redux With React Query?

Before React Query, Redux combined with useState and useEffect became the medium through which data is fetched in React, since the emergence of features like “Hooks” and “Context,” we have seen many state management libraries, and you can learn about hooks here if handling state management using hooks still appeals to you, however, you will see why it is not the best practice.

React Query is one of the state management libraries, it uses a declarative approach to real-time data polling using minimal code, it is backend agnostic with dedicated devtools.

Why should I use React Query?

Redux is good at synchronizing data across client applications because the data is local and can be controlled. Server Data on the other hand is different, it is not easy to detect changes in server data, we can fetch and keep server data in Redux store but it becomes difficult to know when that data is stale, building custom hooks to monitor changes can be time-consuming.

So, what do we do then?

You have your answer, React Query abstracts these worries, with a menu of features, you can customize your application the way you want, some of the features you can use are:

  • The refetchOnWindowFocus feature pre-fetch data depending on application tab activity. It pauses the activity when a user leaves a tab and is refocused when the user is back to the tab, set it to true; refetchOnWindowFocus : true
  • You can set the number of request retries on requests, in the case of random errors. This mechanism automatically does a re-call when a previous call fails, by default it is three retries before returning an error.
  • Your UI updates instantly when data is written to the server, and you can make changes appear with optimistic updates without latency.
  • Multiple queries in the same component can be done easily, and run automatically at the same time, if one query depends on data from another query, there is an enabled option, where one query will not execute until the query the component is dependent on has finished executing.
  • Data fetching by default returns a status and the data itself. When data is still loading the status will be loading, If the request fails it will actually try the request three times, and if still fails at that point, the returned status will be an error.

What we know about React state management

Anytime a user interacts with a button on a React application, the application responds and re-renders its user interface to synch the corresponding changes accordingly.

These changes are what we call the state of the application, state is used to save data that can be accessed by different components across the application, think of the state as registering the behavior of every component in a storage place. It saves the dynamics of components that are constantly responding to user interactions.

Types of States in React

There is the local state, local state is only useful in the component scope, it is the place where the app’s theme and sidebar state live.

There is the global state, all components have access to the data in the global state, and it’s the general storage place for components that need data from it.

Lastly, we have the server state, server state is persisted in a remote server and depends on both client and server states to send and receive its updates.

Client State and Server State

Client state is familiar to any react developer because it is used to persist changes in the application both local and global state comprised client state, client state is useful only in the local component and is temporary, it is accessed with synchronous APIs that have little or no latency.

The applications’ instance is what owned the client state, because of this, we can rely on the data to be up to date at all times in the application because we watch and control the data in this state.

With server state, we have to make asynchronous requests using asynchronous APIs, this data is on a server somewhere beyond our control. This is what makes the server state hard to manage, we are getting asynchronous data, and we don’t know when changes occur so we have to monitor for these changes to make sure we are keeping the data up to date.

Other Available Comparable Libraries

There are similar other tools that do what react query does with minimal differences, a mindbogglingly tool that exists with great similarities is the zeit/swr, this library is so similar that the only thing that differentiates it from React Query is the additional React Query devtool used for debugging errors. We also have MobX and Apollo Client, GraphQL and RTK-Query, Zustand, and Hookstate.

Why do you need React Query?

You need React Query for its declarative method of implementation, minimal code means fewer bugs.

Fetching Data the Old Way in React

In the code below, we are fetching data with the use of the fetchData function. If the Axios call with method URL, headers, and body parameters respond with data, we take the data that is coming from the calls’ variables such as the response, loading status, and error to save it in the state and use it to set them in their appropriate parts of the fetchData block, if you want to learn more about this method of fetching data in React, this material does a great job.

Notice that we are saving not just the response data, we are also saving the state and errors, this is what React Query avoids. The returned data, status, and error are all saved in the global state, what similarities do they have aside from the fact that they are a factor of a single endpoint?

import { useState, useEffect } from 'react';
import axios from 'axios';

axios.defaults.baseUrl = 'https://api.openweathermap.org/data/2.5/weather';

const fetchWithAxios = ({ url, method, body=null, headers=null }) => {

const [ response, setResponse ] = useState(null);
const [ loading, setLoading ] = useState(true);
const [ error, setError ] = useState('');

const fetchData = () => {
axios[method]( (url, JSON.parse(headers), JSON.parse(body)) )
.then( (res) => {
setResponse(res.data);
} )
.catch( (error) => {
setError(error);
} )
.finally( () => {
setLoading(false);
} );
};
useEffect( () => {
fetchData();
}, [method, url, body, headers] );

return { response, error, loading };
};

export default fetchWithAxios;

Fetching and updating data with React Query

In general, React Query is considered the missing database, based on React database.

However technically speaking the API makes fetching, caching synchronizing, and updating servers easier. Currently, there are no synchronous methods for fetching, storing, or updating synchronous data in React without using “global states” like Redux. Initially, it looks like a simple library but it’s bursting with complex tools, handling many server management problems that you might have within a program. We described in the subsequent sections, how you can query the data in a database, and retrieve and update user data in the server.

Example of Fetching Data with React-Query

Project Set up

Let’s create an application.

npx create-react-app weather-app

To install React Query, you can use NPM

npm install react-query axios

or Yarn, if it is your favorite tool

$ yarn add react-query

Now that we have React Query installed in our project, we can go ahead and wrap the App.js component with the QueryClientProvider component from React-query, this is what gives us access to all the hooks from react-query.

import { QueryClient, QueryClientProvider } from "react-query";
import { ReactQueryDevtools } from "react-query-devtools";
const queryClient = new QueryClient({});const App = () => {
return (
<QueryClientProvider client={queryClient}>
{/* The rest of your application */}
</QueryClientProvider>
);
};
export default App;

React Query has a great Devtool feature that can be used in applications to detect abnormal occurrences when our application starts to misbehave.

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

Devtool visualizes how React Query work underhood, it is a monitoring tool and a debugger.

import { QueryClient, QueryClientProvider } from "react-query";
import { ReactQueryDevtools } from "react-query/devtools";
const queryClient = new QueryClient({});const App = () => {
return (
<QueryClientProvider client={queryClient}>
{/* The rest of your application */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}

Now we are ready to see how to fetch data with React Query.

Fetching Server Data with the userQuery Hook

We define our query for a user with a Query Key and an asynchronous Query Function.

The Query Key is unique per query, React Query uses keys to manage auto caching under the hood, and if any part of the Query Key changes, the query will automatically fetch new data for that specific user and store it in the cache with cache invalidation and de-duping handled.

When the user ID (Query Key) is waiting to receive data, the isLoading will be true to indicate the query has no data yet, it is very useful to render a spinner while you are waiting to show data to the user.

If there is the same query somewhere with the same Query Key in another component, it’ll scan the cache to see if the data is already there, then simply return it, to avoid unnecessary queries to the server.

queryCache.invalidateQueries function is used to make React Query send a new request to refetch data.

queryCache is a utility instance that contains many functions that you can use to further manipulate queries, you read about it here <<Query Cache>>

useQuery works anywhere it is used without waiting for the component to mount, you retrieve data with status updates when the data is ready, there is no need to monitor and store isLoading in your state.

isError will be true if the asynchronous call returned an error, this error state gives more information about what went wrong. You can use this to render error messages to the screen.

React Query is configured to see cached data as stale data that needs constant updates.

Stale queries automatically query for new data when new instances of the query mount. For example, when you leave and come back to a browser window, new data is fetched.

Let’s see how it works.

import { useQuery } from 'react-query'

With that import you can go ahead and use the useQuery, it takes a key and an asynchronous function.

In the example below, the key is ‘weather’ and the function is fetchWeatherData.

function App() {
const { isLoading, isError, data, error } = useQuery('weather', fetchWeatherData)
}

fetchWeatherData is our asynchronous call to return an array with the weather data, this call can be made with Axios or the browser built-in fetch() function. You can see the useQuery hook result contains some states we can use, let’s discuss them in detail.

The string value ‘weather’ is the query key, it saves every change in a different key so that the first time we pulled data, it saved it, then, the second time we pull again another version of the same data, it gets saved using a variation of the key, in that way we can always go back to the first operation.

isLoading will be true when the query has no data yet, this is when to render spinner while the application is waiting for a response.

isError will be true if the asynchronous call returned an error, the error state will give more information about why the status is an error. This is useful if we need to render some error messages.

data returns the result of the asynchronous call, this is the data to render to users.

Our implementation to this point hasn’t used anything aside from the useQuery hook. We didn’t use anything like useEffect to mount components so that we can pull the data to a useState constant. React Query does all of that for us.

The network call can be made using any of the HTTP request functions. React Query is flexible with what you used in making asynchronous requests.

Modifying Server Data With the useMutation Hook

Queries are synonymous with HTTP operations using the Get method.

What happens when we need operations like Post, Update and Delete?

Whenever we need to perform post requests like Create, Update or Delete a resource on the server, the useMutation hook is the right hook for such operations.

Mutations, in contrast to queries, are imperative.

Queries are involuntary, this means that the moment there is a function to fetch data, React Query keeps fetching and caching data whenever there is a change.

Mutations are not like that, they are intentional, every time you need to post, update or delete, we must trigger the operation ourselves, queries run automatically, and we need to trigger a mutation method in order for the operation to be carried out.

Everything happens in the cache to make the user experience better, we can optimistically update the cache and then invalidate (make that particular data useless, remember the cache is a layer with different levels of sub-keys) it after successfully executing the mutation. This article explains catching without React Query.

useMutation provides to handles we can use to deal with every stage of the mutation lifecycle, with this, we can revert back to a previous state.

Anytime there is a request, the UI refresh automatically to sync the changes and we keep the data in sync by invalidating the cache and refetching the data to get a pool of additional new data.

UseMution is similar to useQuery, there difference lies in the number of arguments they receive. useMution takes one argument instead of two.

It receives a callback function to perform an asynchronous task that returns a promise.

Now imagine that we are already fetching and showing the weather data, and if you want to add a piece of new weather information from your vicinity to the server, you can post it.

To post data, you will need to declare a new queryCLient to have access to the cache. If you want to provide the same client through the entire application, QueryClientProvider is what to use.

Once you have a new queryClient, we can use the useMutation hook to create a new post.

function MyWeatherfn() {
// Access the client
const queryClient = useQueryClient();
// Queries
const query = useQuery('weather', getWeatherData);
// Mutations
const mutation = useMutation(postWeatherData, {
onSuccess: () => {
queryClient.invalidateQueries('weather');
},
});
return (
<div>
<ul>
{query.data.map((weatherInfo) => (
<li key={weatherInfo.id}> {weatherInfo.temperature} </li>
))}
</ul>
<button
onClick={() => {
mutation.mutate({
id: Date.now(),
state: 'California',
temperature: '76 degrees',
});
}}
>
Add new weather
</button>
</div>
);
}

There are some important things in the code above.

We declared our mutation in the same way we declared our queries.

To use it, we called the mutate function and passed it to our weather payload as parameters.

Once the asynchronous call is finished and ready, we can use the onSuccess option and invalidate the query, then refetch the data again by calling invalidateQueries.

Similar to the useQuery hook, the useMutation hook out of the box gives response status.

const { isLoading, isError, error, data, isSuccess } = useMutation(postWeatherData, {
onSuccess: () => {
queryClient.invalidateQueries('weather');
},
});

We can use isLoading to indicate that something is being posted to the server while we wait.

data is the response to the asynchronous call.

isError gives us information in case something wrong happened.

Prefetching and Optimistic Update

Prefetching allows us to fetch the data before it is even needed, the ideal case for this is when you can anticipate what the users need even before they need it.

Noticing a pattern in user behavior like page navigation lets you prefill data before they arrive on the page.

Custom hooks make it easy to expose a function to prefetch such data ahead.

We fetch data in advance if we can predict user behavior and save the data before a user navigates to the page.

export const usePrefetchUsers = () => {
const queryClient = useQueryClient();
const prefetchUsers = async () => {
await queryClient.prefetchQuery(
['users'],
fetchUsers,
);
};

return { prefetchUsers };
};

Optimistic Updates do update optimistically, it ensures updates do not fail but that is not just it, what really makes this feature amazing is that you can roll back your updates if anything went wrong.

For example, say you rate this article, the rating score will be saved in the cache waiting for the time there is a connection, the data remain local in the cache and can be reverted back since there is no transaction with the server yet.

Everything is sent the moment a connection is achieved, in this way, you can be optimistic that your update is sure to get through the network and won’t fail.

Infinite scroll and Pagination

We want to fetch all the weather data at once, but the API forced us to fetch page by page.

React Query has a built-in solution for pagination.

In the same way, we use the useQuery hook, we can use the useInfiniteQuery, which has state properties to handle data.

const {
data,
error,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
status,
} = useInfiniteQuery('weather', getRegionalWeather, {
getNextPageParam: (lastPage) =>
lastPage.page < lastPage.total_pages ? lastPage.page + 1 : undefined,
});

We built our small weather application using React Query to show what it can do.

In order to have real data in our application, we used the weather database API to fetch data from the server. We made a call using getRegionalData function.

The first difference between useQuery and useInfiniteQuery is the shape of the response data.

We returned response called data which contains data.pages, an array of all the pages.

data.pageParams has all the parameters used to fetch the pages.

In the code above, our getRegionalWeather expects a parameter with the next page’s number to fetch. The first time useInfiniteQuery executes it will fetch page = 1 then getNextPageParam calculates the next page during a second execution if there are any remaining pages to fetch.

We can use hasNextPage to run again useInfinitQuery and fetch the next page, isFetchingNextPage indicates that the call is actively fetching, and the status tells us if all has been well or an error occurred. That is all there is to get you start with React Query.

Additional Materials

Other useful tutorials that you can further look at to better understand React Query are:

React Query: It’s Time to Break up with your “Global State” by Tanners Linsley

React Query — A practical example by Martin Mato

Why we switched to React Query for Managing Server State by alto

React Query Tutorial Videos

Conclusion

With Redux, we had to build solutions to fix state management problems, with React Query, we have methods to call and configure as we desire.

React Query provides these features out of the box. We don’t need to worry about when to fetch new data or monitor requests to know when to retry a failed call, this improves user experience as well as developers’.

When you design your code with optimal performance and optimization in mind, you are not just saving yourself the trouble, you are helping create a change in the lives of the people that use your products.

Our job is to help you do good work as a react developer and we want to hear from you to know if we have succeeded in achieving just that with this article, go ahead and comment or chat with us if there is anything you need help with or wants to share, we are better together. Addios Amigo!

--

--

Raphael Sani Enejo (SanRaph)

A Software Engineer with a strong user focus. Extensive experience in developing robust code and building world-class applications and life-changing products.