React Query for Data Fetching

Data fetching is one of the most critical parts of building modern React applications. It involves retrieving data from external sources like APIs and databases and displaying it in the app. However, handling asynchronous data fetching manually in React can be complex due to issues like caching, synchronization, error handling, and ensuring optimal performance. React Query simplifies this process by providing a powerful set of hooks to manage server state and asynchronous data fetching in React apps.

What is React Query?

React Query is a library for server state management in React applications. Unlike React’s built-in state management, which is focused on UI state (e.g., form inputs, component visibility), React Query handles server state, which involves data fetched from an external source (such as an API) and is subject to synchronization, caching, and updating.

Key Features of React Query:

  • Simplified data fetching: React Query simplifies API calls using intuitive hooks (useQuery, useMutation).
  • Automatic caching: React Query automatically caches fetched data, ensuring it is reused efficiently.
  • Background data synchronization: Automatically refetches data in the background when data goes stale or the app reconnects to the network.
  • Error and loading states: Provides built-in support for handling loading and error states.
  • Pagination and infinite queries: Built-in utilities for handling paginated or infinite scrolling data.
  • Optimistic updates: Supports optimistic UI updates, where you update the UI before the server responds.

Setting Up React Query

Installation

To use React Query in your React project, you need to install the library:

				
					npm install @tanstack/react-query

				
			

In addition, to make sure the React Query Devtools are available for debugging and monitoring queries, you can install the React Query Devtools package:

				
					npm install @tanstack/react-query-devtools

				
			

Setting Up QueryClientProvider

React Query requires a QueryClient to manage all queries in the application. You must wrap your app inside a QueryClientProvider to enable React Query functionalities.

				
					// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';

const queryClient = new QueryClient();

ReactDOM.render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>,
  document.getElementById('root')
);

				
			

Explanation:

  • QueryClient: The QueryClient is responsible for managing all queries and mutations.
  • QueryClientProvider: It wraps the application to make React Query available across the app.

Fetching Data with useQuery

The useQuery hook is the heart of React Query’s data fetching capabilities. It is used to fetch data asynchronously from a server and manage its caching, loading, and error states.

Basic Data Fetching with useQuery

Here’s an example of using useQuery to fetch data from a JSON placeholder API.

Example: Basic Data Fetching

				
					// src/components/Posts.js
import React from 'react';
import { useQuery } from '@tanstack/react-query';

const fetchPosts = async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/posts');
  return response.json();
};

function Posts() {
  const { data, error, isLoading } = useQuery(['posts'], fetchPosts);

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>An error occurred: {error.message}</p>;

  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {data.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}

export default Posts;

				
			

Explanation:

  • useQuery(['posts'], fetchPosts): The useQuery hook takes two arguments: a query key (here, 'posts') and a function (fetchPosts) that fetches the data.
  • isLoading: A boolean indicating if the query is still in the loading state.
  • error: Contains the error object if the query failed.
  • data: Contains the fetched data once the query is successful.

Output:

  • While the data is being fetched, “Loading…” is displayed.
  • Once data is fetched, it displays a list of posts.

Caching in React Query

React Query provides automatic caching for your data. Once data is fetched, it is stored in a cache, and subsequent requests for the same data will return the cached version unless explicitly refetched.

How Caching Works

React Query’s cache management is intelligent. If the same query is requested again while the data is still fresh, React Query will serve the data from the cache instead of refetching it from the server.

Example: Demonstrating Caching

				
					function CachedPosts() {
  const { data, isFetching } = useQuery(['posts'], fetchPosts);

  return (
    <div>
      {isFetching && <p>Updating posts...</p>}
      <ul>
        {data.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}

				
			

Explanation:

  • isFetching: Indicates whether a background refetch is in progress (i.e., the query is being updated from the server).
  • If you revisit this component within the cache’s time-to-live (TTL), the data will be served from the cache.

Background Refetching and Stale Data

By default, React Query considers data “stale” after it has been cached for 5 minutes (300,000 milliseconds). Once the data is stale, React Query automatically refetches it when you revisit the component.

Controlling Stale Time

You can customize how long React Query should consider the data fresh using the staleTime option.

Example: Setting a Stale Time

				
					function StalePosts() {
  const { data, isFetching } = useQuery(['posts'], fetchPosts, {
    staleTime: 10000, // Data will be considered fresh for 10 seconds
  });

  return (
    <div>
      {isFetching && <p>Updating posts...</p>}
      <ul>
        {data.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}

				
			

Explanation:

  • staleTime: 10000: Sets the stale time to 10 seconds, meaning the data will be considered fresh for 10 seconds. After that, a background refetch will occur when the component is mounted.

Error Handling in React Query

Handling errors is a crucial part of any data-fetching strategy. React Query provides built-in mechanisms to manage errors seamlessly.

Using the Error Object

React Query automatically returns an error object if the query fails.

Example: Error Handling

				
					function PostsWithError() {
  const { data, error, isLoading } = useQuery(['posts'], async () => {
    const res = await fetch('https://jsonplaceholder.typicode.com/invalid-url');
    if (!res.ok) throw new Error('Network response was not ok');
    return res.json();
  });

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <ul>
      {data.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

				
			

Explanation:

  • If the fetch request fails (due to an invalid URL), React Query returns an error object with a message. The UI can then display the error message appropriately.

Pagination and Infinite Queries

React Query offers built-in support for paginated data, which is common when dealing with large datasets that need to be fetched incrementally.

Pagination with useQuery

To implement pagination, React Query provides options for fetching the next set of data when required.

Example: Paginated Data Fetching

				
					function PaginatedPosts() {
  const [page, setPage] = React.useState(1);

  const fetchPosts = async ({ queryKey }) => {
    const [_key, page] = queryKey;
    const res = await fetch(
      `https://jsonplaceholder.typicode.com/posts?_page=${page}`
    );
    return res.json();
  };

  const { data, isFetching, isPreviousData } = useQuery(
    ['posts', page],
    fetchPosts,
    { keepPreviousData: true }
  );

  return (
    <div>
      {isFetching && <p>Loading posts...</p>}
      <ul>
        {data.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
      <button
        onClick={() => setPage(old => Math.max(old - 1, 1))}
        disabled={page === 1}
      >
        Previous Page
      </button>
      <button onClick={() => setPage(old => old + 1)}>Next Page</button>
    </div>
  );
}

				
			

Explanation:

  • page: Represents the current page number.
  • keepPreviousData: Ensures the previous page’s data remains while fetching the next page’s data.
  • queryKey: The query key now includes page, so React Query refetches data when the page changes.

React Query revolutionizes how data fetching and server-side state management are handled in React applications. From automatic caching, background refetching, and pagination to built-in error handling, it provides a robust, declarative API that greatly simplifies the process. Using React Query allows you to focus more on building the user interface and less on the intricacies of data management, which improves the maintainability and performance of your React applications. Happy Coding!❤️

Table of Contents