Introduction
React, by default, doesn’t ship with support for things like routing, data fetching, and complex state management.
Consequently, several third-party libraries and frameworks have been developed to meet these needs.
Libraries such as React-Router and Reach Router have been developed for routing while libraries and frameworks such as Redux, Mobx, and Recoil have been developed for complex state management.
React Query is a third party library that describes itself as:
The missing data-fetching library for React; since out of the box React does not provide a way to fetch and updated data from components
React Query, however, does a lot more than fetching and updating data. It is an out of the box state management library for asynchronous data akin to Apollo Client but unlike Apollo Client, it supports both REST and GraphQL.
In a nutshell, React Query is a set of custom hooks that makes fetching, caching, and updating asynchronous or server state in React easy.
Why React Query?
One of the challenges we face when building React applications is determining an effective pattern to (fetch and update) work with server state. React does not give us anything out of the box.
Consequently, developers create their own ways by fetching data (server state) inside a useEffect
hook, then copying the result into a component-based state (client state). This pattern works but it is not optimal.
Let’s demonstrate the downsides of this pattern by considering the code below:
import "./styles.css"; import React, {useEffect, useState} from "react"; import axios from 'axios'; export default function App() { const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); const [userData, setUserData] = useState(null); useEffect(() => { async function getUserData() { try { setIsLoading(true); const {data} = await axios.get(`https://jsonplaceholder.typicode.com/users/1`); setUserData(data); setIsLoading(false); } catch (error) { setIsLoading(false); setIsError(error); } } getUserData(); }, []); return ( <div> {isLoading && (<div> ...Loading </div>)} {isError && (<div>An error occured: {isError.message}</div>)} {userData && (<div>The username is : {userData.username}</div>)} </div> ) }
In our small contrived example above, we are fetching the user data from the https://jsonplaceholder.typicode.com/users/1
endpoint. Then we render a view base on the status (success, loading, or error) of the API call. This method has several problems such as:
- We have to make this call at the
App
component or any component high up in our component tree. This is to enable pass down data to other (nested) components that need them using prop drilling. Prop drilling in React is an anti-pattern and should be avoided by all means. Here are some strategies to help avoid prop drilling - We have to repeat this boilerplate code in every component we fetch data. The above example requires both the
useState
and theuseEffect
hook and we used three different client (local) states (isLoading, isError, and userData) to determine the status of the API call. All these would have to be rewritten in every component we need to fetch data. Although we can abstract the hooks logic of this boilerplate code into a custom reusable hook, and reuse it across our app, it still does not solve all of our problems - With this pattern, developers often fall into the pitfall of mixing the client state and the server state together. The resulting state object may contain some component-based (client) state e.g the sidebar status or the theme color with server state e.g the fetched user data. The final state object may look something like this:
const [state, setState] = { showSideBar: false, theme: "dark", currentUser: {}, users: [], posts: [] };
To avoid this trouble, some developers turn to global state management libraries like Mobx and Redux. While this may work, they add an extra layer of complexity to our application, and in some cases that can be overkill.
Also, while some traditional state management libraries are great at managing client state, they are not so efficient in handling server state.
Server state management has unique requirements for this because of the following:
- It’s persisted remotely and it is not in our control
- It’s accessible and changeable by other people
- It can become stale (out of date)
- It requires an asynchronous API for fetching and updating
Consequently, to efficiently manage server state we need:
- To store our server state in an in-memory cache
- A mechanism to know if the state has changed
- To periodically update the state in the background
- To reflect updates as quickly as possible
These are not features that we can easily code on our own. Fortunately, these are the problems React Query was created to solve.
Out of the box React Query gives us a set of hooks for fetching, caching, and updating async data (server state).
In the next section, we will elaborate on this by refactoring our boilerplate code above to use React Query.
Getting started
Installation
# NPM npm i react-query #Yarn yarn add react-query
To refactor our boilerplate code above to use React Query follow the steps below:
- Set configurations to connect our app to React Query’s cache using the
QueryClient
and theQueryClientProvider
like this:import "./styles.css"; import React from "react"; import { QueryClient, QueryClientProvider } from "react-query"; import User from "./Components/User"; // Create a client const queryClient = new QueryClient(); export default function App() { return ( // Provide the client to your App <QueryClientProvider client={queryClient}> <User /> </QueryClientProvider> ); }
- Fetch data from our component using React Query and a data fetching library like axios:
import React from "react"; import { useQuery } from "react-query"; import axios from "axios"; const User = () => { const fetchUser = async () => { const { data } = await axios.get( `https://jsonplaceholder.typicode.com/users/1` ); return data; }; const { isLoading, isSuccess, error, isError, data: userData } = useQuery("user",fetchUser); return ( <div> {isLoading && <article>...Loading user </article>} {isError && <article>{error.message}</article>} {isSuccess && ( <article> <p>Username: {userData.username}</p> </article> )} </div> ); }; export default User;
From the example above, we can see that by using the useQuery
hook from React Query we have removed complex useEffect
and useState
logic from our code. This is cleaner, maintainable, and DRY.
Also, React Query stores the server state in the cache we have configured above and our components are served from there, thus enhancing performance.
React Query keeps the server state updated by periodically making API calls to the endpoint in the background. This is to ensure that our component always gets the latest server state.
There are a lot more advantages that the React Query library brings. Our small example above gives a high-level introduction to React Query and we have barely scratched the surface of its features.
In the next section, we will focus on the new awesome features added to React Query version 3.
New features
Query data Selectors
With these features React Query brings some of the good parts of GraphQL to REST. The useQuery
and the useInfiniteQuery
hooks now have a select
option. This enables us to select or transform the desired parts of the query result.
We can now select only the desired part of the query result in our example above like this:
... const { isLoading, isSuccess, error, isError, data: username } = useQuery("user", fetchUser,{ select: (user) => user.username }); ...
We would then render the username
like this:
... {isSuccess && ( <article> <p>Username: {username}</p> </article> )} ...
The useQueries
hook
The useQueries
hook is used to fetch a variable number of queries and returns an array with all the query results.
The useQueries
hook takes a parameter which is an array containing different query option objects like this:
const results = useQueries([ { queryKey: ['user', 1], queryFn: fetchUser }, { queryKey: ['user', 2], queryFn: fetchUser }, { queryKey: ['user', 3], queryFn: fetchUser }, { queryKey: ['user', 4], queryFn: fetchUser }, ])
Retry/offline mutations
React Query mutations never had retry
but in React Query 3 you can pass a second argument to the useMutation
hook to configure retry
like this:
const mutation = useMutation(addUser, {retry: 3});
So if a mutation fails because the device is offline, the mutation is retried the set number of times (three times in the above case) when the device is reconnected.
However, by default, React Query will not retry a mutation if it fails.
Persist mutation
In React Query 3, a mutation can be persisted to storage using hydrate functions. This is useful if you want to pause the mutation because the device is offline and resume the mutation when the device is reconnected. You can get more on this here.
QueryObserver
The QueryObserver
function works with the new
operator. It is used to create or watch a query like this:
const observer = new QueryObserver(queryClient, { queryKey: 'videos' }) const unsubscribe = observer.subscribe(result => { // do something here. unsubscribe() })
The QueryObserver
can also be used to observe and switch between queries. It takes two parameters which are the queryClient
and an option object
. The options for the QueryObserver
are identical to those of the useQuery
hook.
InfiniteQueryObserver
This is similar to the QueryObserver
function. It also works with the new operator but its use case is different.
The InfiniteQueryObserver
hook enables us to observe and switch between infinite queries. It takes two parameters which are the queryClient
and an option object
as shown below:
const observer = new InfiniteQueryObserver(queryClient, { queryKey: 'videos', queryFn: fetchVideos, getNextPageParam: (lastPage, allPages) => lastPage.nextCursor, getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor, }) const unsubscribe = observer.subscribe(result => { // do something unsubscribe() })
The options for the InfiniteObserverQuery
is exactly the same as the options for the InfiniteQuery
hook.
QueriesObserver
The QueriesObserver
can be used to create or observe multiple queries like this:
const observer = new QueriesObserver(queryClient, [ { queryKey: ['users', 1], queryFn: fetchUsers }, { queryKey: ['users', 2], queryFn: fetchUsers }, ]) const unsubscribe = observer.subscribe(result => { // do something unsubscribe() })
It also works with the new
operator and it takes two parameters which are the queryClient
and an options object
.
Set default options for specific queries
The QueryClient.setQueryDefaults()
method enables us to set default options for specific queries like this:
queryClient.setQueryDefaults('users', { queryFn: fetchUsers } function GetUsers() { const { data } = useQuery('users') return data; }
Set default options for specific mutations
This method allows us to set default options for specific mutations like this:
queryClient.setMutationDefaults('addUser', { mutationFn: addUser }) function AddUser() { const { mutate } = useMutation('addUser') // do something... }
The useIsFetching
hook
The useIsFetching
hook is an optional hook that returns the number of queries your application is fetching in the background.
It now has a filter that can be used to return only the numbers of queries that are fetching, that match the filter. Consider the example below:
... const isFetching = useIsFetching() // returns the how many queries are fetching const isFetchingPosts = useIsFetching(['users']) // returns how many queries matching the users filter that are fetching. ...
Final thoughts
React Query is an awesome library that addresses the pains of managing asynchronous data when working with React. It makes working with server state a breeze.
React Query 3, as we have seen in this post, adds some awesome features to this great library. Also, the React Query core is now separated from React and it can be used standalone or with other frameworks.
The post What’s new in React Query 3 appeared first on LogRocket Blog.