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-queryThey also recommend a plugin to catch bugs and inconsistencies but not sure I want to, frankly…
npm i -D @tanstack/eslint-plugin-queryBasic 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
useQueryhook, we get 3 objects. - The hook expects
- A query key (what we’re looking for)
- The function,
fetchis 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.
isPendingorstatus === 'pending': The query has no data yet, wait.isErrororstatus === 'error': An error occurred. theerrorproperty will have more infoisSuccessorstatus === 'success': Successfully fetched and data available. the data will be in thedataproperty. When fetching, the query will haveisFetchingset 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
statusandfetchStatusto 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
statusprovide information about the data, did it get it or not? - The
fetchStatusgive information about thequeryFnpassed 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
staleTimeconfigs used by hooks. - If the query is currently being rendered by
useQueryand 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
isIdleorstatus === 'idle':The mutation is currently idle or in a fresh/reset stateisPendingorstatus === 'pending':The mutation is currently runningisErrororstatus === 'error': The mutation encountered an errorisSuccessorstatus === '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
errorproperty. - Success: if it succeeds, the data is available via the
dataproperty.
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'))