Skip to main content

React Query

Why React Query ?

React Query helps solves issues such as:

  1. Self hanlding loading state: Displaying placeholder while request is stil in-flight and error handling.
  2. Race condition: Request invoked later might get resolved earilier, resulting in view and state out of sync and potentially UI flash. (Though solveable through using flag and useEffect hook cleanup mechanism.)
  3. Data duplication: 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.
  4. Cache invalidation: Data might be stale if not handled properly.

React Query not only solves these problems but also offers additional features such as: cache management, auto refetch, 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-memory Javascript Map as cache.
  • QueryClientProvider wraps the lowest common parent of components that need query management.
  • QueryClientProvider uses Context for dependency injection.
  • queryKey passed to the useQuery 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:

  1. Request has been sent, response has yet to receive
  2. 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()
},
})
}

References