React Query — You do not need a Global State Manager

Mucahid Yazar
13 min readAug 5, 2022

Today I won’t give you something that you can’t find in the documentation of @tanstack/react-query. So you can find more detail on the documentation. I’ll give you most use cases with examples and some of my experience with @tanstack/react-query.

What is React Query in a nutshell?

  • 2 hooks + 1 utility
  • 5 kb
  • ready-to-use
  • highly configurable

You can use @tanstack/react-query and your global state management together. And with @tanstack/react-query you can fetch, cache, and update data in your React and React Native applications all without touching any “global state”. So time to replace some of your state management logic with @tanstack/react-query.

Why should you use @tanstack/react-query in your project?

  • You can’t mess it up. Yes if you use global state management and you don’t have enough experiences in react you can easily knot your project. But with react-query, you can’t do that even if you want it :)
  • It gives us the power of best practices of fetching with its own states, (like isLoading, isError, error, etc…)
  • Powerful cache and stale system.
  • Provide shorter codes. So at the end of the day, you will delete lots of line codes in your codebase.

I wanna show my notes on react-query. So let’s look at some key use cases.

Installation

yarn add @tanstack/react-query @tanstack/react-query/devtools axios

Setup

  • I will go with a next app. And you can also use a free API. (apilist.fun)
  • You should import QueryClientProvider, and QueryClient to your main App file and wrap your component with QueryClientProvider like below.
  • Then you should create a queryClient with QueryClient like below and you should give this queryClient to QueryClientProvider.
  • And to see dev tools we should import ReactQueryDevtools and write it inside the QueryClientProvider. And set it in a position to see it.
import '../styles/globals.css'
import type {AppProps} from 'next/app'
import {QueryClientProvider, QueryClient} from '@tanstack/react-query'
import {ReactQueryDevtools} from '@tanstack/react-query/devtools'
function MyApp({Component, pageProps}: AppProps) {
const queryClient = new QueryClient()
return (
<QueryClientProvider {...pageProps} client={queryClient}>
<Component {...pageProps} />
<ReactQueryDevtools initialIsOpen={false} position="bottom-right" />
</QueryClientProvider>
)
}
export default MyApp

APIs

useQuery

Basic Fetch

We can use useQuery hook to fetch datas like below. it will return us lots of things. Like data, isLoading, error.

import type {NextPage} from 'next'
import {useQuery} from '@tanstack/react-query'
import Link from 'next/link'
import axios from 'axios'
const Home: NextPage = () => {
const {data, isLoading, error} = useQuery(
['users',]
async () => {
return await axios.get('http://localhost:4000/users')
},
)
return (
<div>
{isLoading && <>Loading...</>}
{error && <>Error: {error.message}</>}
{data?.data.map(item => (
<p key={item.id}>{item.name}</p>
))}
</div>
)
}
export default Home

Advanced Fetch

isFetching

You can see the fetching moment by isFetching like below.

refetch

Or you can refetch with your action. You can give this utility to a onClick or anything that you want to trigger by.

import type {NextPage} from 'next'
import {useQuery} from '@tanstack/react-query'
import Link from 'next/link'
import axios from 'axios'
const Home: NextPage = () => {
const {data, isLoading, error, isFetching, refetch} = useQuery(
['users'],
async () => {
return await axios.get('http://localhost:4000/users')
}
)
return (
<div>
<nav>
<Link href={`/`}>Home</Link>
<Link href={`/users`}>Users</Link>
<Link href={`/users-rq`}>Users-RQ</Link>
<Link href={`/users-rq-ssr`}>Users-RQ-SSR</Link>
</nav>
<div>
{isLoading && <>Loading...</>}
{isFetching && <>Fetching...</>}
{error && <>Error: {error.message}</>}
{data?.data.map(item => (
<p key={item.id}>{item.name}</p>
))}
<button onClick={refetch}>Fetch Users</button>
</div>
</div>
)
}
export default Home

Info About Times

1000 => 1 second 5000 => 5 seconds 5000000 => 5 minutes

cacheTime

default => 5000000 / 5 minutes It will cache the data for 5 seconds if you write 5000 like me.

staleTime

default => 0 / 0 seconds Your data that you fetch will be stale data after 4 seconds

refetchOnMount

default => true other options => “always” | false

refetchOnWindowFocus

default => true other options => “always” | false Focus = We are doing something outside of the browser or outside of our app tab then we are returning to our app tab. This will fetch the data again if you focus your app again. For example, if you open another app or another tab then if you come back to your app page, it will fetch the data again.

import type {NextPage} from 'next'
import {useQuery} from '@tanstack/react-query'
import Link from 'next/link'
import axios from 'axios'
const Home: NextPage = () => {
const {data, isLoading, error, isFetching, refetch} = useQuery(
['users'],
async () => {
return await axios.get('http://localhost:4000/users')
},
{
cacheTime: 5000,
staleTime: 4000,
refetchOnMount: true,
refetchOnWindowFocus: true
},
)
return (
<div>
<nav>
<Link href={`/`}>Home</Link>
<Link href={`/users`}>Users</Link>
<Link href={`/users-rq`}>Users-RQ</Link>
<Link href={`/users-rq-ssr`}>Users-RQ-SSR</Link>
</nav>
<div>
{isLoading && <>Loading...</>}
{isFetching && <>Fetching...</>}
{error && <>Error: {error.message}</>}
{data?.data.map(item => (
<p key={item.id}>{item.name}</p>
))}
<button onClick={refetch}>Fetch Users</button>
</div>
</div>
)
}
export default Home

refetchOnWindowFocus and staleTime

staleTime: 10000,
refetchOnWindowFocus: true,

If we use this together and if the data is not stale, we will not fetch data again, even if we pass refetchOnWindowFocus: true. refetchOnWindowFocus will fetch if your data is a staled data.

Of course you can get over this problem if you need. Just pass “always” as value. like below…

staleTime: 10000,
refetchOnWindowFocus: "always",

refetchInterval (Polling)

default => false This will fetch data on every timespan which you wrote below.

refetchIntervalInBackground

default => false This will fetch data even if your focus is not on your app tab or browser either

import type {NextPage} from 'next'
import {useQuery} from '@tanstack/react-query'
import Link from 'next/link'
import axios from 'axios'
const Home: NextPage = () => {
const {data, isLoading, error, isFetching, refetch} = useQuery(
['users'],
async () => {
return await axios.get('http://localhost:4000/users')
},
{
refetchInterval: 2000,
refetchIntervalInBackground: true,
},
)
return (
<div>
<nav>
<Link href={`/`}>Home</Link>
<Link href={`/users`}>Users</Link>
<Link href={`/users-rq`}>Users-RQ</Link>
<Link href={`/users-rq-ssr`}>Users-RQ-SSR</Link>
</nav>
<div>
{isLoading && <>Loading...</>}
{isFetching && <>Fetching...</>}
{error && <>Error: {error.message}</>}
{data?.data.map(item => (
<p key={item.id}>{item.name}</p>
))}
<button onClick={refetch}>Fetch Users</button>
</div>
</div>
)
}
export default Home

onSuccess & onError

You can pass a callback function for onSuccess and onError. Also if the fetching is successful you will get the data if it has an error you will get the error.

select

You can use select to modify your return data. As you see below before I was using data.data.map but after select, I can use just data.map

import type {NextPage} from 'next'
import {useQuery} from '@tanstack/react-query'
import Link from 'next/link'
import axios from 'axios'
const Home: NextPage = () => {
const onSuccess = data => {
console.log("You've got data: ", data)
}
const onError = error => {
console.log("You've got an error: ", error)
}
const {data, isLoading, error, isFetching, refetch} = useQuery(
['users'],
async () => {
return await axios.get('http://localhost:4000/users')
},
{
onSuccess,
onError,
select: data => data.data,
},
)
return (
<div>
<nav>
<Link href={`/`}>Home</Link>
<Link href={`/users`}>Users</Link>
<Link href={`/users-rq`}>Users-RQ</Link>
<Link href={`/users-rq-ssr`}>Users-RQ-SSR</Link>
</nav>
<div>
{isLoading && <>Loading...</>}
{isFetching && <>Fetching...</>}
{error && <>Error: {error.message}</>}
{/* before select => {data?.data.map(item => ( */}
{data?.map(item => (
<p key={item.id}>{item.name}</p>
))}
<button onClick={refetch}>Fetch Users</button>
</div>
</div>
)
}
export default Home

Multiple Places with the Same useQuery

If you need to use the same useQuery multiple components or multiple pages, you can create a utility with the same useQuery logic and you can use it anywhere you want.

/hooks/useUsersData.js

import {useQuery} from '@tanstack/react-query'
import axios from 'axios'
export const useUsersData = ({config}) => {
return useQuery(
['users'],
async () => {
return await axios.get('http://localhost:4000/users')
},
{
...config,
},
)
}

/pages/xxx.js

import type {NextPage} from 'next'
import Link from 'next/link'
import {useUsersData} from '../../src/hooks'
const Home: NextPage = () => {
const onSuccess = data => {
console.log("You've got data: ", data)
}
const onError = error => {
console.log("You've got an error: ", error)
}
const {data, isLoading, error, isFetching, refetch} = useUsersData({
onSuccess,
onError,
select: data => data.data,
})
return (
<div>
<nav>
<Link href={`/`}>Home</Link>
<Link href={`/users`}>Users</Link>
<Link href={`/users-rq`}>Users-RQ</Link>
<Link href={`/users-rq-ssr`}>Users-RQ-SSR</Link>
</nav>
<div>
{isLoading && <>Loading...</>}
{isFetching && <>Fetching...</>}
{error && <>Error: {error.message}</>}
{/* before select => {data?.data.map(item => ( */}
{data?.map(item => (
<p key={item.id}>{item.name}</p>
))}
<button onClick={refetch}>Fetch Users</button>
</div>
</div>
)
}
export default Home

How useQuery cache in Dynamic Pages like /users/1, /users/2

As you know when we create [id].js file in next js for dynamic pages. It is similar in pure react too. So the page is the same file. So how can useQuery understand the previous cache is from /1 or /2. We will see how @tanstack/react-query handle this with useQuery.

First let’s create a dynamic page under the /users /pages/users/[id].js

import type {NextPage} from 'next'
import {useQuery} from '@tanstack/react-query'
import Link from 'next/link'
import axios from 'axios'
import {useUsersData} from '../../src/hooks'
import {useRouter} from 'next/router'
const Users: NextPage = () => {
const router = useRouter()
const {id} = router.query const {data, isLoading, error} = useUsersData(id, {
select: data => data.data,
})
return (
<div>
<nav>
<Link href={`/`}>Home</Link>
<Link href={`/users`}>Users</Link>
<Link href={`/users-rq`}>Users-RQ</Link>
<Link href={`/users-rq-ssr`}>Users-RQ-SSR</Link>
</nav>
<div>
{isLoading && <>Loading...</>}
{error && <>Error: {error.message}</>}
<div>Id: {id}</div>
<div>Name: {data?.name}</div>
<div>Alter Ego: {data?.alterEgo}</div>
</div>
</div>
)
}
export default Users

Now let’s create our useQuery hooks. As you see below we pass an array as its first argument. one is ‘users’ the second one is a dynamic value. In this way, @tanstack/react-query understands cache correctly. If we don’t do this and if we pass just ‘users’ instead of an array, then when we open /users/1 @tanstack/react-query will cache this, then when we open /users/2, @tanstack/react-query will show use the cache of /users/1. So it is an unwanted side-effect that we don’t want. So we can handle it like below.

import {useQuery} from '@tanstack/react-query'
import axios from 'axios'
export const useUsersData = (userId, config) => {
return useQuery(
['users', userId],
async () => {
return await axios.get(`http://localhost:4000/users/${userId}`)
},
{
...config,
},
)
}

Or we can handle it like below with queryKey.

const fetchUsers = async ({queryKey}) => {
// queryKey === ['users', userId]
const userId = queryKey[1]
return await axios.get(`http://localhost:4000/users/${userId}`)
}
export const useUsersDataWithQueryKey = (userId, config) => {
return useQuery(['users', userId], fetchUsers, {
...config,
})
}

Dynamic Multiple Requests

Or we can fetch multiple requests as dynamic. Ex: with props like below

import type {NextPage} from 'next'
import {useQueries} from '@tanstack/react-query'
import axios from 'axios'
const fetchUser = async (id: string) => {
return await axios.get(`http://localhost:4000/users/${id}`)
}
const DynamicParallelQueries: NextPage = ({userIds = [1, 3]}) => {
const [{data: userOne}, {data: userThree}] = useQueries(
userIds.map(id => ({
queryKey: ['user', id],
queryFn: async () => fetchUser(id),
})),
)
return (
<div>
<p>Users</p>
<p>UserOne Name: {userOne?.data.username}</p>
<p>UserThree Name: {userThree?.data.username}</p>
</div>
)
}
export default DynamicParallelQueries

Dependent Query Fetching

As you can see below we can fetch dependent query fetching together. We can wait request with useQuery thanks to enabled.

import type {NextPage} from 'next'
import {useQuery} from '@tanstack/react-query'
import Link from 'next/link'
import axios from 'axios'
const fetchUserByEmail = async (email: string) => {
return await axios.get(`http://localhost:4000/users/${email}`)
}
const fetchCoursesByEmail = async () => {
return await axios.get(`http://localhost:4000/courses`)
}
const DependentQueries: NextPage = ({email = 'mucahidyazar@gmail.com'}) => {
const {data: userData} = useQuery(['user', email], () =>
fetchUserByEmail(email),
)
const user = userData?.data
const userId = user?.id
const {data: coursesData} = useQuery(
['courses', userId],
() => fetchCoursesByEmail(),
{
enabled: !!userId,
select: data =>
data?.data?.filter(course => course.author === user?.username),
},
)
return (
<div>
<div>
<p>User Id: {userId}</p>
{email}
<p>Courses</p>
{coursesData?.map(item => (
<div key={item.id}>
<p>Author: {item.id}</p>
<p>{item.title}</p>
</div>
))}
</div>
</div>
)
}
export default DependentQueries

useQueryClient().getQueryData & initialData

useQueryClient => we can create a query client with useQueryClient hook. And we can use its getQueryData to get the data that it fetched before. initialData => we can use initialData key to return an initial value with useQuery hooks.

In this example, we send a request to get all users before this request below which is “users”. Then we click one of the users to see its detail and we send another useQuery with “user” key. But we don’t want to see loading, we want to see data instead of that. Then we can use “getQueryData” of queryClient. like below.

import {useQuery, useQueryClient} from '@tanstack/react-query'
import axios from 'axios'
export const useUserData = (userId, config) => {
const queryClient = useQueryClient()
return useQuery(
['user', userId],
async () => {
return await axios.get(`http://localhost:4000/users/${userId}`)
},
{
...config,
initialData: () => {
const user = queryClient
.getQueryData('users')
?.data?.find(item => item.id == userId)
if (user) {
return {
data: user,
}
} else {
return undefined
}
},
},
)
}

Pagination & keepPreviousData and Avoiding Layout Shift

Pagination is as simple as below. We just keep a state to know page number and increment and decrement it. I wrote hardcoded it but you can get it from your database dynamically.

keepPreviousData

default => false we can use “keepPreviousData” key using old data. We can avoid layout shifting thanks to “keepPreviousData”. We just need to turn on “keepPreviousData” to true. And by this way, we will keep and show previous data until we fetch the new data on the page. For example below we fetch the first page and we show 3 colors on 1. page. But when we click next we will still show the first page items until we fetch the items of the 2. page.

import type {NextPage} from 'next'
import {useQuery} from '@tanstack/react-query'
import axios from 'axios'
import {useState} from 'react'
const PaginatedQueries: NextPage = () => {
const [page, setPage] = useState(1)
const {data, isLoading, isError, error} = useQuery(
['colors', page],
async () => {
return await axios.get(
`http://localhost:4000/colors?_limit=3&_page=${page}`,
)
},
{
keepPreviousData: true,
},
)
if (isLoading) {
return <>Loading...</>
}
if (isError) {
return <>Error: {error?.message}</>
}
return (
<div>
<p>Colors</p>
{data?.data.map(item => (
<p key={item.id}>{item.label}</p>
))}
<button onClick={() => setPage(prev => prev - 1)} disabled={page < 2}>
Prev
</button>
<button onClick={() => setPage(prev => prev + 1)} disabled={page > 2}>
Next
</button>
</div>
)
}
export default PaginatedQueries

useInfiniteQuery & hasNextPage & fetchNextPage

useInfiniteQuery => We can use “useInfiniteQuery” to implement and fetch infinite loading to our page. fetchNextPage => When we use this hook we can also use “fetchNextPage” to trigger the load more button. hasNextPage => “hasNextPage” will give us that we have a next page or not. We will return its return with “getNextPageParam” option.

import type {NextPage} from 'next'
import {useInfiniteQuery} from '@tanstack/react-query'
import axios from 'axios'
const fetchColors = async ({pageParam = 1}) => {
// pageParam => 1, 2, 3, ...
return await axios.get(
`http://localhost:4000/colors?_limit=3&_page=${pageParam}`,
)
}
const InfiniteQueries: NextPage = () => {
const {data, isLoading, isError, error, hasNextPage, fetchNextPage} =
useInfiniteQuery(['colors'], fetchColors, {
getNextPageParam: (_lastPage, pages) => {
if (pages.length < 3) {
return pages.length + 1
} else {
return undefined
}
},
})
if (isLoading) {
return <>Loading...</>
}
if (isError) {
return <>Error: {error?.message}</>
}
return (
<div>
<p>Colors</p>
{data?.pages.map((group, index) => (
<p key={index}>
{group.data.map(item => (
<div key={item.id}>{item.label}</div>
))}
</p>
))}
<button onClick={fetchNextPage} disabled={!hasNextPage}>
show more
</button>
</div>
)
}
export default InfiniteQueries

useMutation

mutate & POST Requests

We can use mutate for post requests and we can use them like below.

queryClient.invalidateQueries

We can mark the query as stale and we can tell it should be re-fetched again. So below when we add a user with useMutation hook then we can create a “queryClient” and use “invalidateQueries” method inside of its onSuccess case. We can call it and we can say the users’ data is stale you should re-fetch it.

import axios from 'axios'
import type {NextPage} from 'next'
import Link from 'next/link'
import {useEffect, useState} from 'react'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
const Home: NextPage = () => {
const [email, setEmail] = useState()
const [password, setPassword] = useState()
const {
data: usersData,
isLoading,
isError,
error,
refetch,
} = useQuery(['users'], async () => {
return await axios.get('http://localhost:4000/users')
})
const addUser = async user => {
return axios.post('http://localhost:4000/users', user)
}
const queryClient = useQueryClient()
const {
mutate,
isError: mutateIsError,
error: mutateError,
isLoading: mutateIsLoading,
} = useMutation(addUser, {
onSuccess: () => {
queryClient.invalidateQueries('users')
},
})
const handleAddUserClick = () => {
const user = {
email,
password,
}
mutate(user)
}
if (isLoading) return <>Loading...</>
if (isError) return <>Error: {error.message}</>
return (
<div>
<nav>
<Link href={`/`}>Home</Link>
<Link href={`/users`}>Users</Link>
<Link href={`/users-rq`}>Users-RQ</Link>
<Link href={`/users-rq-ssr`}>Users-RQ-SSR</Link>
</nav>
<div>
<div>
<input
type="text"
value={email}
onChange={e => setEmail(e.target.value)}
/>
<input
type="text"
value={password}
onChange={e => setPassword(e.target.value)}
/>
<button onClick={handleAddUserClick}>Add User</button>
</div>
{usersData?.data?.map(item => (
<p key={item.id}>{item.id}</p>
))}
<button onClick={refetch}>Refetch</button>
</div>
</div>
)
}
export default Home

setQueryData

Instead of using “invalidateQueries” like above, we can use “setQueryData” to get updated data. with “invalidateQueries” we can send a new fetch request and get updated data again. Instead of this we can use “setQueryData” and manipulate the “query” of “users”. In this way, we do not need to send an extra one more request to get new data.

    onSuccess: data => {
// queryClient.invalidateQueries('users')
queryClient.setQueryData('users', oldQueryData => {
return {
...oldQueryData,
data: [...oldQueryData.data, data.data],
}
})
}

refetchQueries

Or if we need to fetch these data from more than one place, then we need to fetch the data for all of them. So we can use “refetchQueries” to update them all.

     onSuccess: data => {
// queryClient.invalidateQueries('users')
// queryClient.setQueryData('users', oldQueryData => {
// return {
// ...oldQueryData,
// data: [...oldQueryData.data, data.data],
// }
// })
queryClient.refetchQueries("users")
}

Optimistic Updates

Source => react-query.tanstack.com/guides/optimistic-..

  • We can immediately update the user interface with new data. (with this => queryClient.setQueryData(‘users’, old => […old, newUser]))
  • Then we can send our request, if we have an error we can get back the new data from UI and show the previous data. (with onError => queryClient.setQueryData(‘users’, context.previousUsers))
  • And we always re-fetch if the request succeeds or fails.

And these steps give us an optimistic update.

const queryClient = useQueryClient()
const {
mutate,
isError: mutateIsError,
error: mutateError,
isLoading: mutateIsLoading,
} = useMutation(newUser, {
//! onMutate => runs before the mutation is sent to the server
onMutate: async newUser => {
// we will cancel the queries because of we dont want they overwrite us
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
await queryClient.cancelQueries('users')
// Snapshot the previous value
const previousUsers = queryClient.getQueryData('users')
// Optimistically update to the new value
queryClient.setQueryData('users', old => [...old, newUser])
// Return a context object with the snapshotted value
return {previousUsers}
},
// If the mutation fails, use the context returned from onMutate to roll back
onError: (_error, _user, context) => {
queryClient.setQueryData('users', context.previousUsers)
},
// Always refetch after error or success:
onSettled: () => {
queryClient.invalidateQueries('users')
},
})

--

--