Intro
TanStack Query (TQ) is an async data-fetching library that handles fetching, caching, synchronization, and updates server-state. It used to be called React-Query but was absorbed by the TanStack org.
TQ and Redux?
Yes, these may seem like both handle frontend state, because they do. However, they’re often used in tandem, because they serve different purposes.
- TQ is for fetching and caching server-data.
- Redux stores the client-side state such as UI settings, mode selections, etc; and makes them available to the entire app.
If both are used for the same data (copying fetched API results into Redux), this means you have two sources of truth, this can (and likely will) lead to bugs or state mismatch!
Why?
Most frameworks don’t have opinionated ways of state-management and data-fetching and certainly don’t handle async server-state well.
Server-state is tricky because:
- It’s in a location you may not control or own.
- Requires async APIs for fetching and updating.
- Implies shared ownership and can be changed by other people without your knowledge.
- Can end up being outdated in the app if not regularly monitored and updated.
// Incoming Massive shoulder patting…
“TanStack Query is hands down one of the best libraries for managing server state. It works amazingly well out-of-the-box, with zero-config, and can be customized to your liking as your application grows.”
Installation
They have a whole lot of options on their installation page, using package managers or CDNs. I will use NPM:
npm i @tanstack/react-query
They also recommend a plugin to catch bugs and inconsistencies but not sure I want to, frankly…
npm i -D @tanstack/eslint-plugin-query
Basic Example
They demo the core pillars of TQ with the following example.
import {
QueryClient,
QueryClientProvider,
useQuery,
} from '@tanstack/react-query'
const queryClient = new QueryClient()
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>
)
}
function Example() {
const { isPending, error, data } = useQuery({
queryKey: ['repoData'],
queryFn: () =>
fetch('https://api.github.com/repos/TanStack/query').then((res) =>
res.json(),
),
})
if (isPending) 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>
)
}
Breakdown:
- They import the TQ client and its hook
useQuery
- Initialize an instance of the client, before the component for obvious reasons… // It's to avoid initializing on each component call.
- Wrap your components (entire app in this example) in the
<QueryClientProvider>
component, likely to provider access similar to Context and Redux Provider. - Using the
useQuery
hook, we get 3 objects. - The hook expects
- A query key (what we’re looking for)
- The function,
fetch
is fine
- Check if it’s returned an error or is still pending
- Return component’s JSX.
Core Concepts - Quick Guide
Besides the previous “basic” example, they also have a “Quick Start” example that covers the three core concepts, Queries, Mutations, and Query Invalidation.
Queries
These are declarative dependencies for async data sources, associated with a unique identifier/key.
A Query is used with Promise methods (incl. GET
and POST
methods) to get data from a remote source.
==// Use Mutations if you plan on mutating server-data!==
Subscribing
Components sub to queries using the previously mentioned useQuery
hook.
Which needs:
- A unique key for the query
- A function that returns a promise that:
- Resolves the data, or
- Throws an error Example:
import { useQuery } from '@tanstack/react-query'
function App() {
const info = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList })
}
The unique key you provide is used internally for refetching, caching, and sharing your queries throughout your application.
Returned values: The hook will return all the info, which is an object with several fields such as { isLoading, isError, data, error, refetch, isFetching }
.
Pausing/Disabling auto fetching
Note, that useQuery
runs as soon as a component begins its lifecycle (renders), if you want to hold it off, then pass the enabled:false
option to it.
Example:
import { useQuery } from '@tanstack/react-query'
function App() {
const { isLoading, isError, data, error, refetch, isFetching } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList,
enabled: false,
})
}
Query Result States
The results from the hook have a few possible states to account for. We’ve seen these in the Basic Example breakdown.
isPending
orstatus === 'pending'
: The query has no data yet, wait.isError
orstatus === 'error'
: An error occurred. theerror
property will have more infoisSuccess
orstatus === 'success'
: Successfully fetched and data available. the data will be in thedata
property. When fetching, the query will haveisFetching
set totrue
.
Realistically, for most purposes, you’ll only need to check when it’s pending or if error’s occurred, otherwise you’ve got your data.
Fetch States
It’s got more statuses… // Not sure why they have string statuses AND boolean ones.
fetchStatus === 'fetching'
: The query is currently fetching.fetchStatus === 'paused'
: The query wanted to fetch, but it is paused. More info in the Network Mode guide.fetchStatus === 'idle'
: The query is not doing anything at the moment.
Two Statuses Explanation
Apparently, because it’s possible for all combinations of
status
andfetchStatus
to occur because of background re-fetching and stale status while revalidating. Examples:
- A successful,
status == "success"
, query can befetchStatus == "idle"
but can also"fetching"
during a refetch…- Queries with no data will often be
status == "pending"
andfetchStatus == "fetching"
but can also be"paused"
…
State Rules of Thumb
- The
status
provide information about the data, did it get it or not? - The
fetchStatus
give information about thequeryFn
passed to the hook. Is it running or not?
Query Invalidation
Sometimes you need to invalidate a query or render it invalid. This can be because it’s become stale, taken too long that it’s outta date, or because the user’s done an action that renders it invalid…etc. This is known as QueryInvalidation and is important when dealing with async data fetching.
the QueryClient
has an invalidateQueries
method that lets you, smartly, mark queries as stale and potentially refetch them too!
Example:
// Invalidate every query in the cache
queryClient.invalidateQueries()
// Invalidate every query with a key that starts with `todos`
queryClient.invalidateQueries({ queryKey: ['todos'] })
When you invalidate a query, two things happend,
- It’s status is set to stale, overriding any
staleTime
configs used by hooks. - If the query is currently being rendered by
useQuery
and other hooks, it’s refetched in the backgroud. // nice!
Query Matching
With methods like invalidateQueries
and removeQueries
you can match multiple queries by their prefix, or specifically target a query. Check out Query Filters.
Prefix Targeting
In this example, they use a prefix to target all queries that start their query key with it.
import { useQuery, useQueryClient } from '@tanstack/react-query'
// Get QueryClient from the context
const queryClient = useQueryClient()
queryClient.invalidateQueries({ queryKey: ['todos'] })
// Both queries below will be invalidated
const todoListQuery = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList,
})
const todoListQuery = useQuery({
queryKey: ['todos', { page: 1 }],
queryFn: fetchTodoList,
})
Variable Targeting
Can target queries with specific variables using a more specified key.
queryClient.invalidateQueries({
queryKey: ['todos', { type: 'done' }],
})
// The query below will be invalidated
const todoListQuery = useQuery({
queryKey: ['todos', { type: 'done' }],
queryFn: fetchTodoList,
})
// However, the following query below will NOT be invalidated
const todoListQuery = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList,
})
Exclusive Targeting
If you want to invalidate only what you target, use the exact: true
option in invalidateQueries
along with the key.
Custom targeting
If you need more specificity, pass a predicate function in the predicate: (q)=>{...}
option. It will receive each and every query, and must evaluate it to true or false for it to work.
Example:
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] === 'todos' && query.queryKey[1]?.version >= 10,
})
// The query below will be invalidated
const todoListQuery = useQuery({
queryKey: ['todos', { version: 20 }],
queryFn: fetchTodoList,
})
// The query below will be invalidated
const todoListQuery = useQuery({
queryKey: ['todos', { version: 10 }],
queryFn: fetchTodoList,
})
// However, the following query below will NOT be invalidated
const todoListQuery = useQuery({
queryKey: ['todos', { version: 5 }],
queryFn: fetchTodoList,
})
Mutations
Mutations are for CRUD effects on server-side data.
TQ has a hook for this, useMutation
.
Example:
function App() {
const mutation = useMutation({
mutationFn: (newTodo) => {
return axios.post('/todos', newTodo)
},
})
return (
<div>
{mutation.isPending ? (
'Adding todo...'
) : (
<>
{mutation.isError ? (
<div>An error occurred: {mutation.error.message}</div>
) : null}
{mutation.isSuccess ? <div>Todo added!</div> : null}
<button
onClick={() => {
mutation.mutate({ id: new Date(), title: 'Do Laundry' })
}}
>
Create Todo
</button>
</>
)}
</div>
)
}
Breakdown:
- …
Mutation States
isIdle
orstatus === 'idle'
:The mutation is currently idle or in a fresh/reset stateisPending
orstatus === 'pending'
:The mutation is currently runningisError
orstatus === 'error'
: The mutation encountered an errorisSuccess
orstatus === 'success'
: The mutation was successful and mutation data is available There’s even more info based on the state:- Error: if it errors out, the error is available via the
error
property. - Success: if it succeeds, the data is available via the
data
property.
Quick Start Example
This example illustrates all 3 concepts in action.
import {
useQuery,
useMutation,
useQueryClient,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
import { getTodos, postTodo } from '../my-api'
// Create a client
const queryClient = new QueryClient()
function App() {
return (
// Provide the client to your App
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
)
}
function Todos() {
// Access the client
const queryClient = useQueryClient()
// Queries
const query = useQuery({ queryKey: ['todos'], queryFn: getTodos })
// Mutations
const mutation = useMutation({
mutationFn: postTodo,
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
return (
<div>
<ul>
{query.data?.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
<button
onClick={() => {
mutation.mutate({
id: Date.now(),
title: 'Do Laundry',
})
}}
>
Add Todo
</button>
</div>
)
}
render(<App />, document.getElementById('root'))