Skip to main content

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:

  1. Self hanlding loading state is required: including display 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.
  3. 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.
  4. 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-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()
},
})
}