What is React Query?

Omer Shahzad
8 min readFeb 2, 2023

Since React is the UI Library there are no specific patterns for data fetching. We use useEffect for data fetching and useState for data storing

If we need to use data through the application using a State management library like Redux or context APIs.

Let Start, Install React Query from the following command.

npm i react-query

In your App.js file import the QueryClientProvider and QueryClient.

import { QueryClientProvider , QueryClient } from 'react-query';

Then wrap your app JSX with <QueryClientProvider> and create the instance of QueryClient. And pass it to <QueryCLientProvider> to access your complete application.

function App() {

//creating the instance of QueryClient
const queryClient = new QueryClient()
return (
<QueryClientProvider client={queryClient}>
<div className="App"></div>
</QueryClientProvider>
);
}

export default App;

ReactQueryDevTool:

React query provides the building dev tool to track all the queries. Import this from react-query and use it in your App.js to access this dev tool in all files

import { QueryClientProvider , QueryClient } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools'


function App() {

//creating the instance of QueryClient
const queryClient = new QueryClient()
return (
<QueryClientProvider client={queryClient}>
<div className="App"></div>
// initialIsOpen is like props which tells not to open devTool when the app start
// position : where the devTool Icon shown
<ReactQueryDevtools initialIsOpen ={false} position={'bottom-right'} />
</QueryClientProvider>
);
}

export default App;

QueryCache

Every query result is stored for 5 minutes and the next query relies on this cached for subsequent queries. The network checks if the cached and if he found the cached data then is key “isLoading” will remain false. but “isFetching” will change every time to check whether the data is changed or not then it will automatically update the view as well as cached data.

UseQuery

useQuery requires at least two arguments

  1. Unique Key
  2. The function which returns the promise
useQuery("super-heroes", () => { return axios.get("http://localhost:4000/superheroes") });

3. This hook returns multiple things we can destructure when we need them.

We can reduce the UseQuery function by creating the separate function

  const fetchData = () => {
return axios.get("http://localhost:4000/superheroes");
};

1. isLoading → data is a load or not. it is Boolean

2. data is the actual data from the API

3. error and IsError → isError is the boolean value and error has the error which is occurred

4. isFetching → isFetching is the boolean value that tells if the data is again fetched or not to check whether the data is changed or not while checking the data from the cache

  const { isLoading, data, error, isError, isFetching, refetch } = useQuery("super-heroes", fetchData)

Cached Time

This is the 3rd argument to change the cached time default time is 5min but you can change it according to your need. we can use to reduce the network request when the data is not changed often.

 { cacheTime: 5000 } 

StaleTime

This is used to change or reduce the network request and the time indicates to pause the request for some time. this time indicates that the new network is not triggered in this time frame. This time is in milliseconds.

The default time is 0s which means on every change or every page change the network is triggered we can increase the staleTime which helps to track either pause the request for sometime

{ staleTime: 30000 }

RefetchOnMount

when set to true every time when you navigate to this component it will trigger the network request.
when set to False it will just send the request when the component did mount ( refresh )

{ refetchOnMount : true }

RefetchOnWindowFocus

When the window got focus it automatically fetches data but while working with react it will not auto-fetch. The default value is set to “true” but we can change it if we won't fetch it every time the screen got focused.

{ refetchOnWindowFocus : true }  

RefetchInterval

Refetch data on a specific time interval which is mentioned in milliseconds this is also called “polling”

{ refetchInterval : 2000 }

enabled

if we want to fetch data on-click then we off the auto fetching. For this, we need to set “false”. Also, we can destruct the “refetch” property from useQuery and pass it to our onClick event.

const { refetch } = useQuery("key" , "function" , { enabled : false } )

onClick={()=> refetch}

select

we can transform our data into different types by using the “select” keyword. This’ll update the “data” key of useQuery

 select: (data) => {
const superHeroName = data.data.map((item) => item.name);
return superHeroName;
}

This will make the “isLoading” state false and stop to render the loading text again and again. This provides the initial data for the query. for this, we also need to import the QueryClient and create the instance to access the cached data.

This increases the data viewing experience of the user. we can use this to fetch the API data from the response of other APIs and provide the initial data to the API

For Example, The listing page may not have the complete data for the detail page but sufficient data to fetch the details for the detail page here we can use this. This initial data can be used to avoid the loader

Steps:

import useQueryClient and create the instance which helps us to get the cached data

const fetchSuperHero = ({ queryKey }) => {
const heroId = queryKey[1]
return axios.get(`http://localhost:4000/superheroes/${heroId}`)
}

export const useSuperHeroData = heroId => {
const queryClient = useQueryClient()
return useQuery(['super-hero', heroId], fetchSuperHero, {

initialData: () => {
//'super-hero' is the key of query "data" points the array in whih we have the responce
const hero = queryClient.getQueryData('super-heroes')?.data?.find(hero => hero.id === parseInt(heroId))
if (hero) {
return { data: hero }
} else {
return undefined
}
}

set the callback onSuccess and onError

{ 
onSuccess,
onError
}

Custom Hook

create the “hooks” folder and create the useSuperHeroData.js and create your custom hook.

import { useQuery} from 'react-query'
import axios from 'axios'

const fetchSuperHero = ({ queryKey }) => {
const heroId = queryKey[1]
return axios.get(`http://localhost:4000/superheroes/${heroId}`)
}

//we can also pass the array in the queryKey
export const useSuperHeroData = heroId => {
return useQuery(['super-hero', heroId], fetchSuperHero )
}

Import this hook where you need

import { useSuperHeroData } from "../hooks/useSuperHeroDetail";

//we can destructure the keys from our hook also
const { data, isLoading, isFetching, isError, error } = useSuperHeroData(id);

QueryById

This can be used when we want to fetch data by some id. if we want to pass any arguments to fetched function we need to make it an anonymous function otherwise it will not call the function by just passing the traditional way of passing the argument in the function.

const fetchSuperHero = (heroId) => {

return axios.get(`http://localhost:4000/superheroes/${heroId}`)
}

---------- NOT THE CORRECT WAY ------------------
export const useSuperHeroData = heroId => {
return useQuery('super-hero', fetchSuperHero(id) )
}

---------- CORRECT WAY ------------------
export const useSuperHeroData = heroId => {
return useQuery('super-hero', () => fetchSuperHero(id) )
}

Without using the anonymous function pass the Id

const fetchSuperHero = ({ queryKey }) => {
const heroId = queryKey[1]
return axios.get(`http://localhost:4000/superheroes/${heroId}`)
}

export const useSuperHeroData = heroId => {
return useQuery(['super-hero', heroId], fetchSuperHero)
}

Parallel Queries

In dynamic queries, we can make allies of every key which we can restructure from useQuery().

  const { data: SuperHeroData, isLoading: isLoadingSuperHero } = useQuery("fetchHero", fetchHero );
const { data: FriendData, isLoading: isLoadingFriend } = useQuery("fetchUser", fetchUser );

Dynamic Parallel Queries

When we have to load data of multiple users or we want to pass multiple arguments like getting the data of 5 different users one by one but we pass the id at the same time or we want the details of multiple books by passing their titles then Parallel comes into the picture.

We are receiving the array of “heroIds” using props and we are calling functions by each id individually

To achieve this we can replace our useQueryuseQueries

The useQueries hook accepts an array with query option objects identical to the useQuery hook.

 const results = useQueries([
{ queryKey: ['post', 1], queryFn: fetchPost },
{ queryKey: ['post', 2], queryFn: fetchPost },
])
const qurriesData = useQueries(
heroIds.map((id) => {
return {
queryKey: ["Super-Hero", id],
queryFn: () => fetchData(id),
};
})
);

Dependent Queries

Sequential queries depend on the result of other queries

const fetchDataFromPropsEmail = (email) => {
return axios.get(`http://localhost:4000/user/${email}`);
};

const fetchDataByUserEmail = (id) => {
return axios.get(`http://localhost:4000/userData/${id}`);
};

const DependentQurries = ({ email }) => {
const { data: user } = useQuery(["user", email], () => fetchDataFromPropsEmail(email) );
const id = user?.data.id;

// now this qurries is dependent to the result of above querry
const { data: userDetail } = useQuery(["userDetail", id],() => fetchDataByUserEmail(id),
{
//this is used to fetch data automatically
// "!!" means convert tha string into boolean
enabled: !!id
}
);

Paginated Queries

This is also used in UseQuery to provide the pagination feature also. This key is used to keep showing the previous until the new data is shown to the user.

{ keepPreviousData : true }

const { isLoading , isError , error , data } = useQuery(["colors" , pageNumber] , fetchColors , {
keepPreviousData : true
} )

Infinite Queries

To implement this we need to replace the useQueryuseInfinateQurries

“hasNextPage” → boolean which tells whether all the pages are ended or not.

“fetchNextPage” → function which is used to fetch the next page data.

“isFetchingNextPage” → boolean which is used to show the loaded while fetching the next page

“getNextPageParams” → function which returns the next page number depending on the logic you implement it

“_” before the last page tells that we don't need this param so we just pass add the underscore with it.

Important like other queries it doesn't return data in “data.data” it returns in “data.pages”

The fetcher function automatically receives the “pageParam” from the “getNextPageParams” function we just need to accept it from the fetcher function

const fetchColors = ({pageParam = 1}) => {
return axios.get(`http://localhost:4000/colors?_limit=2&_page=${pageParam}`)
}

const { isLoading , isError , error , data , isFetchingNextPage , isFetching , hasNextPage, fetchNextPage} = useInfiniteQuery(["colors"] , fetchColors, {
getNextPageParam : (_lastPage , pages) => {
if(pages.length < 4){
return pages.length + 1
} else{
return undefined
}
}
} )

// Button logic is like this
<button onClick={() => fetchNextPage()} disabled={!hasNextPage}>
Load more
</button>

Mutations:

It is used to update, delete and post requests. useMutation is the hook. And it doesn't need a unique key like useQuery. This hook returns the function mutate . first argument of this hook is the function then we can use its other function like

onSuccess → which is triggered when the mutation is successfully done

onError → which is triggered when the mutation returns some error.

onMutate -> this will trigger before the useMutation function

  const { mutate } = useMutation( () => 
request({ url: "/list", method: "post", data: addHero }),
);

mutate(addHero);

Query Invalidation

Automatically fetch the data when a successful mutation has occurred. used with the queryClient. we can use this onSuccessor onSettledor else depending on your condition.

queryClient.invalidateQueries('super-heroes') 
onSuccess : (data) => {
queryClient.invalidateQueries('super-heroes')
}

onSettled: () => {
queryClient.invalidateQueries("super-heroes");
},

Handling Mutation Response

setQueryData” and “getQueryData” is used to handle the mutation data. when we are getting the query data we need to store the data in some variable

const previousData = queryClient.getQueryData("super-heroes");

but in the case of setting, we can just set the data

queryClient.setQueryData("super-heroes", context.previousData);

Cancel Queries

we can use this if we do not want to auto-fetch some queries that interface with our custom logic so we cancel that queries to avoid auto-fetching.

queryClient.cancelQueries("super-heroes");

Optimistic Update

When we assume that our mutation query is done without any error and we are sure about our result of success then we use this query. This is used to enhance the user experience. For this, we need 3 callbacks

  1. onMutate()=>{}
  2. onError() => {}
  3. onSettleed() => {}

onMutate() is called before the mutation function is fired and it receives the same variable(data, object) the mutation function receives.

Before updating the optimistic update we must need to store the old data in case to roll back if our mutation fails.

Sign up to discover human stories that deepen your understanding of the world.

Omer Shahzad
Omer Shahzad

Written by Omer Shahzad

Software Engineer | MERN Stack Developer | AI | Langchain | OpenAI

Responses (1)

Write a response

I Would say it was a great cheat sheet