React Query
Why React Query ?
A good reading with step-by-step example of what problems might be encountered without React Query: Why React Query?.
In short, React Query helps solves issues such as:
- Self hanlding loading state is required: including display placeholder while request is stil in-flight and error handling.
- Race condition: Request invoked later might get resolved earilier, resulting in view and state out of sync and potentially UI flash.
- Data duplication: Race condition can be solved thourgh using flag and useEffect hook cleanup mechanism to ensure data is updated with latest state. Different components might request same data causing data duplication. A custom hook utilizing context that maintains a in-memory cache can be abstracted for reuse across component to solve the problem.
- Cache invalidation
React Query not only solves these problems but also offers additional features such as: cache management/invalidation, auto refetching, scroll recovery, offline support, dependent queries, paginated queries, request cancellation, prefetching, polling, mutations, infinite scrolling, data selectors, etc.
Usage
Basic Syntax
QueryClient
maintinas a in-memoryJavascript Map
as cache.QueryClientProvider
wraps the lowest common parent of components that need query management.QueryClientProvider
uses Context for dependency injection.queryKey
passed to theuseQuery
hook must be globally unique.queryFn
must return a promise that resolves with the data to cache.
/* eslint-disable jsx-a11y/anchor-is-valid */
import React from 'react'
import ReactDOM from 'react-dom/client'
import {
QueryClient,
QueryClientProvider,
useQuery,
} from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import axios from 'axios'
const queryClient = new QueryClient()
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>
)
}
function Example() {
const { isLoading, error, data, isFetching } = useQuery({
queryKey: ['repoData'],
queryFn: () =>
axios
.get('https://api.github.com/repos/tannerlinsley/react-query')
.then((res) => res.data),
})
if (isLoading) return 'Loading...'
if (error) return 'An error has occurred: ' + error.message
return (
<div>
<h1>{data.name}</h1>
<p>{data.description}</p>
<strong>👀 {data.subscribers_count}</strong>{' '}
<strong>✨ {data.stargazers_count}</strong>{' '}
<strong>🍴 {data.forks_count}</strong>
<div>{isFetching ? 'Updating...' : ''}</div>
<ReactQueryDevtools initialIsOpen />
</div>
)
}
const rootElement = document.getElementById('root')
ReactDOM.createRoot(rootElement).render(<App />)
Query Status
Two ways are provided through the useQuery
hook to check the query status:
const { data, status } = useQuery({
queryKey: ['demo'],
queryFn: () => Promise.resolve('success');
})
if (status === 'pending') return (<div>Pending</div>);
if (status === 'error') return (<div>Error occurred</div>);
if (status === 'success') return (<div>Success</div>);
or
const { data, isPending, isError, isSuccess, isLoading, isFetching } = useQuery({
queryKey: ['demo'],
queryFn: () => Promise.resolve('success');
})
if (isPending) return (<div>Pending</div>);
if (isError) return (<div>Error occurred</div>);
if (isSuccess) return (<div>Success</div>);
Note that isLoading
is different from isPending
. That is because for a query to be pending, it could mean either:
- Request has been sent, response has yet to receive
- Request can't be sent for some reason, e.g offline, disabled observer in react-query
So isLoading
is equivalent to isPending
&& isFetching
.
Dependency Array
The queryKey
array servers as a dependency array, queryFn
will be re-run whenever a value in the queryKey array changes.
React Query handles this by hashing the objects in the array determiniscally, so a requirement for the queryKey
is that objects has to be JSON serializable. If a Map or Set is used as queryKey
, a custom queryKeyHashFn
must be provided.
queryKey
is designed as an array is because it can further be used to group queries into categories.
// 🟢 Declaratibe way
export default function useRepos(sort) {
return useQuery({
queryKey: ['repos', { sort }],
queryFn: async () => {
const response = await fetch(
`https://api.github.com/orgs/TanStack/repos?sort=${sort}`
)
if (!response.ok) {
throw new Error(`Request failed with status: ${response.status}`)
}
return response.json()
},
})
}
// 🔴 Imperative way
const { data, status, refetch } = useRepos(selection)
...
onChange={(event) => {
const sort = event.target.value
setSelection(sort)
refetch()
}}
export default function useRepos(sort) {
return useQuery({
queryKey: ['repos'],
queryFn: async () => {
const response = await fetch(`https://api.github.com/orgs/TanStack/repos?sort=${sort}`)
if (!response.ok) {
throw new Error(`Request failed with status: ${response.status}`)
}
return response.json()
},
})
}