April 10, 2023
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.
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-query
to 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 useQuery
hook to fetch data from the server. The
entire useEffect
was replaced with a single line with React Query. The
isInitialLoading
, error
and the data state is handled out of the box.
Provides tools to improve user experience and prevents 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, useQuery
is 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 TodoList
component 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 useQuery
to 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 retryDelay
option. 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 refetchOnWindowFocus
is 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
},
},
});
We can install React Query using npm
or yarn
. Open the terminal and
navigate to the project directory. Then, run one of the following commands:
Using npm:
npm install @tanstack/react-query
Using yarn:
yarn add @tanstack/react-query
To integrate react query, first we need to wrap it a QueryClientProvider
.
In the App.jsx
file (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;
ReactQueryDevtools
is a great tool for debugging and optimizing our React
Query cache. With ReactQueryDevtools
, 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 queryClient
holds 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 call
useQueryClient()
, we get access to this client. The useQuery
&
useMutation
hooks use the query client internally.
staleTime
is 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
staleTime
that needs adjusting. We have rarely ever needed to tamper with
the cacheTime
.
React query provides us mainly two hooks to fetch and mutate data:
useQuery
and useMutation
.
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 to useQuery
is 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 useQuery
hook returns an object with three properties: data
,
isInitialLoading
, and isError
. In addition to these, useQuery
also
returns various other properties like isSuccess
, isFetching
etc and
callbacks like onError
,onSettled
,onSuccess
etc. The data property
contains the data that was fetched, while isInitialLoading
and isError
are 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 useMutation
to send data to an API endpoint
at https://api.example.com/todos
using a POST
request. The first
argument to useMutation
is 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 useMutation
is an options object that allows us to
specify a callback onSuccess
function to be called when the mutation
succeeds. In addition to onSuccess
, useMutation
provides us various
other callbacks such as onError
, onMutate
,onSettled
etc. In this case,
we're using the onSuccess
option to invalidate the todos
query in the
queryClient
. This will cause the query to be refetched the next time it's
requested, so the updated data will be displayed.
The QueryClientProvider
is wrapped in the App.jsx
file.
The queryClient
is placed in utils/queryClient.js
file and it can set the
default values for stale time, cache time etc.
We store the query keys in constants/query.js
file.
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/reactQuery
folder and based on the structure of the app, we create folders or files
inside this folder. For example, we can have a hooks/reactQuery/settings
folder to separate settings-related hooks from other API hooks.
React query hook files are named in the format use*Api
where *
is the
specific set of apis
we 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*Api
hook 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
If this blog was helpful, check out our full blog archive.