This describes the basics on how to use the React Query Library
- Install the library
npm install react-query
- Create query client:
- manages queries and cache
import { QueryClient, QueryClientProvider } from "react-query";
const queryClient = new QueryClient();
function App() {
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>;
}
export default App;
- Apply QueryProvider:
- provides cache and client config to children
- takes query client as the value
- Run
useQuery
- hook to query the server
import { useQuery } from "react-query";
async function fetchPosts() {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts?_limit=10&_page=0"
);
return response.json();
}
const { data } = useQuery("posts", fetchPosts);
return (
<ul>
{data?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
More on useQuery
- isFetching
- the async query function hasn't yet resolved
- isLoading
- no cached data, plus
isFetching
- no cached data, plus
- Shows queries (by key)
- status of queries
- last updated timestamp
- Data explorer
- Query explorer
- To add the
react query dev tools
:
// App component
import { ReactQueryDevtools } from "react-query/devtools";
return (
<>
...
<ReactQueryDevtools />
</>
);
More on React Query Dev Tools
- Data refetch only triggers for stale data
- component remounts, window refocus
staleTime
translates to "max age"- how to tolerate data potentially being out of date
- You can add the
staleTime
in theoptions
object ofuseQuery
const { data, error, isError, isLoading } = useQuery("posts", fetchPosts, {
staleTime: 2000,
});
- The default
staleTime
is0
to always have updated data
staleTime
is for re-fetchingcache
is for data that might be re-used later- query goes into "cold storage" if there's no active
useQuery
- cache data expires after
cacheTime
(default: five minutes)- how long it's been since the last active
useQuery
- how long it's been since the last active
- after the cache expires, the data is garbage collected
- cache is backup data to display while fetching
- query goes into "cold storage" if there's no active
- Data for queries with known keys only re-fetched upon trigger
- Example triggers:
- component remount
- window refocus
- running refetch function
- automated refetch
- query invalidation after a mutation
- To fix this, it's possible to pass an array for the query, not only a string
- Treat the query as a dependency array
['comments', post.id]
- adds data to cache
- automatically state (configurable)
- shows while re-fetching
- as long as cache hasn't expired
- pre-fetching can be used for any anticipated data needs
- not just pagination
import { useEffect, useState } from "react";
import { useQuery, useQueryClient } from "react-query";
export function Posts() {
const [currentPage, setCurrentPage] = useState(1);
// the query client is necessary to pre-fetch data
const queryClient = useQueryClient();
useEffect(() => {
// condition to pre-fetch data
if (currentPage < maxPostPage) {
const nextPage = currentPage + 1;
queryClient.prefetchQuery(["posts", nextPage], () =>
fetchPosts(nextPage)
);
}
}, [currentPage, queryClient]);
const { data } = useQuery(
["posts", currentPage],
() => fetchPosts(currentPage),
{
staleTime: 2000,
// keep data in cache
keepPreviousData: true,
}
);
}
More on Pre-fetching
- Making a network call that changes data on the server
- Optimistic updates (assume change will happen)
- Update react query cache with data returned from the server
- Trigger re-fetch of relevant data (invalidation)
useMutation
- similar to
useQuery
- returns a
mutate
function - doesn't need a query key
isLoading
but noisFetching
- by default, no retries (configurable)
- similar to
More on Mutations
useInfiniteQuery
- Requires different API format than pagination
- Tracks next query
- next query is returned as part of the data
- Data object has 2 properties:
pages
pageParams
tracks the keys of queries that have been retrieved (not commonly used)
- Every query has its own element in the pages array
- Current value of
pageParam
is maintained by react query useInfiniteQuery
options:getNextPageParam
: (lastPage, allPages)- updates
pageParam
- might use all of the pages of data (allPages)
lastPage
works as anext
property
- updates
fetchNextPage
- function to call when the user needs more data
hasNextPage
- based on return value of
getNextPageParam
- if
undefined
, no more data
- based on return value of
isFetchingNextPage
- for displaying loading spinner
- difference between
isFetching
&isFetchingNextPage
- Component mounts:
data: undefined
- Component mounts -> Fetch first page:
data: undefined & pageParam: default
- After component mounts and data fetched:
data.pages[0]: {...} & update pageParamZ
hasNExtPage
? ->fetchNextPage
- No more pages? ->
pageParam: undefined & hasNextPage: false
import { useInfiniteQuery } from "react-query";
const initialUrl = "https://dummy/api";
const fetchUrl = async (url) => {
const response = await fetch(url);
return response.json();
};
export function InfinitePeople() {
const {
data,
fetchNextPage,
hasNextPage,
isLoading,
isFetching,
isError,
error,
} = useInfiniteQuery(
"sw-people",
({ pageParam = initialUrl }) => fetchUrl(pageParam),
{ getNextPageParam: (lastPage) => lastPage.next || undefined }
);
if (isLoading) {
return <h3>Loading...</h3>;
}
if (isError) {
return <h3>Error {error.toString()}</h3>;
}
return (
<>
{isFetching && <h3>Loading...</h3>}
<InfiniteScroll loadMore={fetchNextPage} hasMore={hasNextPage}>
{data.pages.map((pageData) =>
pageData.map((person) => <Person person={person} />)
)}
</InfiniteScroll>
</>
);
}
- Just as in react, custom hooks help abstract logic from a component grouping multiple hooks
import { useQuery } from "react-query";
import type { Treatment } from "../../../../../shared/types";
import { axiosInstance } from "../../../axiosInstance";
import { queryKeys } from "../../../react-query/constants";
// for when we need a query function for useQuery
async function getTreatments(): Promise<Treatment[]> {
const { data } = await axiosInstance.get("/treatments");
return data;
}
export function useTreatments(): Treatment[] {
// const fallback = [];
const { data = [] } = useQuery(queryKeys.treatments, getTreatments);
return data;
}
Read more on Custom hooks
-
In smaller apps:
- used isFetching from useQuery return object
- isLoading is isFetching plus no cached data
-
In larger apps:
- loading spinner whenever any query isFetching
- useIsFetching helps in this case
-
No need for isFetching on every custom hook / useQuery call
- No
useError
analogy foruseFetching
- not a boolean: unclear how to implement
- Instead, set default
onError
handler for queryClient
{
queries: { useQuery options },
mutations: { useMutation options }
}
- It's possible to pass a default error handler to the query client constructor
- this will handle all errors inside the query provider
export function queryErrorHandler(error: unknown): void {
const id = "react-query-error";
const title =
error instanceof Error
? error.toString().replace(/^Error:\s*/, "")
: "Error connecting to server";
toast.closeAll();
toast({ id, title, status: "error", variant: "subtle", isClosable: true });
}
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
onError: queryErrorHandler,
},
},
});
- Then use the queryClient without any optional parameters
export function useTreatments(): Treatment[] {
const { data = [] } = useQuery(queryKeys.treatments, getTreatments);
return data;
}
- using the same key for every query
- nothing to trigger refetch
- component remount
- window refocus
- running refetch function manually
- automated refetch
- use keys for every change!
- treat keys a dependency array
const { data: appointments = fallback } = useQuery(
[queryKeys.appointments, monthYear.year, monthYear.month],
() => getAppointments(monthYear.year, monthYear.month)
);
- data can be pre-fetched using the
useQueryClient
hook and theprefetchQuery
method
import { useQuery, useQueryClient } from "react-query";
// prefetch next month when monthYear changes
const queryClient = useQueryClient();
useEffect(() => {
// assume increment of one month
const nextMonthYear = getNewMonthYear(monthYear, 1);
queryClient.prefetchQuery(
[queryKeys.appointments, nextMonthYear.year, nextMonthYear.month],
() => getAppointments(nextMonthYear.year, nextMonthYear.month)
);
}, [queryClient, monthYear]);
- transform or select a part of the data returned by the query function
- it allows to filter out data from
useQuery
- react query memoizes data to reduce unnecessary computation
- tech details:
- triple equals
"==="
comparison ofselect
function - only runs if data changes and the function has changed
- triple equals
- need a stable function (
useCallback
for anonymous function) useCallback
will also improve the performance of the caching method- Select is not an option for pre-fetch!
More on Data Transformation
// imports...
async function getStaff(): Promise<Staff[]> {
const { data } = await axiosInstance.get("/staff");
return data;
}
export function useStaff(): UseStaff {
const [filter, setFilter] = useState("all");
const selectFn = useCallback(
(unfilteredStaff) => filterByTreatment(unfilteredStaff, filter),
[filter]
);
const fallback = [];
// use of the select property to apply filter to cached data
const { data: staff = fallback } = useQuery(queryKeys.staff, getStaff, {
select: filter !== "all" ? selectFn : undefined,
});
return { staff, filter, setFilter };
}
- re-fetch ensures stale data gets updated from server
- leave page and refocus
- stale queries are re-fetched automatically in the background when:
- new instance of the query mount
- every time a react component using react query mounts
- the window is refocused
- the network is reconnected
- configured
refetchInterval
has expired- automatic
polling
- automatic
- global or query-specific options:
refetchOnMount
,refetchOnWindowFocus
refetchOnReconnect
,refetchInterval
- increase stale time
- turn off refetchOnMount / refetchOnWindowFocus / refetchOnReconnect
- only for very rarely changed, not mission-critical data
- suppressing the refetch can be done in each individual query client or in the global query client
export function useTreatments(): Treatment[] {
const { data = [] } = useQuery(queryKeys.treatments, getTreatments, {
staleTime: 600000, // 10 minutes
cacheTime: 900000, // 15 minutes (doesn't make sense for staleTime to exceed cacheTime)
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
});
return data;
}
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
onError: queryErrorHandler,
staleTime: 10 * 60 * 1000,
cacheTime: 15 * 60 * 1000,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
},
},
});
- dependent queries
- without a provider, no persistence across
useUser
calls - react query acting as a provider for auth
- use
queryClient.setQueryData
- add to
updateUser
andclearUser
import { useState } from "react";
import { useQuery, useQueryClient } from "react-query";
import { queryKeys } from "../../../react-query/constants";
import {
clearStoredUser,
getStoredUser,
setStoredUser,
} from "../../../user-storage";
async function getUser(user: User | null): Promise<User | null> {
if (!user) return null;
const { data } = await axiosInstance.get(`/user/${user.id}`, {
headers: getJWTHeader(user),
});
return data.user;
}
export function useUser(): UseUser {
const [user, setUser] = useState<User | null>(getStoredUser());
const queryClient = useQueryClient();
// call useQuery to update user data from server
useQuery(queryKeys.user, () => getUser(user), {
enabled: !!user,
onSuccess: (data) => setUser(data),
});
// meant to be called from useAuth
function updateUser(newUser: User): void {
// set user in state
setUser(newUser);
// update user in localstorage
setStoredUser(newUser);
// pre-populate user profile in React Query client
queryClient.setQueryData(queryKeys.user, newUser);
}
// meant to be called from useAuth
function clearUser() {
// update state
setUser(null);
// remove from localstorage
clearStoredUser();
// reset user to null in query client
queryClient.setQueryData(queryKeys.user, null);
}
return { user, updateUser, clearUser };
}
-
in this case, after the user data has been fetched, it is set to the query client using the
setQueryData
method -
useQuery
caches user data and refreshes from server- refreshing from server will be important for mutations
-
useUser
manages user data in query cache andlocalStorage
- set query cache using
setQueryData
on sign in / sign out
- set query cache using
-
user
useQuery
dependent onuser
state being truthyuser
state initially set byupdateUser
called by authsignin
- can't query server if we don;t have a user ID!
- can't remove query because query writes to user state
- invalidate query on mutation so data is purged from the cache
- update cache with data returned from the server after mutation
- optimistic update (assume mutation will succeed, rollback if not)
- errors:
- set
onError
callback inmutations
property of query clientdefaultOptions
- set
- loading:
useIsMutating
is analogous touseIsFetching
- update
Loading
component to show onisMutating
- to start using mutations we need to setup our
queryClient
export const queryClient = new QueryClient({
defaultOptions: {
...
mutations: {
onError: queryErrorHandler,
},
},
});
- then, we can use the
useIsMutating
hook to have a global indicator
import { useIsFetching, useIsMutating } from "react-query";
const isMutating = useIsMutating();
const display = isFetching || isMutating ? "inherit" : "none";
- use
useMutation
to update data in the server - very similar to
useQuery
- differences:
- no cache data
- no retries
- no refetch
- no
isLoading
vsisFetching
- returns
mutate
function which actually runs mutation onMutate
callback (useful for optimistic queries)
- type for returning
mutate
function from custom hook
import { UseMutateFunction, useMutation } from "react-query";
export function useReserveAppointment(): UseMutateFunction<
void,
unknown,
Appointment,
unknown
> {
const { user } = useUser();
const { mutate } = useMutation((appointment: Appointment) =>
setAppointmentUser(appointment, user?.id)
);
return mutate;
}
- invalidate appointments cache data on mutation
- so users don't have to refresh the page
invalidateQueries
effects:- marks query as stale
- triggers re-fetch if query currently being rendered
mutate
>onSuccess
>invalidateQueries
>re-fetch
More on query invalidation
const { mutate } = useMutation(
(appointment: Appointment) => setAppointmentUser(appointment, user?.id),
{
onSuccess: () => {
queryClient.invalidateQueries([queryKeys.appointments]);
toast({
title: "You have reserved the appointment!",
status: "success",
});
},
}
);
return mutate;
invalidateQueries
takes a query keyprefix
- invalidate all related queries at once
- can make it exact with
{exact: true}
option - other queryClient methods take prefix too (like
removeQueries
)
- update query cache with results from mutation server call
- will update query cache and
localStorage
More on updates from mutation responses
- update cache before response from server
- "optimistic" that the mutation will work
- cache gets updated quicker
- especially useful if lots of components rely on it
- what if the server update fails?
useMutation
hasonMutate
callback- returns context value that's handed to
onError
for rollback - context value contains previous cache data
- returns context value that's handed to
onMutate
function can also cancel re-fetches-in-progress- don't want to overwrite optimistic update with old data from server!
More on optimistic updates
- user triggers update with
mutate
- send update to server
onMutate
- cancel queries in progress
- update query cache
- save previous cache value
- on success?
- invalidate query
- on error?
- uses context to roll back cache
- invalidate query
- in order to cancel from react query, query function must:
- return a promise with a
cancel
property that cancels query
- return a promise with a
More on query cancellation