Mastering Data Fetching with React Query

Karthik Joshi
10 min readApr 6, 2024

--

React query logo

Data fetching in React applications can be a breeze with React Query. Let's dive into the various features of React query and how it can simplify our lives as a developer.

What is React Query?

React Query is a data fetching library now it’s gone under the Tanstack umbrella (official docs), react query is nothing but a wrapper on the fetch method under the hood but when we want to implement pooling, dynamic query with Normal fetch / Axios it becomes harder Using React query we can achieve that with best optimization possible that is why we need React query.

Installation & Implementation

Installation is easier with npm or any other package management tool

npm i @tanstack/react-query
# or
pnpm add @tanstack/react-query
# or
yarn add @tanstack/react-query
# or
bun add @tanstack/react-query

The library provides a provider i.e Query Client Provider which we need to wrap for the application & we need to initialize Qureyclient and pass it to the provider

import { RouterProvider } from "react-router-dom";
import routes from "./routes.tsx";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import "./App.css"

export const queryClient = new QueryClient();

function App() {
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={routes}></RouterProvider>
</QueryClientProvider>
);
}

export default App;

Tanstack provides Devtools for React query where we can visualize the query, The state of the query Data received from the query, etc. We can install the dev tools by installing @tanstack/react-query-devtools Then Initialize it using the ReactQueryDevtools component.

npm i @tanstack/react-query-devtools
import { RouterProvider } from "react-router-dom";
import routes from "./routes.tsx";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import "./App.css"
export const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={routes}></RouterProvider>
<ReactQueryDevtools initialIsOpen={false} position="bottom"/>
</QueryClientProvider>
);
}

export default App;

How to fetch the Data using the React query

useQuery is a hook provided by React Query for handling data fetching. It abstracts the complexities of manual data fetching and provides a streamlined approach to fetching and managing data in React components. Using useQuery is straightforward. You define a query key, specify the function, and handle the resulting data, loading state, and errors. Let's break it down:

const { data, isLoading, isError, error } = useQuery({
queryKey: ['some-key'],
queryFn: () => axios.get('url'),
});

Features of useQuery

  1. Simplifying Data Fetching: With useQuery, React Query manages the loading state and errors for you behind the scenes. This eliminates the need for boilerplate code, making the codebase cleaner and more maintainable.
  2. Caching and Performance: One of the standout features of React Query is its built-in caching mechanism. It automatically caches data for a specified duration, reducing unnecessary network requests and improving performance. we can customize caching behavior to suit the application’s needs using the ghTime property.
  3. Behind-the-scene data fetching: React query fetches the data behind the scenes whenever The component is Mounted on the Dom and whenever the window focus changes we can configure these properties also refetchOnMount: true/false, refetchOnWindowFocus: true/false.
  4. Real-Time Updates with Polling: React Query makes implementing polling for real-time data updates a breeze. By configuring the refetchIntervaltime interval for polling. we can ensure that our data stays up-to-date without the hassle of manual polling implementations. We can also configure Polling in Background by refetchIntervalBackground: True/false.
  5. Data fetching programmatically: Sometimes we want to trigger the Request on some Dom Events like click, redirect, etc. React-query Provides the refetch Method which we can Execute on some events. To prevent the usequery Execution During the Mount phase we can Declare the Enable to false & Then we can use the refetch Method to trigger the Data fetching.
  6. Stale-Time: Sometimes It is ok to show the old data to the user for a specific duration In that Scenario StaleTime helps we can configure how many seconds/ duration Stale the Data so that the react query will not fetch the updated data from the server till that duration.
  7. Data Transformations: Sometimes, data returned from the server needs to be transformed before rendering. With React Query’s select function, you can easily manipulate the data to fit the application's schema, all within the useQuery hook.
  8. Retry Logic: React Query automatically retries failed requests four times, ensuring robustness and reliability in data fetching logic. This built-in retry mechanism saves you from writing additional error-handling code.
  9. Query by ID: We can pass Id/any value to use query to get the dala by passing to the query function.
const fetchitemsById = (id:string) =>{
return axios.get(`/item${id}`)
})

useQuery ({
querykey:[id],
queryFn:({queryKey})=>fetchitemsById(queryKey)
})

10. Initial Data: useQuery Provides the option to pass the Initial Data till the background fetch happens we can utilize this feature to check the Data is already present in the cached Data if it is present return that Data otherwise undefined if we return undefined It fetches the data from the server.

useQuery({
queryKey:['super-hero', id],
queryFn:({queryKey}) => fetchherobyid(queryKey[i])),
initialData:()=>{
const heros=queryKey.getQueryData (["super-heroes"])
const hero=heroes.data.find((hero) => hero.id==id))
if(hero){
return {
data:hero
}
else{
return undefined;
}
}
}
)

Parallel Queries in React Query

With React Query, fetching multiple sets of data concurrently is a breeze. By simply using multiple useQuery hooks, React Query handles the execution of these queries in parallel behind the scenes. This ensures optimal performance and reduces the need for complex orchestration.

Dynamic Parallel Queries

Need to fetch data for an array of dynamic IDs? React Query has you covered with the useQueries hook. This special hook allows you to define an array of queries dynamically, providing query keys and query functions for each query. The result? An array of query results is ready for consumption.

const queryResults = useQueries({
queries: ids.map(id => ({
queryKey: ["post", id],
queryFn: () => fetchGithubUser(id)
}))
});

Dependent Queries

Sometimes, we need to fetch data that depends on the result of another query. React Query’s useQuery hook makes this a breeze with the enabled property. By setting enabled to false, you can prevent a query from executing on the component mount. Then, you can enable the query based on the result of another query, creating dependent queries effortlessly.

const { isLoading, data } = useQuery({
queryKey: ["github", id],
queryFn: () => axios.get("/users/2")
});

const followersUrl = data?.data.followers_url;

const { isLoading: followersLoading, data: followers } = useQuery({
queryKey: ["github-followers", id],
queryFn: () => axios.get(followersUrl),
enabled: !!followersUrl
});

Exploring Paginated Queries with React Query

When dealing with large datasets, paginated queries become essential for optimizing performance and enhancing user experience. Let’s explore how React Query simplifies paginated data fetching and handling.

Setting Up Mock Data with JSON Server

To simulate paginated data fetching, we can use JSON Server. By defining a db.json file and running JSON Server with specific parameters, we can create a mock API that serves paginated data. Here's how you can set it up:

{
"scripts": {
"start-json": "json-server --watch db.json"
}
}
{
"superheroes": [
{
"id": "1",
"name": "Batman 2",
"alterEgo": "Bruce Wayne"
},
{
"id": "2",
"name": "Superman",
"alterEgo": "Clark Kent"
},
{
"id": "3",
"name": "Wonder Woman",
"alterEgo": "Princess Diana"
},
{
"id": "4",
"name": "Wonder Woman",
"alterEgo": "Princess Diana"
},
{
"id": 1710411211800,
"name": "bahubali",
"alterEgo": "prabhas"
}
],
"colors": [
{
"id": "1",
"name": "Red"
},
{
"id": "2",
"name": "Green"
},
{
"id": "3",
"name": "Blue"
},
{
"id": "4",
"name": "Pink"
},
{
"id": "5",
"name": "Purple"
},
{
"id": "6",
"name": "Black"
},
{
"id": "7",
"name": "White"
},
{
"id": "8",
"name": "Orange"
},
{
"id": "9",
"name": "Yellow"
},
{
"id": "10",
"name": "Grey"
}
]
}

Fetching Paginated Data with React Query

With React Query, fetching paginated data is a breeze. By providing parameters such as _per_page (items per page) and _page (current page) in the query URL, we can retrieve paginated data effortlessly.

const { isLoading, isError, data } = useQuery({
queryKey: ['colors', currentPage],
queryFn: () => fetchColors(currentPage),
placeholderData: previousData => previousData
});

Handling Pagination State

Maintaining a pagination state is crucial for seamless navigation through paginated data. React Query makes this easy by triggering queries based on state updates. By incrementing or decrementing the current page, we can simulate pagination effects and fetch the corresponding data.

const [currentPage, setCurrentPage] = useState<number>(1);

const onPageDecrement = useCallback(() => {
if (currentPage > 1) {
setCurrentPage(prev => prev - 1);
}
}, [currentPage]);

const onPageIncrement = useCallback(() => {
if (currentPage < data?.data.pages) {
setCurrentPage(prev => prev + 1);
}
}, [currentPage, data]);

React Query simplifies paginated data fetching and handling by providing intuitive tools and mechanisms. By leveraging JSON Server for mock data and utilizing React Query’s capabilities, developers can effortlessly implement paginated queries in their applications.

With React Query, pagination becomes a seamless part of the data fetching process, enhancing performance and user experience across the board.

Mastering Infinite Queries with React Query

Infinite scrolling has become a popular user experience pattern, especially for applications dealing with large datasets. React Query simplifies the implementation of infinite queries with its useInfiniteQuery hook. Let's delve into how to harness the power of infinite queries in React applications.

Setting Up Infinite Queries

To begin with infinite queries, we first need to define our query function and configure the useInfiniteQuery hook. This hook requires three essential props: queryKey, queryFn, initialPageParam, and getNextPageParam.

const fetchColors = ({ pageParam }) => {
return axios.get(`url?_per_page=2&_page=${pageParam}`);
};

const {
data,
isError,
isLoading,
fetchNextPage,
hasNextPage
} = useInfiniteQuery({
queryKey: ['colors'],
queryFn: fetchColors,
initialPageParam: 1,
getNextPageParam: (lastPage, allPages) => {
if (allPages.length < lastPage.data.pages) {
return allPages.length + 1;
} else {
return undefined;
}
}
});

Fetching Infinite Data

With useInfiniteQuery, fetching infinite data becomes seamless. The fetchColors function receives the pageParam as an argument, allowing us to fetch the next page of data from the API. By returning the next page's URL or page number based on the API response, we enable infinite scrolling effortlessly.

Rendering Infinite Data

Rendering infinite data is straightforward with React Query. The data object returned by useInfiniteQuery contains an array of pages, each representing a page of data. You can iterate over these pages and render the data in UI.

{data.pages.map(page => (
// Render data from each page here
))}

Triggering Infinite Loading

To trigger infinite loading, we utilize the fetchNextPage function provided by React Query. By checking the hasNextPage prop, we can determine whether there is more data to fetch. If hasNextPage is true, we enable the "Show more" button to fetch the next page of data.

<Button disabled={!hasNextPage} onClick={() => fetchNextPage()}>
Show more
</Button>

Infinite queries offer a seamless way to implement infinite scrolling and efficiently handle large datasets in React applications. With React Query’s useInfiniteQuery hook, fetching, rendering, and loading infinite data becomes a breeze, providing users with a smooth and responsive experience.

Mutations with React Query

Mutations, the process of sending data to the backend, are a crucial aspect of modern web development. With React Query’s useMutation hook, performing mutations becomes a seamless experience. Let's explore how to leverage useMutation to handle mutations effectively in React applications.

Performing Mutations

Performing mutations with React Query is straightforward. By providing the mutation function (mutationFn) and a mutation key (mutationKey), we define the method for sending POST requests to the backend. The useMutation hook returns a mutate function, which we can execute upon form submission, for example:

const { mutate } = useMutation({
mutationFn: addHero,
mutationKey: ["add-superhero"]
});

function onSubmit(data) {
mutate({
...data
});
}

Handling Mutations and Updating Data

After performing a mutation, it’s often necessary to update the local data to reflect the changes made on the server. React Query provides hooks onSuccess to handle mutation success events. We can use these hooks to invalidate queries or update local data accordingly.

onSuccess(data, _, variables, context) {
queryClient.invalidateQueries({
queryKey: ["super-hero"]
});
}

Updating Local Data

Instead of hitting an API to fetch updated data after a mutation, we can update the local data directly with the response from the POST request. This is particularly useful when the POST API returns the data payload submitted. We can use the setQueryData method provided by React Query to update the local data.

onSuccess(data, _, variables, context) {
queryClient.setQueryData(["super-hero"], oldData => {
return {
...oldData,
data: [...oldData.data, data.data]
};
});
}

Optimized way of Updating Data on Mutation

To ensure that the local data reflects the changes made by a mutation optimally, we can update the query data preemptively before sending the POST request. This can be achieved using the onMutate hook provided by React Query. We update the query data with the new item, ensuring that the user sees the changes instantly.

onMutate(newHero) {
const previousData = queryClient.getQueryData(["super-heroes"]);
queryClient.setQueryData(["super-heroes"], oldData => ({
...oldData,
data: [...oldData.data, newHero]
}));
return { previousData };
}

Handling Errors

In the event of a mutation error, it’s crucial to revert the local data to its previous state to maintain consistency. React Query’s onError hook allows us to handle mutation errors gracefully by resetting the query data to its previous state.

onError(_error, _, context) {
queryClient.setQueryData(["super-heroes"], context?.previousData);
}

Handling Settled State

After a mutation has settled, whether it was successful or not, it’s essential to invalidate the query to refetch the latest data from the server. React Query’s onSettled hook provides a way to handle this scenario by invalidating the query data.

onSettled(data, _, error) {
queryClient.invalidateQueries(["super-heroes"]);
}

Optimizing data updates after mutations is crucial for maintaining data consistency and providing a seamless user experience. With React Query’s powerful hooks like onMutate, onError, and onSettled, we can handle mutations effectively and ensure that the local data remains in sync with the server data.

Conclusion

React Query revolutionizes data fetching and management in React applications with its intuitive API and powerful features. By mastering the concepts discussed in this guide, you’ll be able to build React applications that are performant, reliable, scalable, and user-friendly.

So why wait? Dive into React Query's world and unleash React applications' full potential today!

--

--

Karthik Joshi

I am a passionate software engineer with a strong focus on front-end development. I thrive on diving deep into various aspects of software development