Introduction to React Query

Share with a friend:

When it comes to managing data and handling complex asynchronous operations in React applications, developers often face challenges. State management, caching, and handling server interactions can be cumbersome tasks, especially in large-scale applications. This is where React Query comes to the rescue.

React Query is a powerful data-fetching library that simplifies the process of handling asynchronous data in React applications. It provides a simple and intuitive API for fetching, caching, and updating data, making it easier to create responsive and fast user experiences.

In this article, we will cover React Query extensively. The content is useful whether you are a complete beginner or if you already have some prior experience. This article might be too much to read in one sitting, so bookmark it and come back whenever you like. You may also navigate to sections relevant to you by using the table of contents below.


Note: This article covers React Query version 3. The latest version of React Query is version 5 and has minor breaking changes. To learn more, visit here. The contents of this article will still be helpful as the fundamentals of the library have not changed despite major updates.


Table of Contents

1. What is React Query?

React Query, developed by Tanner Linsley, is a JavaScript library designed to handle data-fetching and state management in React applications. It is built on top of the React hooks API, which makes it easy to integrate into any existing or new React project. React Query provides a declarative approach to data-fetching, abstracting away complex asynchronous operations, and making it easier to manage data and its lifecycle.

The library follows a set of principles that contribute to its simplicity and effectiveness:

  • Normalization and Deduplication: React Query ensures that the fetched data is normalized and deduplicated. This means that if multiple components request the same data, it will be fetched only once and shared across all the components, reducing unnecessary network requests.
  • Stale-While-Revalidate (SWR): React Query employs the SWR strategy, which allows components to display stale data while simultaneously fetching fresh data in the background. This approach enhances the user experience by presenting data immediately and updating it as soon as the latest information is available.
  • Automatic Caching: React Query automatically caches the fetched data, reducing the need for developers to manually manage caching logic. This further optimizes the application’s performance and minimizes server requests.
  • Smart Error Handling: The library provides built-in error handling mechanisms, enabling developers to handle errors gracefully and display error states to users when necessary.
  • Pagination and Infinite Loading: React Query offers built-in support for handling paginated data and infinite loading scenarios, streamlining the process of displaying large sets of data without compromising performance.
  • Optimistic Updates: The library allows for optimistic updates, where the UI is updated optimistically before the actual data mutation is confirmed by the server. This creates a smooth and responsive user experience.

2. Key Features of React Query

Before we dive deeper into how to use React Query, let’s explore its key features:

2.1. Declarative Data Fetching

React Query allows developers to fetch data declaratively using React hooks. The useQuery hook is at the heart of React Query and is used to fetch data from a server or any data source. By defining queries as functions, you can easily manage the data flow and avoid the complexities of manual data fetching.

2.2. Caching and Automatic Background Updates

One of the standout features of React Query is its automatic caching mechanism. When a query fetches data, React Query caches the results for future use. If another component needs the same data, React Query will return the cached data, avoiding redundant network requests.

Furthermore, React Query automatically performs background updates, ensuring that the data stays fresh without requiring manual intervention. This approach, combined with the SWR strategy, guarantees an up-to-date user experience without compromising performance.

2.3. Pagination and Infinite Loading

Dealing with paginated data can be challenging, but React Query simplifies the process with its built-in pagination support. The useInfiniteQuery hook is specifically designed to handle infinite loading scenarios, such as infinite scroll, providing a smooth and efficient way to display large datasets.

2.4. Mutations and Optimistic Updates

In addition to fetching data, React Query allows you to handle mutations or updates to the server data with the useMutation hook. Optimistic updates are supported out of the box, enabling you to update the UI optimistically before the server confirms the mutation. This leads to a better user experience and reduces the perceived latency.

2.5. Query State Management

React Query provides various utilities to manage the state of queries and mutations. The onSuccess, onError, and onSettled options allow you to specify how the UI should react when the query is successful, encounters an error, or completes.

2.6. Error Handling

Error handling is a crucial aspect of any data-fetching library, and React Query doesn’t disappoint in this regard. It allows you to gracefully handle errors and display error states to users when something goes wrong during data fetching or mutations.

2.7. DevTools for React Query

To aid in debugging and understanding the behavior of queries and mutations, React Query provides DevTools. These tools offer insights into cache behavior, query/mutation status, and performance metrics, making it easier to optimize and fine-tune your application.

With a good understanding of React Query’s key features, let’s move on to setting up React Query in a project.

3. Installation and Setup

To start using React Query, you’ll need to have a React project set up. If you’re starting a new project, you can create one using Create React App (CRA) or any other preferred boilerplate. For an existing project, ensure that you have React version 16.8.0 or later installed, as React Query relies on React hooks.

To install React Query, you can use npm or Yarn. Open your terminal and run the following command:

# Using npm
npm install react-query

# Using Yarn
yarn add react-query

Note: The commands above are used to install React Query version 3. The latest version of React Query is now version 5. To learn more visit, here.


Once the installation is complete, you can start using React Query in your components. To use the hooks provided by React Query, you need to wrap your application with the QueryClientProvider.

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

const queryClient = new QueryClient();

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

In the code above, we import QueryClient and QueryClientProvider from react-query. We create a new instance of QueryClient and wrap our main component (in this case, <App/>) with the QueryClientProvider, passing the queryClient instance as a prop. With React Query set up in our project, let’s now explore basic data fetching using React Query.

4. Basic Data Fetching with React Query

The useQuery hook is the primary hook provided by React Query for fetching data. It takes a query key (a unique identifier for the query) and a function (usually an API call) that returns the data. Let’s create a simple example to fetch data from an API using useQuery.

import React from 'react';
import { useQuery } from 'react-query';

const fetchData = async () => {
  const response = await fetch('https://api.example.com/data');
  if (!response.ok) {
    throw new Error('Network response was not ok');
  }
  return response.json();
};

const DataComponent = () => {
  const { data, isLoading, error } = useQuery('data', fetchData);

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  return (
    <div>
      {data.map(item => (
        <p key={item.id}>{item.name}</p>
      ))}
    </div>
  );
};

export default DataComponent;

In the example above, we define a fetchData function that fetches data from the specified API endpoint. We then use the useQuery hook to initiate the data fetching process. The first argument to useQuery is the query key, which is a string that uniquely identifies the query. In this case, we use 'data' as the key.

The second argument is the fetchData function, which will be called by React Query to fetch the data. The hook handles the rest, including caching and background updates. The hook returns an object with the data, isLoading, and error properties. We can use these properties to handle different states of the query.

If the data is still loading, we display a “Loading…” message. If there’s an error during the fetch, we display an error message. Otherwise, we render the fetched data as a list of paragraphs.

React Query will automatically cache the data, and if the same query is triggered later, it will use the cached data unless the data becomes stale.

5. Querying with Parameters

In many cases, you might need to fetch data with dynamic parameters, such as filtering data or paginating results. React Query makes it easy to pass parameters to the query function using the queryKey argument. Let’s extend our previous example to fetch data with a parameter.

import React from 'react';
import { useQuery } from 'react-query';

const fetchFilteredData = async (category) => {
  const response = await fetch(`https://api.example.com/data?category=${category}`);
  if (!response.ok) {
    throw new Error('Network response was not ok');
  }
  return response.json();
};

const FilteredDataComponent = ({ category }) => {
  const { data, isLoading, error } = useQuery(['filteredData', category], () => fetchFilteredData(category));

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  return (
    <div>
      {data.map(item => (
        <p key={item.id}>{item.name}</p>
      ))}
    </div>
  );
};

export default FilteredDataComponent;

In this example, we create a new function called fetchFilteredData that takes a category parameter. The category parameter will be used to filter the data from the API. We modify the useQuery hook to use the category prop as part of the query key. The query key is now an array, with the first element being the string 'filteredData' and the second element being the category prop value.

Whenever the category prop changes, React Query will trigger a new fetch with the updated parameter, and the results will be cached separately for each category.

6. Caching and Stale Data

Caching is a crucial feature of React Query that allows you to optimize your application’s performance by storing fetched data and reusing it when needed. React Query provides a default caching mechanism, but you can also customize it to suit your specific requirements.

By default, React Query caches data in memory using a simple JavaScript object. When the same query is made multiple times, the library returns the cached data instead of making redundant network requests. This behavior ensures that your application stays responsive and data-intensive components render quickly.

The caching mechanism also employs the Stale-While-Revalidate (SWR) strategy. When a query is made, React Query will return the cached data immediately (stale), and in the background, it will fetch fresh data (revalidate) from the server. When the new data is available, React Query updates the cached data, and any component using the same query key will receive the latest data.

You can also customize caching behavior by specifying caching options when creating the QueryClient instance. For example, you can set the staleTime option to define how long the cached data remains “fresh” before React Query considers it stale and triggers a background revalidation.

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

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60 * 1000, // 1 minute
    },
  },
});

const fetchData = async () => {
  // Fetch data from the server
};

const DataComponent = () => {
  const { data, isLoading, error } = useQuery('data', fetchData);

  // Rest of the component
};

const App = () => (
  <QueryClientProvider client={queryClient}>
    <DataComponent />
  </QueryClientProvider>
);

export default App;

In the example above, we create a new instance of QueryClient with a default option of staleTime: 60 * 1000, which means that the cached data will be considered stale after 1 minute. After that, React Query will fetch fresh data in the background.

You can also manually invalidate the cache for a specific query using the invalidateQueries function. This function allows you to refetch data for a particular query, which can be useful when you want to ensure the data is up-to-date.

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

const queryClient = new QueryClient();

const fetchData = async () => {
  // Fetch data from the server
};

const updateData = async (newData) => {
  // Send the updated data to the server
};

const DataComponent = () => {
  const { data, isLoading, error } = useQuery('data', fetchData);

  const mutation = useMutation(updateData, {
    onSuccess: () => {
      // Invalidate the 'data' query to refetch fresh data
      queryClient.invalidateQueries('data');
    },
  });

  const handleUpdate = () => {
    const newData = /* ... */; // Get the new data from somewhere
    mutation.mutate(newData);
  };

  // Rest of the component
};

const App = () => (
  <QueryClientProvider client={queryClient}>
    <DataComponent />
  </QueryClientProvider>
);

export default App;

In this example, we use the useMutation hook to perform data updates. In the onSuccess callback of the useMutation options, we call queryClient.invalidateQueries('data') to invalidate the cache for the ‘data’ query. This ensures that the data will be refetched when the update is successful.

7. Pagination and Infinite Loading

In many applications, data is presented in paginated form to improve user experience and reduce the load time of displaying large datasets. React Query provides built-in support for pagination with the usePaginatedQuery hook.

Let’s create an example of paginated data fetching with React Query:

import React from 'react';
import { usePaginatedQuery } from 'react-query';

const fetchPaginatedData = async (page) => {
  const response = await fetch(`https://api.example.com/data?page=${page}`);
  if (!response.ok) {
    throw new Error('Network response was not ok');
  }
  return response.json();
};

const PaginatedDataComponent = () => {
  const [page, setPage] = React.useState(1);
  const { resolvedData, latestData, status } = usePaginatedQuery(['paginatedData', page], fetchPaginatedData);

  React.useEffect(() => {
    if (status === 'success' && !latestData?.hasMore) {
      setPage(1); // Reset to the first page when there's no more data
    }
  }, [latestData, status]);

  return (
    <div>
      {status === 'loading' && <div>Loading...</div>}
      {status === 'error' && <div>Error: {error.message}</div>}
      {status === 'success' && (
        <div>
          {resolvedData.data.map(item => (
            <p key={item.id}>{item.name}</p>
          ))}
          {latestData?.hasMore && (
            <button onClick={() => setPage(old => old + 1)}>Load More</button>
          )}
        </div>
      )}
    </div>
  );
};

export default PaginatedDataComponent;

In this example, we create a new function called fetchPaginatedData, which takes a page parameter representing the current page of data to fetch from the API. The API endpoint is modified to include the page parameter in the URL.

We use the usePaginatedQuery hook, similar to useQuery, but with the difference that it fetches paginated data. The page state variable is managed using React’s useState hook and passed as part of the query key to usePaginatedQuery.

When the component mounts or when the page changes, the hook fetches the data for the specified page. If there’s no more data available (indicated by latestData?.hasMore), we reset the page back to 1 to avoid requesting non-existing pages.

This example demonstrates how React Query makes it easy to implement pagination without worrying about managing the state of the current page or handling loading and error states.

8. Mutations with React Query

So far, we have seen how to fetch data using React Query. However, in real-world applications, data often needs to be updated or mutated. React Query provides the useMutation hook for handling data mutations and updates to the server.

Let’s create an example of data mutation using useMutation:

import React from 'react';
import { useMutation } from 'react-query';

const updateData = async (newData) => {
  const response = await fetch('https://api.example.com/data', {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(newData),
  });
  if (!response.ok) {
    throw new Error('Network response was not ok');
  }
  return response.json();
};

const DataUpdateComponent = () => {
  const [formData, setFormData] = React.useState({ name: '', age: 0 });
  const mutation = useMutation(updateData);

  const handleSubmit = (event) => {
    event.preventDefault();
    mutation.mutate(formData);
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <label>
          Name:
          <input type="text" value={formData.name} onChange={(e) => setFormData({ ...formData, name: e.target.value })} />
        </label>
        <label>
          Age:
          <input type="number" value={formData.age} onChange={(e) => setFormData({ ...formData, age: parseInt(e.target.value) })} />
        </label>
        <button type="submit">Update Data</button>
      </form>
      {mutation.isLoading && <div>Updating...</div>}
      {mutation.isSuccess && <div>Successfully updated data!</div>}
      {mutation.isError && <div>Error: {mutation.error.message}</div>}
    </div>
  );
};

export default DataUpdateComponent;

In this example, we define a updateData function that performs a PUT request to update data on the server. We use the useMutation hook to handle the data update. The mutation object returned by useMutation contains various properties that allow us to track the status of the mutation.

When the form is submitted, we call mutation.mutate(formData) to trigger the update. React Query takes care of handling the mutation and updating the cache if necessary. The status of the mutation can be tracked using properties like isLoading, isSuccess, and isError.

9. Query State Management

React Query provides several options for managing the state of queries and mutations. These options allow you to customize the behavior of queries based on their status.

9.1. onSuccess, onError, and onSettled

The useQuery and useMutation hooks accept options objects with onSuccess, onError, and onSettled properties. These properties are callback functions that are called when the query or mutation is successful, encounters an error, or settles (either successfully or with an error).

import React from 'react';
import { useQuery, useMutation } from 'react-query';

const fetchData = async () => {
  // Fetch data from the server
};

const updateData = async (newData) => {
  // Send the updated data to the server
};

const DataComponent = () => {
  const { data, isLoading, error } = useQuery('data', fetchData);

  const mutation = useMutation(updateData, {
    onSuccess: () => {
      console.log('Mutation successful!');
    },
    onError: (error) => {
      console.error('Mutation error:', error);
    },
    onSettled: (data, error, variables, context) => {
      console.log('Mutation settled:', data, error, variables, context);
    },
  });

  // Rest of the component
};

In the example above, we define onSuccess, onError, and onSettled callback functions for the useMutation hook. These functions will be called when the mutation is successful, encounters an error, or settles, respectively. You can use these callbacks to handle different scenarios and update the UI accordingly.

9.2. Manual Query Refetching

Sometimes, you might want to trigger a query refetch manually, for example, when the user clicks a “Refresh” button. React Query provides a refetch function for this purpose.

import React from 'react';
import { useQuery } from 'react-query';

const fetchData = async () => {
  // Fetch data from the server
};

const DataComponent = () => {
  const { data, isLoading, error, refetch } = useQuery('data', fetchData);

  const handleRefresh = () => {
    refetch();
  };

  // Rest of the component
};
</code>

In this example, we use the refetch function returned by useQuery to manually trigger a refetch of the ‘data’ query when the user clicks the “Refresh” button. This allows you to give users control over when to update the data.

10. Error Handling

Error handling is a crucial aspect of any data-fetching library, and React Query provides mechanisms to handle errors effectively.

10.1. Handling Query Errors

In the examples we’ve seen so far, we’ve used the error property from the query object to handle errors and display error messages in the UI. React Query will automatically set this property when the query encounters an error.

import React from 'react';
import { useQuery } from 'react-query';

const fetchData = async () => {
  const response = await fetch('https://api.example.com/data');
  if (!response.ok) {
    throw new Error('Network response was not ok');
  }
  return response.json();
};

const DataComponent = () => {
  const { data, isLoading, error } = useQuery('data', fetchData);

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  return (
    <div>
      {data.map(item => (
        <p key={item.id}>{item.name}</p>
      ))}
    </div>
  );
};
</code>

In this example, if the network response is not ok (i.e., the API call fails), an Error object is thrown, and React Query sets the error property with the error information. We then use this property to display an error message in the UI.

10.2. Handling Mutation Errors

Similar to queries, you can also handle errors for mutations using the onError callback provided by the useMutation hook.

import React from 'react';
import { useMutation } from 'react-query';

const updateData = async (newData) => {
  const response = await fetch('https://api.example.com/data', {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(newData),
  });
  if (!response.ok) {
    throw new Error('Network response was not ok');
  }
  return response.json();
};

const DataUpdateComponent = () => {
  const [formData, setFormData] = React.useState({ name: '', age: 0 });
  const mutation = useMutation(updateData, {
    onError: (error) => {
      console.error('Mutation error:', error);
    },
  });

  // Rest of the component
};

In this example, we define an onError callback function to handle errors that may occur during the mutation. When the mutation encounters an error, the callback is executed, allowing you to handle the error accordingly.

11. DevTools for React Query

React Query comes with a set of DevTools that provide insights into the cache behavior, query status, and performance metrics of your application. The DevTools can be incredibly useful during development and debugging.

To enable the DevTools, you can install the React Query DevTools package for your project

npm i react-query-devtools

Once installed, you can add the floating icon to show or hide the DevTools to your project.

import { ReactQueryDevtools } from 'react-query-devtools'
 
function App() {
  return (
    <>
      {/* The rest of your application */}
      <ReactQueryDevtools initialIsOpen={false} />
    </>
  )
}

The React Query DevTools allow you to:

  • Inspect the cache, including the cached data and query keys.
  • View query statuses (e.g., loading, success, error) and the timestamps of when they were last updated.
  • See the data returned by queries and the number of times each query has been executed.
  • View performance metrics, such as the number of active and inactive queries.

These insights can help you identify any inefficiencies, caching issues, or unnecessary network requests, ultimately leading to a more optimized and performant application.

12. Tips and Best Practices

As you start using React Query in your projects, consider the following tips and best practices:

12.1. Use Query Keys Wisely

Choose query keys thoughtfully to ensure they are unique and descriptive. Good query keys help React Query identify and cache data effectively. Avoid using objects or arrays as query keys, as they may lead to unexpected caching behavior.

12.2. Prefetch Data

React Query allows you to prefetch data using the prefetchQuery function. Prefetching data in the background can improve the user experience by ensuring data is available when needed, even before the component mounts.

import React from 'react';
import { prefetchQuery } from 'react-query';

const fetchData = async () => {
  // Fetch data from the server
};

// Prefetch the 'data' query in the background
prefetchQuery('data', fetchData);

12.3. Use Query Invalidation Carefully

While the invalidateQueries function can be handy to refetch data when mutations are successful, use it judiciously. Invalidating too many queries can lead to unnecessary network requests and potentially impact performance.

12.4. Configure Query Options

Take advantage of the defaultOptions and queryFnParamsFilter properties when creating the QueryClient instance to configure global query options and prevent unnecessary refetching.

12.5. Optimize Mutations

When using useMutation, you can optimize your application further by using optimistic updates and providing an onMutate callback to handle optimistic updates manually.

12.6. Implement Error Boundaries

React Query does a great job of handling errors, but it’s essential to implement error boundaries in your application to gracefully handle any uncaught errors.

13. Conclusion

React Query’s declarative approach to data-fetching and its built-in caching mechanism significantly improve the performance and responsiveness of your React applications. The ability to handle complex data-fetching scenarios with ease, coupled with the DevTools for debugging and optimizing queries, makes React Query a valuable tool in the React developer’s toolbox.

Share with a friend:

Rajae Robinson

Rajae Robinson is a young Software Developer with over 3 years of work experience building websites and mobile apps. He has extensive experience with React.js and Next.js.

Recent Posts