React Query library provides hooks for fetching, caching and updating asynchronous data in React. React Query allows you to defeat and overcome the tricky challenges and hurdles of server state and control your app data before it starts to control you.
npm i react-query
A query is a declarative dependency on an asynchronous source of data that is tied to a unique key. A query can be used with any Promise based method (including GET and POST methods) to fetch data from a server.
To subscribe to a query in your components or custom hooks, call the useQuery hook with at least:
- A unique key for the query
- A function that returns a promise that: Resolves the data, or Throws an error
import { useQuery } from 'react-query'
function App() {
const info = useQuery('todos', fetchTodoList)
}
The unique key you provide is used internally for refetching, caching, and sharing your queries throughout your application.
The query results returned by useQuery contains all of the information about the query that you'll need for templating and any other usage of the data.
A query can only be in one of the following states at any given moment:
isLoading
orstatus === 'loading'
- The query has no data and is currently fetchingisError
orstatus === 'error'
- The query encountered an errorisSuccess
orstatus === 'success'
- The query was successful and data is availableisIdle
orstatus === 'idle'
- The query is currently disabled (you'll learn more about this in a bit)error
- If the query is in anisError
state, the error is available via the error property.data
- If the query is in asuccess
state, the data is available via the data property.isFetching
- In any state, if the query is fetching at any time (including background refetching) isFetching will be true.
import { useQuery } from 'react-query'
function Todos() {
const { isLoading, isError, data, error } = useQuery('todos', fetchTodoList)
if (isLoading) {
return <span>Loading...</span>
}
if (isError) {
return <span>Error: {error.message}</span>
}
// We can assume by this point that `isSuccess === true`
return (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
Query keys can be as simple as a string, or as complex as an array of many strings and nested objects. As long as the query key is serializable, and unique to the query's data, you can use it!
// A list of todos
useQuery('todos', ...) // queryKey === ['todos']
// Something else, whatever!
useQuery('somethingSpecial', ...) // queryKey === ['somethingSpecial']
// An individual todo
useQuery(['todo', 5], ...) // queryKey === ['todo', 5]
// An individual todo in a "preview" format
useQuery(['todo', 5, { preview: true }], ...) // queryKey === ['todo', 5, { preview: true }]
// A list of todos that are "done"
useQuery(['todos', { type: 'done' }], ...) // queryKey === ['todos', { type: 'done' }]
A query function can be literally any function that returns a promise. The promise that is returned should either resolve the data or throw an error.
useQuery(['todos'], fetchAllTodos)
useQuery(['todos', todoId], () => fetchTodoById(todoId))
useQuery(['todos', todoId], async () => {
const data = await fetchTodoById(todoId)
return data
})
useQuery(['todos', todoId], ({ queryKey }) => fetchTodoById(queryKey[1]))
For React Query to determine a query has errored, the query function must throw. Any error that is thrown in the query function will be persisted on the error state of the query.
const { error } = useQuery(['todos', todoId], async () => {
if (somethingGoesWrong) {
throw new Error('Oh no!')
}
return data
})
Explicitly throwing errors when required
useQuery(['todos', todoId], async () => {
const response = await fetch('/todos/' + todoId)
if (!response.ok) {
throw new Error('Network response was not ok')
}
return response.json()
})
Query keys are not just for uniquely identifying the data you are fetching, but are also conveniently passed into your query function and while not always necessary, this makes it possible to extract your query functions if needed:
function Todos({ status, page }) {
const result = useQuery(['todos', { status, page }], fetchTodoList)
}
// Access the key, status and page variables in your query function!
function fetchTodoList({ queryKey }) {
const [_key, { status, page }] = queryKey
return new Promise()
}
If the number of queries you need to execute is changing from render to render, you cannot use manual querying since that would violate the rules of hooks. Instead, React Query provides a useQueries hook, which you can use to dynamically execute as many queries in parallel as you'd like.
useQueries accepts an array of query options objects and returns an array of query results:
function App({ users }) {
const userQueries = useQueries(
users.map(user => {
return {
queryKey: ['user', user.id],
queryFn: () => fetchUserById(user.id),
}
})
)
}
To achieve this, it's as easy as using the enabled option to tell a query when it is ready to run
// Get the user
const { data: user } = useQuery(['user', email], getUserByEmail)
const userId = user?.id
// Then get the user's projects
const { isIdle, data: projects } = useQuery(
['projects', userId],
getProjectsByUser,
{
// The query will not execute until the userId exists
enabled: !!userId,
}
)
// isIdle will be `true` until `enabled` is true and the query begins to fetch.
// It will then go to the `isLoading` stage and hopefully the `isSuccess` stage :)
If you would like to show a global loading indicator when any queries are fetching (including in the background), you can use the useIsFetching hook:
import { useIsFetching } from 'react-query'
function GlobalLoadingIndicator() {
const isFetching = useIsFetching()
return isFetching ? (
<div>Queries are fetching in the background...</div>
) : null
}
If a user leaves your application and returns to stale data, React Query automatically requests fresh data for you in the background. You can disable this globally or per-query using the refetchOnWindowFocus option:
// Disabling auto refetch globally
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
})
function App() {
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}
When a useQuery query fails (the query function throws an error), React Query will automatically retry the query if that query's request has not reached the max number of consecutive retries (defaults to 3) or a function is provided to determine if a retry is allowed.
You can configure retries both on a global level and an individual query level.
- Setting retry = false will disable retries.
- Setting retry = 6 will retry failing requests 6 times before showing the final error thrown by the function.
- Setting retry = true will infinitely retry failing requests.
- Setting retry = (failureCount, error) => ... allows for custom logic based on why the request failed.
import { useQuery } from 'react-query'
// Make a specific query retry a certain number of times
const result = useQuery(['todos', 1], fetchTodoListPage, {
retry: 10, // Will retry failed requests 10 times before displaying an error
})
The default retryDelay is set to double (starting at 1000ms) with each attempt, but not exceed 30 seconds:
// Configure for all queries
import { QueryCache, QueryClient, QueryClientProvider } from 'react-query'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
},
},
})
function App() {
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}
const result = useQuery('todos', fetchTodoList, {
retryDelay: 1000, // Will always wait 1000ms to retry, regardless of how many retries
})
const result = useQuery(['projects', page], fetchProjects)
By setting keepPreviousData to true we get a few new things:
- The data from the last successful fetch available while new data is being requested, even though the query key has changed.
- When the new data arrives, the previous data is seamlessly swapped to show the new data.
isPreviousData
is made available to know what data the query is currently providing you
function Todos() {
const [page, setPage] = React.useState(0)
const fetchProjects = (page = 0) => fetch('/api/projects?page=' + page).then((res) => res.json())
const {
isLoading,
isError,
error,
data,
isFetching,
isPreviousData,
} = useQuery(['projects', page], () => fetchProjects(page), { keepPreviousData : true })
return (
<div>
{isLoading ? (
<div>Loading...</div>
) : isError ? (
<div>Error: {error.message}</div>
) : (
<div>
{data.projects.map(project => (
<p key={project.id}>{project.name}</p>
))}
</div>
)}
<span>Current Page: {page + 1}</span>
<button
onClick={() => setPage(old => Math.max(old - 1, 0))}
disabled={page === 0}
>
Previous Page
</button>{' '}
<button
onClick={() => {
if (!isPreviousData && data.hasMore) {
setPage(old => old + 1)
}
}}
// Disable the Next Page button until we know a next page is available
disabled={isPreviousData || !data?.hasMore}
>
Next Page
</button>
{isFetching ? <span> Loading...</span> : null}{' '}
</div>
)
}
Rendering lists that can additively "load more" data onto an existing set of data or "infinite scroll" is also a very common UI pattern. React Query supports a useful version of useQuery called useInfiniteQuery for querying these types of lists.
When using
useInfiniteQuery
, you'll notice a few things are different:
data
is now an object containing infinite query data:data.pages
array containing the fetched pagesdata.pageParams
array containing the page params used to fetch the pages- The
fetchNextPage
andfetchPreviousPage
functions are now available- A
hasNextPage
boolean is now available and is true if getNextPageParam returns a value other than undefined- A
hasPreviousPage
boolean is now available and is true if getPreviousPageParam returns a value other than undefined- The
isFetchingNextPage
andisFetchingPreviousPage
booleans are now available to distinguish between a background refresh state and a loading more state
import { useInfiniteQuery } from 'react-query'
function Projects() {
const fetchProjects = ({ pageParam = 0 }) =>
fetch('/api/projects?cursor=' + pageParam)
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status,
} = useInfiniteQuery('projects', fetchProjects, {
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
return status === 'loading' ? (
<p>Loading...</p>
) : status === 'error' ? (
<p>Error: {error.message}</p>
) : (
<>
{data.pages.map((group, i) => (
<React.Fragment key={i}>
{group.projects.map(project => (
<p key={project.id}>{project.name}</p>
))}
</React.Fragment>
))}
<div>
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'Nothing more to load'}
</button>
</div>
<div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
</>
)
}
If you only want to actively refetch a subset of all pages, you can pass the refetchPage function to refetch returned from useInfiniteQuery.
const { refetch } = useInfiniteQuery('projects', fetchProjects, {
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
// only refetch the first page
refetch({ refetchPage: (page, index) => index === 0 })
Skipping pages for fetching data
function Projects() {
const fetchProjects = ({ pageParam = 0 }) =>
fetch('/api/projects?cursor=' + pageParam)
const {
status,
data,
isFetching,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
} = useInfiniteQuery('projects', fetchProjects, {
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
// Pass your own page param
const skipToCursor50 = () => fetchNextPage({ pageParam: 50 })
}