Introduction
React Query is a powerful tool that simplifies the data fetching, caching and synchronization with the server by providing a declarative and composable API of hooks. It was created by Tanner Linsley in 2020 and has gained a lot of popularity since then.
It uses a cache-first approach to optimize the user experience by reducing the number of network requests made by the application. React Query also provides built-in support for features like pagination, optimistic updates, and retrying failed requests.
While building neeto we started using React Query and we learned a few things about how to effectively use it. Below are some of the lessons we learned.
Major advantages of using React Query
-
Simplifies data fetching - It erases a lot of boilerplate and makes our code easier to read.
Here is a comparison of code structure with and without using
react-queryto fetch data from the server:Without React Query:
const [isLoading, setIsLoading] = useState(false); const [data, setData] = useState([]); const [error, setError] = useState(null); useEffect(() => { setIsLoading(true); (async () => { try { const data = await fetchPosts(); setData(data); } catch (error) { setError(error); } finally { setIsLoading(false); } })(); }, []);With React Query:
const { isInitialLoading, data, error } = useQuery(["posts"], fetchPosts);React Query provides a
useQueryhook to fetch data from the server. The entireuseEffectwas replaced with a single line with React Query. TheisInitialLoading,errorand the data state is handled out of the box. -
Provides tools to improve user experience and prevent unnecessary API calls.
React Query provides a powerful caching mechanism that can help prevent unnecessary API calls by storing the results of API requests in a cache. When a component requests data from the API using React Query, it first checks if the data is already present in the cache. If the data is present and hasn't expired, React Query returns the cached data instead of making a new API call.
Here's an example:
Suppose we have a component that displays a list of todos fetched from an API endpoint at
https://api.example.com/todos. We can use React Query to fetch the data and store it in the cache like this:import { useQuery } from "@tanstack/react-query"; const TodoList = () => { const { data, isInitialLoading, isError } = useQuery( ["todos"], () => axios.get("https://api.example.com/todos"), { staleTime: 10000, // data considered "fresh" for 10 seconds } ); if (isInitialLoading) { return <div>Loading...</div>; } if (isError) { return <div>Error fetching data</div>; } return ( <ul> {data.map(todo => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ); }; export default TodoList;In this example,
useQueryis used to fetch the data from the API endpoint. The first argument to useQuery is a unique key that identifies the query, and the second argument is a function that returns the data by making a request to the API endpoint.Now, suppose the user navigates away from the
TodoListcomponent and then comes back to it, if the data in the cache hasn't expired, React Query will return the cached data instead of making a new API call, which can help prevent unnecessary network requests.If the cache time has expired, React Query will not show the loading state because it will automatically trigger a new API call to fetch the data. During this time, React Query will return the stale data from the cache while it waits for the new data to arrive. This means that our component will continue to display the old data until the new data arrives, but it won't show a loading state because it's not waiting for the data to load.
Once the new data arrives, React Query will update the cache with the new data and then return the updated data to the component.
-
Request Retries - Ability to retry a request in case of errors.
React Query provides built-in support for request retries when API requests fail. This can be useful in situations where network connectivity is unreliable, or when the server is under heavy load and returns intermittent errors.
When we use useQuery or useMutation from React Query, we can provide an optional retry option that specifies the number of times to retry a failed request. Here's an example:
const { data, isInitialLoading, isError } = useQuery( ["todos"], () => axios.get("https://api.example.com/todos"), { retry: 4, // retry up to 4 times } );In this example, we're using
useQueryto fetch data from an API endpoint. We've set the retry option to 4, which means that React Query will retry the API request up to 4 times if it fails. If the API request still fails after 4 retries, React Query will return an error to our component.We can also customize the behavior of request retries by providing a function to the
retryDelayoption. This function should take the current retry count as an argument and return the number of milliseconds to wait before retrying the request. Here's an example:const { data, isInitialLoading, isError } = useQuery( ["todos"], () => axios.get("https://api.example.com/todos").then(res => res.json()), { retry: 3, // retry up to 3 times retryDelay: retryCount => Math.min(retryCount * 1000, 30000), // wait 30 seconds between retries } ); -
Window Focus Refetching - Refetching based on application tab activity.
Window Focus Refetching is a feature of React Query that allows us to automatically refetch our data when the user returns to our application's tab in their browser. This can be useful if we have data that changes frequently or if we want to ensure that our data is always up to date.
React Query will automatically refetch our data when the user returns to our application's tab in their browser. Please note that
refetchOnWindowFocusis true by default.To disable it, we can do it per query or globally in the query client.
Disabling per-query:
useQuery(["todos"], fetchTodos, { refetchOnWindowFocus: false });Disabling globally:
const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, // default: true }, }, });
How to integrate React Query
-
We can install React Query using
npmoryarn. Open the terminal and navigate to the project directory. Then, run one of the following commands:Using npm:
npm install @tanstack/react-queryUsing yarn:
yarn add @tanstack/react-query -
To integrate react query, first we need to wrap it a
QueryClientProvider. In theApp.jsxfile (or whichever is the topmost parent),import queryClient from "utils/queryClient"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { QueryClientProvider } from "@tanstack/react-query/reactjs"; const App = () => ( <QueryClientProvider client={queryClient}> <Main /> <ReactQueryDevtools initialIsOpen={false} /> </QueryClientProvider> ); export default App;ReactQueryDevtoolsis a great tool for debugging and optimizing our React Query cache. WithReactQueryDevtools, we can view the current state of our queries, including the query status, data, and any errors that may have occurred. We can also view the cache entries and manually trigger queries or invalidate cache entries.import { QueryClient } from "@tanstack/react-query"; const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 100_000, }, }, }); export default queryClient;The
queryClientholds the query cache. The query client provider is a React context provider, which allows us to access the client (and thus the cache) without passing it as a prop explicitly. Every time we calluseQueryClient(), we get access to this client. TheuseQuery&useMutationhooks use the query client internally.staleTimeis the duration until a query transitions from fresh to stale. As long as the query is fresh, data will always be read from the cache only.cacheTime: The duration until inactive queries will be removed from the cache. This defaults to 5 minutes.Most of the time, if we want to change one of these settings, it's the
staleTimethat needs adjusting. We have rarely ever needed to tamper with thecacheTime. -
React query provides us mainly two hooks to fetch and mutate data:
useQueryanduseMutation.useQuery:const { data, isInitialLoading, isError } = useQuery(["todos"], () => axios.get("https://api.example.com/todos") );In this example, we're using useQuery to fetch data from an API endpoint at
https://api.example.com/todos. The first argument touseQueryis a unique key that identifies this query. The second argument is a function that returns a promise that resolves to the data we want to fetch.The
useQueryhook returns an object with three properties:data,isInitialLoading, andisError. In addition to these,useQueryalso returns various other properties likeisSuccess,isFetchingetc and callbacks likeonError,onSettled,onSuccessetc. The data property contains the data that was fetched, whileisInitialLoadingandisErrorare booleans that indicate whether the data is currently being fetched or if an error occurred while fetching it.useMutation:import { useQueryClient, useMutation } from "@tanstack/react-query"; const queryClient = useQueryClient(); const { mutate, isLoading } = useMutation( data => axios.post("https://api.example.com/todos", { body: JSON.stringify(data), }), { onSuccess: () => { queryClient.invalidateQueries("todos"); }, } );In this example, we're using
useMutationto send data to an API endpoint athttps://api.example.com/todosusing aPOSTrequest. The first argument touseMutationis a function that takes the data we want to send and returns a promise that resolves to the response from the API.The second argument to
useMutationis an options object that allows us to specify a callbackonSuccessfunction to be called when the mutation succeeds. In addition toonSuccess,useMutationprovides us various other callbacks such asonError,onMutate,onSettledetc. In this case, we're using theonSuccessoption to invalidate thetodosquery in thequeryClient. This will cause the query to be refetched the next time it's requested, so the updated data will be displayed.
The standards we follow at BigBinary
-
The
QueryClientProvideris wrapped in theApp.jsxfile. -
The
queryClientis placed inutils/queryClient.jsfile and it can set the default values for stale time, cache time etc. -
We store the query keys in
constants/query.jsfile.export const DEFAULT_STALE_TIME = 3_600_000; export const QUERY_KEYS = { WEB_PUSH_NOTIFICATIONS: "web-push-notifications", WIDGET_SETTINGS: "widget-settings", WIDGET_INSTALLATION_SCRIPT: "widget-installation-script", }; -
To fetch and mutate data, we can create API hooks in
hooks/reactQueryfolder and based on the structure of the app, we create folders or files inside this folder. For example, we can have ahooks/reactQuery/settingsfolder to separate settings-related hooks from other API hooks. -
React query hook files are named in the format
use*Apiwhere*is the specific set ofapiswe are trying to use.For example inside
hooks/reactQuery/useIntegrationsApi.js:import { prop } from "ramda"; import { useMutation, useQuery } from "react-query"; import { DEFAULT_STALE_TIME, QUERY_KEYS } from "src/constants/query"; import integrationsApi from "apis/integrations"; import queryClient from "utils/queryClient"; const { THIRD_PARTY_APPS } = QUERY_KEYS; const onMutation = () => queryClient.invalidateQueries(THIRD_PARTY_APPS); export const useUpdateIntegration = () => useMutation(({ id, options }) => integrationsApi.update(id, options), { onSuccess: onMutation, }); export const useFetchIntegrations = () => useQuery(THIRD_PARTY_APPS, integrationsApi.fetchThirdPartyApps, { staleTime: DEFAULT_STALE_TIME, select: prop("thirdPartyApps"), }); -
We import the api hooks from the
use*Apihook we have created and use it in the file we would like to fetch or mutate queries.For example:
import { useFetchIntegrations, useUpdateIntegration, } from "hooks/reactQuery/useIntegrationsApi"; const Integrations = () => { const { data: integrationApps, isLoading } = useFetchIntegrations(); const { mutate: updateIntegration, isLoading: isInstalling } = useUpdateIntegration(); return ( <div> <DataTable data={data} /> </div> ); }; export default Integrations;
At BigBinary, we are using React Query v3. Since the update from v3 to v4, there has been some breaking changes. For more information, refer: Documentation