Data Management in React with React-Query: A Comprehensive Guide
Tags: webdev, react, programming, tutorial
Published: 2023-04-13 11:23:51

React is a popular JavaScript library for building user interfaces, and it's often used to create complex and dynamic web applications. One common challenge when building these applications is managing state across multiple components and handling data fetching and mutations. React-Query is a powerful library that helps solve these challenges by providing a simple and intuitive API for managing server state in React applications.

In this article, we'll explore the basics of React-Query and how it can be used to simplify data fetching and mutations in React applications. We'll cover topics such as querying data with useQuery, mutating data with useMutation, and exploring advanced features like prefetching and polling. By the end of this article, you'll have a solid understanding of how React-Query works and how it can help streamline your development workflow. So let's dive in!

## Getting Started with React-Query

React-Query can be installed via npm or yarn by running the following command:

```
npm install react-query
# or
yarn add react-query
```
Once installed, you can begin using React-Query in your React components by importing the useQuery hook from the react-query package.

## Creating a Basic Query to Fetch Data from an API

To create a basic query to fetch data from an API, we'll use the useQuery hook. This hook takes two arguments: a unique queryKey and a queryFn function that returns a promise containing the data we want to fetch.

Here's an example of using useQuery to fetch data from a fake API that returns a list of users:

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

function Users() {
  const { isLoading, error, data } = useQuery('users', () =>
    fetch('https://jsonplaceholder.typicode.com/users').then((res) =>
      res.json()
    )
  );

  if (isLoading) return <p>Loading...</p>;

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

  return (
    <ul>
      {data.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
```
In this example, we're using the useQuery hook to fetch a list of users from the fake JSONPlaceholder API. We're passing 'users' as the queryKey to uniquely identify this query, and we're using the queryFn to make a GET request to the API and parse the response as JSON. The hook returns an object with three properties: isLoading, error, and data. We're using these properties to handle the loading and error states of the query, and to display the fetched data in a list of users.

## Displaying the Fetched Data in a React Component

Once we've fetched data using useQuery, we can display it in our [React components](https://www.cronj.com/blog/react-js-tutorial/topics/react-component/) just like any other data. In the example above, we're using the data property returned by the useQuery hook to render a list of users in an unordered list.

By default, React-Query will cache the results of each query for a configurable amount of time, so subsequent renders of our component that use the same query will use the cached data instead of refetching from the server. This caching behavior can be customized using various options provided by React-Query, which we'll explore in more detail later in this article.

That's it for getting started with React-Query! In the next section, we'll explore querying data in more depth and learn how to customize query options.

## Querying Data with React-Query

React-Query provides two main hooks for querying data: useQuery and useInfiniteQuery.

**useQuery**
useQuery is used for fetching a single page of data. It takes a queryKey argument and a queryFn function that returns a promise containing the data to be fetched. The queryKey is a unique identifier for the query, and the queryFn is a function that is called when the query is executed.

Here's an example of using useQuery to fetch a list of posts from a fake API:

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

function Posts() {
  const { isLoading, error, data } = useQuery('posts', () =>
    fetch('https://jsonplaceholder.typicode.com/posts').then((res) =>
      res.json()
    )
  );

  if (isLoading) return <p>Loading...</p>;

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

  return (
    <ul>
      {data.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}
```
In this example, we're using useQuery to fetch a list of posts from the fake JSONPlaceholder API. We're passing 'posts' as the queryKey to uniquely identify this query, and we're using the queryFn to make a GET request to the API and parse the response as JSON. We're then displaying the fetched data in a list of post titles.

**useInfiniteQuery**
useInfiniteQuery is used for fetching data in chunks, where each chunk represents a page of data. It takes a queryKey argument and a queryFn function that returns a promise containing the data to be fetched. The queryKey is a unique identifier for the query, and the queryFn is a function that is called with a pageParam argument that represents the next page of data to fetch.

Here's an example of using useInfiniteQuery to fetch a list of comments from a fake API in chunks of 10:

```
import { useInfiniteQuery } from 'react-query';

function Comments() {
  const fetchComments = async (key, nextCursor = 0) => {
    const res = await fetch(
      `https://jsonplaceholder.typicode.com/comments?_start=${nextCursor}&_limit=10`
    );
    const data = await res.json();
    const hasNextPage = data.length === 10;
    const nextPageCursor = hasNextPage ? nextCursor + 10 : undefined;
    return { data, nextPageCursor };
  };

  const {
    isLoading,
    isError,
    data,
    error,
    fetchNextPage,
    hasNextPage,
  } = useInfiniteQuery('comments', fetchComments, {
    getNextPageParam: (lastPage, allPages) =>
      lastPage.nextPageCursor || undefined,
  });

  if (isLoading) return <p>Loading...</p>;

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

  return (
    <>
      {data.pages.map((page) => (
        <ul key={page.nextPageCursor}>
          {page.data.map((comment) => (
            <li key={comment.id}>{comment.name}</li>
          ))}
        </ul>
      ))}
      {hasNextPage && (
        <button onClick={() => fetchNextPage()}>Load More</button>
      )}
    </>
  );
}
```
In this example, we're using `useInfinite Queryto fetch comments from the JSONPlaceholder API in chunks of 10. We define afetchCommentsfunction that takes anextCursor` argument to fetch the next page of comments from the API. This function returns an object containing the fetched data and the cursor for the next page of data, which is used to fetch the next page of comments.

We're then using useInfiniteQuery to execute the fetchComments function and fetch comments in chunks of 10. We're passing 'comments' as the queryKey to uniquely identify this query, and we're using the getNextPageParam option to get the cursor for the next page of data.

We're displaying the fetched comments in a list, and we're using the fetchNextPage function and hasNextPage flag to allow the user to load more comments.

## Customizing Query Options

React-Query provides a number of options to customize query behavior. Here are some of the most common options:
1. cacheTime: The time in milliseconds that the query should be cached. Default is 5 minutes.
2. staleTime: The time in milliseconds that the cached data should be considered fresh. Default is 0, meaning the data will never be considered stale.
3. refetchInterval: The time in milliseconds that the query should automatically refetch data. Default is false, meaning no automatic refetching will occur.
4. retry: The number of times the query should be retried if it fails. Default is 3.
5. onSuccess: A function that is called when the query is successful.
6. onError: A function that is called when the query fails.

## Handling Loading and Error States

React-Query provides isLoading, isError, data, and error properties to help handle loading and error states in React components. The isLoading property is true while the query is fetching data, the isError property is true if the query fails, the data property contains the fetched data, and the error property contains information about the error if the query fails.

Here's an example of how to use these properties to handle loading and error states in a React component:

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

function UserProfile({ userId }) {
  const { isLoading, isError, data, error } = useQuery(
    ['user', userId],
    () => fetch(`https://jsonplaceholder.typicode.com/users/${userId}`).then((res) =>
      res.json()
    )
  );

  if (isLoading) return <p>Loading...</p>;

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

  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.email}</p>
    </div>
  );
}
```
In this example, we're using useQuery to fetch user data from the JSONPlaceholder API. We're passing an array containing the userId as the queryKey to uniquely identify this query, and we're using the queryFn to make a GET request to the API and parse the response as JSON.

We're then using the isLoading and isError properties to conditionally render a loading message or an error message if the query fails. If the query succeeds, we're displaying the fetched user data in a React component.

## Mutating Data with React-Query

React-Query provides a useMutation hook to handle data mutations, such as creating, updating, or deleting data. Here's an example of how to use useMutation to create a new comment:

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

function NewCommentForm({ postId }) {
  const [text, setText] = useState('');
  const [createComment, { isLoading, isError, error }] = useMutation(
    (newComment) =>
      fetch(`https://jsonplaceholder.typicode.com/comments`, {
        method: 'POST',
        body: JSON.stringify(newComment),
        headers: {
          'Content-type': 'application/json; charset=UTF-8',
        },
      }).then((res) => res.json()),
    {
      onSuccess: () => {
        queryCache.invalidateQueries(['comments', postId]);
      },
      onError: (error) => {
        console.error('Error creating comment:', error);
      },
    }
  );

  const handleSubmit = (event) => {
    event.preventDefault();

    createComment({ postId, text });
    setText('');
  };

  return (
    <form onSubmit={handleSubmit}>
      <textarea value={text} onChange={(event) => setText(event.target.value)} />
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Loading...' : 'Submit'}
      </button>
      {isError && <p>An error occurred: {error.message}</p>}
    </form>
  );
}
```
In this example, we're using useMutation to handle the creation of a new comment. We define a createComment function that takes a newComment argument and sends a POST request to the JSONPlaceholder API to create a new comment. We're using the onSuccess option to invalidate the cache for the comments query when the mutation succeeds, and the onError option to handle errors if the mutation fails.

We're then using the createComment function to handle the form submission when the user submits a new comment. We're displaying a loading message while the mutation is in progress, and an error message if the mutation fails.

## Customizing Mutation Options

React-Query provides several options to customize mutation behavior. Here are some of the most common options:

1. onSuccess: A function that is called when the mutation is successful.
2. onError: A function that is called when the mutation fails.
3. optimisticUpdate: A function that updates the cache optimistically before the mutation is complete.
4. retry: The number of times the mutation should be retried if it fails. Default is 3.
5. retryDelay: The time in milliseconds to wait before retrying the mutation. Default is 0.
6. mutationKey: A unique identifier for the mutation.
7. mutationFn: The function that performs the mutation.

## Handling Loading and Error States

React-Query provides isLoading, isError, data, and error properties to help handle loading and error states in React components. The isLoading property is true while the mutation is in progress, the isError property is true if the mutation fails, the data property contains the result of the mutation, and the error property contains information about the error if the mutation fails.

In the example above, we're using the isLoading and isError properties to conditionally render a loading message or an error message. We're also using the onError option to log the error to the console when the mutation fails.

Overall, useMutation provides a convenient way to handle data mutations in React components, and React-Query provides a powerful set of options to customize mutation behavior and handle loading and error states. By using useMutation, you can manage data mutations in a declarative and composable way, without having to write complex boilerplate code.

In addition to the useMutation hook, React-Query also provides other useful hooks, such as useQueryClient to get access to the query cache, and useIsFetching to get the current number of active queries.

By combining these hooks with the query and mutation options provided by React-Query, you can create efficient and flexible data fetching and mutation patterns in your React applications.

## Advanced Features of React-Query

React-Query offers several advanced features that can enhance the performance and flexibility of your application.

**Prefetching**
Prefetching is the process of fetching data in advance of when it's actually needed, in order to improve the user experience by reducing the perceived load time of the application. React-Query provides a built-in prefetchQuery method that allows you to prefetch data for a given query key. You can use this method in combination with the useQuery hook to prefetch data for a specific query when the user hovers over a link or button that will trigger that query.

**Polling**
Polling is the process of regularly fetching data from the server at a fixed interval. React-Query provides a built-in useQuery option called refetchInterval that allows you to automatically poll for new data at a specified interval. This can be useful for real-time data, such as chat applications or stock prices, where you want to update the data on the screen without requiring the user to manually refresh.

## Integration with Other Libraries and Frameworks

React-Query can be used in conjunction with other popular libraries and frameworks, such as Redux and Next.js. When using React-Query with Redux, you can use the useQueryClient hook to get access to the query cache and dispatch actions to Redux when data changes. When using React-Query with Next.js, you can use the useQuery and useMutation hooks in your pages and components to fetch and mutate data on both the client and server.

## Best Practices for Performance Optimization

To optimize performance with React-Query, there are a few best practices to keep in mind:

1. Use query keys wisely: The query key is used to identify a specific query, and it's important to choose a key that is unique and stable. Using dynamic query keys, such as IDs or timestamps, can result in unnecessary refetching of data.
2. Use caching effectively: React-Query provides a built-in caching mechanism that can help reduce the number of network requests made by your application. Be sure to use cache time and stale time options effectively to ensure that your data is always up-to-date without unnecessary refetching.
3. Avoid over-fetching: Over-fetching is the process of fetching more data than is actually needed by your application. This can result in slower load times and increased network traffic. Be sure to only fetch the data that is necessary for a given component or page.
4. Use server-side rendering: Server-side rendering can improve the performance and SEO of your application by pre-rendering the HTML on the server before sending it to the client. React-Query provides built-in support for server-side rendering with frameworks like Next.js.

In conclusion, React-Query provides a powerful and flexible set of features for fetching and mutating data in your React applications. By using advanced features like prefetching and polling, and following best practices for performance optimization, you can create fast and efficient applications that provide a great user experience.

## Conclusion 

In conclusion, React-Query is a powerful and flexible library for fetching and mutating data in your React applications. Its declarative API and built-in caching mechanism make it easy to manage complex data requirements, while its advanced features like prefetching and polling allow you to create efficient and responsive applications.

By using React-Query in your projects, you can reduce the amount of boilerplate code needed for data fetching and mutation, and simplify your application architecture. Additionally, React-Query's integration with other popular libraries and frameworks, such as Redux and Next.js, makes it a versatile choice for a wide range of applications.

If you're looking to build a React application with advanced data requirements, hire [react agency](https://www.cronj.com/react/react-development-agency) who are experienced in using React-Query to manage data fetching and mutation. By working with experienced developers, you can ensure that your application is optimized for performance and provides a great user experience.

Overall, React-Query is a valuable addition to any React developer's toolkit, and can help you create fast, efficient, and responsive applications with minimal effort.

## Reference

1. https://www.npmjs.com/package/react-query