How to manage server state with React Query
React Query is a library that simplifies the process of managing data fetching and caching in React applications. It provides a set of tools and utilities for fetching and updating data from APIs and other data sources and manages the state of data fetching and caching automatically. The library provides a comprehensive set of hooks and utilities that make it easier to work with data in React components.
In this post, I am going to talk about the key features of React Query. My purpose here is to provide you with a starting point so that you can start working with React Query as soon as possible.
If you have some experience developing React apps, you might have worked with libraries such as Redux for managing client states. In contrast, React Query is a library to manage server state. Therefore, before we talk about React Query, let's be familiar with the difference between client state and server state
Client state vs server state
let’s go through the difference between the client state and the server state. This is helpful in understanding what React query does and why there is such a need for another library to manage the server state.
Client State | Server State | |
Location | Stored on the client (in the browser or device) | Stored on a server or external data source |
Accessibility | Only accessible by the client that stored it | Accessible by any client that has permission to access it |
Data management | Managed by the client (e.g. using state management libraries like Redux) | Managed by the server (e.g. using a database) |
Persistence | May or may not persist between sessions | Typically persists between sessions |
Network requests | May require network requests to fetch or update data | May require network requests to access or update data |
Security | Maybe more secure, as it can be protected by authentication and encryption | Maybe more secure, as it can be protected by authentication and encryption |
Performance | Maybe less scalable than the server state, as it can be limited by the capacity of the client device | Can be more scalable than the client state, as it can be managed by a dedicated server or data source with high capacity |
Scalability | Maybe less secure, as it is accessible to the client and may be subject to tampering or interception | Can be faster to access and update than the server state, as it does not require network requests |
Examples | Component state, Redux state, browser cookies | Database records, API responses, server session data |
client state vs server state
Traditionally, React developers have used Redux or React Context API for client state management. When it comes to interacting with API data, they often use a combination of the useState
hook and the useEffect
hook to fetch API data and populate it in the client state.
React Query is a library that can replace the use of useState
and useEffect
hooks for fetching data from a remote data source, as it provides a set of hooks and utilities for managing and caching API data. It simplifies the process of fetching data from an API, handling the cache, and managing the loading and error states of API requests.
Four basic concepts in React Query
There are four basic concepts you must understand to work with React Query.
Queries:
Requests for data from a remote data source such as an API endpoint or database
Managed by
useQuery
hook
Mutations
requests to add new data or modify existing data on the server
Managed by
useMutation
hook
Query caching: Query caching is a built-in feature of React Query that stores query results in memory
Query invalidation: the process of marking a query as invalid or stale.
Data fetching with useQuery hook
On the official documentation, it says “A query is a declarative dependency on an asynchronous source of data that is tied to a unique key”
Now, what is that word salad !!!!
Here, the declarative dependency refers to the query you declare in the code ( using the useQuery hook ). This query is a request to the server to fetch data, asynchronously, from an API datapoint or database.
Now, let's see how we are going to do this with the useQuery
hook
useQuery hook takes two required options( properties ):
unique key
a function that returns a promise
Syntax:
const query = useQuery( { queryKey: [ ‘key’ ], queryFn: callback })
The querykey
is the unique key. In the latest stable version of React Query, you need to use array notation, to specify this key.
The queryFn
is a callback function that is executed by React Query when the useQuery hook is called. This function performs a specific task (fetching data) at a certain point in the execution of the useQuery
hook.
Other than these two options, you can also have optional properties. I will cover some of the important ones in the following sections.
Example:
const getProducts = () => fetch( 'https://jsonplaceholder.typicode.com/users')
.then( res => res.json() )
const query = useQuery( { queryKey: [‘users’], queryFn: getTodos })
What does it returns
The useQuery
hook returns an object. This object contains information about the state of the query. Some of the important properties and methods are:
properties
data
: The data returned from the query if it has been successfully fetched.error
: Any error that occurred during the query, if there was one.isLoading
: A boolean value indicating whether the query is currently loading or not.isError
: A boolean value indicating whether the query resulted in an error or not.
Methods:
refetch
: A function that allows you to manually trigger a refetch of the query data.remove
: A function that allows you to remove a specific query from the cache.
Therefore, you can apply JavaScri[pt destructuring assignment to access values of these properties
const { isLoading, isError, data, error } = useQuery( { queryKey: [‘todos’], queryFn: getTodos });
You can find a complete reference of useQuery in the official documentation.
useQuery and refetching API data
By default, useQuery fetches data automatically from the API when the component is mounted for the first time. However, subsequent updates to the data are not fetched automatically. In other words, useQuery will not refetch data after an update in the API endpoint or server.
By default, useQuery
options such as refetchOnMount, refetchOnReconnect, and refetchOnWindowFocus
is set to true.
Take a look at the following table.
useQuery option | what it does | default value |
refetchOnWindowFocus | the query will refetch on window focus if the data is stale. | true |
refetchOnMount | refetch on mount if the data is stale | true |
refetchOnReconnect | refetch on reconnect if the data is stale | true |
Assume that you keep your browser idle in the background. Someone make a request to update the API data/server. The data is updated. But, your UI is not updated because your browser is in the background. There is no trigger for useQuery to refetch the updated data from the API endpoint/server. In this situation, you can use refetchInterval
option. You can run useQuery to refetch data at a frequency specified in milliseconds. This will enable you to refetch data even when the browser window is idle.
Create, update, and delete data with useMutation hook
Now, we will take a look at useMutation
hook. You can use this hook to create, update, or delete data from the API endpoint or server.
The useMutation
hook requires at least one option, and all other options are optional.
syntax:
const mutation = useMutation({ mutationFn: mutationFunction })
Example:
const AddUser = useMutation({
mutationFn: ( user ) => {
return fetch('https://jsonplaceholder.typicode.com/users',
{
method:'post',
headers: {
"Content-Type": "application/json",
},
body:JSON.stringify( user )
}).then( res => res.json() )
})
What does it returns
useMutation hook returns an object. This object contains several properties and methods that allow you to interact with the mutation and handle its results. Some of them are:
Properties
isLoading
: A boolean indicating whether the mutation is currently in progress.`isLoading: A boolean indicating whether the mutation is currently in progress.isSuccess
: A boolean indicating whether the mutation has been completed successfully.isError
: A boolean indicating whether an error occurred during the mutation.data
: The data returned by the mutation, if any.
Methods
mutate
: A function that you can call to execute the mutation. You can pass in an object with any necessary variables as the first argument.reset
: A function that resets the mutation to its initial state.onSuccess
: A function that allows you to define a callback to be called when the mutation completes successfully.onError
: A function that allows you to define a callback to be called when an error occurs during the mutation.
A complete reference to this hook can be found in the official documentation
Query Caching in React Query
Let’s assume that you use the useQuery hook to fetch data from some users from a remote server. It might take a while to receive that data. To speed things up, React Query will save this user data on the cache so that it can quickly be retrieved in subsequent requests. This helps reduce the time it takes to load the data.
As I mentioned before, the useQuery
hook uses a unique key. The data returned from the server is cached under this key. For example, if you use [‘users’]
it as the key, this key will be used to extract data from the cache for subsequent requests for the same API endpoint.
By default, useQuery will mark this cache data as stale. There are two options you need to be aware of when it comes to React query caching. That is staleTime
and cacheTime
.
staleTime
: determine the time it takes query results to be considered stale. Time is specified in milliseconds.
Ex: staleTime: 5000
//data can remain stale for up to 5 seconds
This means data will be stale after 5 seconds
On the other hand, cacheTime
option specifies how long the query results remain in the cache.
Ex: cacheTime: 60000
// data will be in the cache for 1 minute
This means the query result will be in the cache for 1 minute and after that period data will be garbage collected.
const query = useQuery({
queryKey: [‘users’],
queryFn: getUsers,
staleTime: 5000,
cacheTime: 60000
})
Query Invalidation in React Query
As previously discussed, the data stored in the cache may become stale based on the staleTime option. However, situations may arise where it is necessary to override the specified staleTime
and mark the data as invalid or stale. For instance, when sending a post request to the API, it is necessary to manually mark the data in the cache as invalid since the data at the API endpoint is the latest data. Consequently, the data in the cache is considered stale immediately after the POST request.
To address this, the React Query QueryClient object provides the invalidateQueries method. This method can be used to mark all or specific queries as stale. you can also mark a specific query as stale using its unique key.
import { useQueryClient } from '@tanstack/react-query'
//creating queryClient object using useQueryClient hook
const queryClient = useQueryClient()
// Invalidate every query in the cache
queryClient.invalidateQueries()
// Invalidate every query with a key that starts with 'users'
queryClient.invalidateQueries({ queryKey: ['users'] })
Wrapping up
In this post, I covered What React query is and why there is a need for it. React Query is a powerful library to manage server state. It utilizes React hooks such as useQuery to fetch data and useMutation to manipulate remote data from an API endpoint or server. React Query also has a smart caching mechanism to manage its in-memory cache. This makes sure that the cache synchronizes with the remote data source. Using React Query, you cut down your code and optimize the performance of React applications.
Resources
Note: In my demo project, I utilize a mock API I created with mockapi.io. However, I highly recommend creating your own API endpoint with mockapi.io. This is because it can be easier for you to do your experiment while you are learning.