What Is React Query? How To Use useMutation, useQuery, prefetch, devtools

What Is React Query? How To Use useMutation, useQuery, prefetch, devtools

Amaresh Adak

By Amaresh Adak

Introduction

In modern web development, building dynamic and responsive user interfaces is crucial for providing a seamless user experience. However, handling data fetching, caching, and state management can be challenging, especially in complex React applications. This is where React Query useMutation, React Query useQuery, Refetch, and Devtools come to the rescue. 🛡️

In this article, we’ll dive into the world of React Query and explore its essential features like react query useMutation, useQuery, refetch, devtools, and more. We’ll walk through real examples to understand how to use it effectively and unleash its power to simplify data handling in your apps. Let’s get started! 🚀

1. What is React Query? 🤔

React Query is a library that makes data fetching, caching, and state management easy and efficient for React applications. It provides hooks that allow developers to fetch and update data with ease, manage loading and error states, and efficiently cache data for optimized performance. With React Query, you can say goodbye to complex manual data handling and focus on building intuitive user interfaces. 🏗️

2. Installing React Query 📥

To get started with it, we need to install it as a dependency in our project. We can choose between npm or yarn to install it. Let’s see how:

npm install react-query
or
yarn add react-query

Once installed, we can import the necessary functions and hooks into our components to start utilizing them. 📦

3. The Core Concepts of React Query 🌟

Before we delve into using React Query, let’s understand its core concepts that form the foundation of its data-fetching capabilities.

3.1 Queries – React query useQuery

Queries are the backbone of React Query, responsible for fetching and caching data from an API or any other data source. They come with various features like caching, background updates, and automatic retries to ensure the data is always up-to-date. To create a basic query, we use the react query useQuery hook as follows:

import { useQuery } from "react-query"

const ExampleComponent = () => {
  const { data, isLoading, error } = useQuery("todos", fetchTodos)

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

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

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

In this example, the useQuery hook fetches a list of todos and handles the loading and error states, allowing us to focus on rendering the data. 📋

3.2 React Query Mutations

Mutations handle the data modification part of react query useMutation. They are used to perform actions like creating, updating, or deleting data on the server. Mutations can also be optimized with optimistic updates to provide a smooth user experience. Let’s create a mutation that adds a new todo item:

import { useMutation, useQueryClient } from "react-query"

const NewTodoForm = () => {
  const queryClient = useQueryClient()
  const addTodoMutation = useMutation(addTodo)

  const handleAddTodo = async (title) => {
    await addTodoMutation.mutateAsync(title)
    queryClient.invalidateQueries("todos")
  }

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        const formData = new FormData(e.target)
        handleAddTodo(formData.get("title"))
      }}
    >
      <input type="text" name="title" />
      <button type="submit">Add Todo</button>
    </form>
  )
}

In this example, the react query useMutation hook allows us to easily add a new to-do item to the server and invalidate the todos query to reflect the changes. ✍️

3.3 Query Invalidation and Refetching

React Query automatically tracks the dependencies of queries and updates the cached data when required. We can also manually invalidate queries and trigger refetches to ensure fresh data when needed. For example, if we have a “Delete” button for a todo item, we can invalidate the todos query after a successful deletion:

import { useMutation, useQueryClient } from "react-query"

const TodoItem = ({ todo }) => {
  const queryClient = useQueryClient()
  const deleteTodoMutation = useMutation(deleteTodo)

  const handleDeleteTodo = async (id) => {
    await deleteTodoMutation.mutateAsync(id)
    queryClient.invalidateQueries("todos")
  }

  return (
    <li>
      {todo.title} <button onClick={() => handleDeleteTodo(todo.id)}>Delete</button>
    </li>
  )
}

With this approach, when we delete a todo item, the todos query will automatically refetch, keeping the UI up-to-date with the latest data. ♻️

3.4 Query Pagination

When dealing with large datasets, pagination becomes essential. React Query useQuery offers built-in support for paginated queries, making it easier to fetch and manage data in chunks. Let’s

see how to implement pagination for our todo list:

import { useQuery } from "react-query"

const TodoList = () => {
  const { data, isLoading, error, fetchNextPage, hasNextPage } = useQuery("todos", fetchTodos, {
    getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
  })

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

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

  return (
    <>
      <ul>
        {data.pages.map((page, pageIndex) => (
          <React.Fragment key={pageIndex}>
            {page.todos.map((todo) => (
              <li key={todo.id}>{todo.title}</li>
            ))}
          </React.Fragment>
        ))}
      </ul>
      {hasNextPage && <button onClick={() => fetchNextPage()}>Load More</button>}
    </>
  )
}

In this example, we use the fetchNextPage function provided by React Query to load more todo items as the user scrolls or clicks the “Load More” button. 📃

3.5 Cache and Background Updates

Caching is one of the main advantages of React Query. It intelligently stores and updates data in the cache, reducing the number of unnecessary network requests. For instance, if we have a dashboard that displays real-time data, we can enable background updates to keep the data fresh:

import { useQuery } from 'react-query';

const Dashboard = () => {
  const { data, isLoading, error } = useQuery('dashboard', fetchDashboardData, {
    refetchInterval: 10000, // 10 seconds
  });

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

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

  return (
    // Display dashboard data here...
  );
};

With this setup, the dashboard query will automatically refetch every 10 seconds, ensuring the data is always up-to-date without any additional manual handling. 🔄

3.6 Automatic Query Retry

In case of network failures or errors, React Query can be configured to automatically retry failed queries, providing a robust and reliable data-fetching mechanism. For example, if there’s a temporary network issue, React Query will retry fetching the data a few times before considering it a failure:

import { useQuery } from "react-query"

const TodoList = () => {
  const { data, isLoading, error } = useQuery("todos", fetchTodos, {
    retry: 3, // Retry 3 times before showing an error
  })

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

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

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

With automatic query retries, React Query ensures a smooth and uninterrupted user experience, even in challenging network conditions. 🔄🔁

4. Using React Query useQuery Hook 🎣

The useQuery hook is the heart of React Query. It allows us to fetch and manage data from the server effortlessly. Let’s explore how to use this hook effectively with some more real-life examples.

4.1 Fetching Data

To fetch data using react query useQuery, we simply provide a key that represents the query and a function to fetch the data. For example:

import { useQuery } from "react-query"

const ExampleComponent = () => {
  const { data, isLoading, error } = useQuery("todos", fetchTodos)

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

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

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

In this example, we use the key 'todos' to represent the query and the fetchTodos function to retrieve the list of todos. The useQuery hook handles the loading and error states, allowing us to focus on rendering the data.

4.2 Handling Loading and Errors

It provides a straightforward way to handle loading and error states when fetching data. The isLoading and error properties returned by the useQuery hook enable us to display appropriate messages or components based on the data-fetching status. For example:

import { useQuery } from "react-query"

const TodoList = () => {
  const { data, isLoading, error } = useQuery("todos", fetchTodos)

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

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

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

In this code snippet, if the data is still loading, we display a “Loading…” message. If there’s an error during the data-fetching process, we display an error message with the specific error information.

4.3 Accessing Cached Data

One of the most significant advantages of React Query is its built-in caching mechanism. It automatically stores fetched data in the cache and updates it when necessary. This caching feature enhances the overall performance of our application by minimizing redundant API calls. To access cached data, we don’t need to perform any additional fetch requests; we can directly access it from the data property of the query result. For example:

import { useQuery } from "react-query"

const ExampleComponent = () => {
  const { data } = useQuery("todos", fetchTodos)

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

In this code snippet, React Query will automatically fetch data the first time ExampleComponent is rendered and cache it. On subsequent renders, the cached data will be directly accessed from the data property, eliminating the need for redundant fetch calls.

4.4 Updating Cached Data

React Query allows us to update the cached data manually. After updating the data on the server through a mutation, we can use the queryClient.invalidateQueries method to invalidate the corresponding query and trigger a refetch. For example:

import { useMutation, useQueryClient } from "react-query"

const EditTodo = ({ todo }) => {
  const queryClient = useQueryClient()
  const editTodoMutation = useMutation(editTodo)

  const handleEditTodo = async (id, newTitle) => {
    await editTodoMutation.mutateAsync({ id, title: newTitle })
    queryClient.invalidateQueries("todos")
  }

  return (
    <div>
      <input defaultValue={todo.title} />
      <button onClick={() => handleEditTodo(todo.id, newTitle)}>Save</button>
    </div>
  )
}

In this code snippet, when the user edits a todo item and clicks the “Save” button, the handleEditTodo function will trigger the mutation, update the data on the server, and then invalidate the 'todos' query. This will automatically refetch the latest data and update the UI.

4.5 Invalidating Queries

As we saw in the previous example, invalidating queries is a powerful feature of React Query. By manually invalidating a query, we can control when and which data should be refetched from the server. This is particularly useful when we need to ensure that data is always fresh, such as after a mutation or when implementing real-time updates. The queryClient.invalidateQueries method allows us to invalidate queries as needed. For example:

import { useQueryClient } from "react-query"

const TodoList = () => {
  const queryClient = useQueryClient()

  const handleDeleteTodo = async (id) => {
    // Perform the delete operation on the server
    await deleteTodoMutation.mutateAsync(id)

    // Invalidate the 'todos' query to trigger a refetch
    queryClient.invalidateQueries("todos")
  }

  // Rest of the code...
}

In this code snippet, when a todo item is deleted, we use queryClient.invalidateQueries('todos') to invalidate the 'todos' query and trigger a refetch of the updated todo list.

4.6 Query Prefetching

Also provides a prefetching mechanism, allowing us to fetch data in advance before it’s needed. This is beneficial when we know that certain data will be required soon and want to reduce the perceived loading time. We can use the queryClient.prefetchQuery method to prefetch data for a specific query key. For example:

import { useQueryClient } from "react-query"

const PrefetchComponent = () => {
  const queryClient = useQueryClient()

  // Prefetch the 'todos' query
  queryClient.prefetchQuery("todos")

  return <p>This component has prefetched the 'todos' data.</p>
}

In this code snippet, the PrefetchComponent will prefetch the 'todos' data when it mounts. Later, when the actual component that uses the 'todos' data is rendered, React Query will use the prefetched data, reducing the need for an additional fetch request.

5. Using React Query useMutation Hook 🔄

The useMutation hook empowers us to handle data mutations easily. Mutations are operations that modify data on the server, such as creating, updating, or deleting data. React Query’s useMutation hook simplifies the process of performing mutations and handling mutation states. Let’s explore how to use this hook effectively.

5.1 Performing Mutations

To perform mutations using React Query useMutation, we provide a mutation function that carries out the data modification on These states allowing us to create a seamless user experience during mutation operations.

5.3 Optimistic Updates

React Query also supports optimistic updates, a technique to provide immediate feedback to users when performing mutations. Optimistic updates involve updating the local data in the UI optimistically before waiting for the server response. If the server operation fails, its automatically rolls back the optimistic update. This technique creates a smooth user experience, even if the server operation takes some time to complete. Let’s see an example of optimistic updates:

import { useMutation } from "react-query"

const OptimisticTodoForm = ({ todo }) => {
  const updateTodoMutation = useMutation(updateTodo, {
    onMutate: (variables) => {
      // Optimistically update the todo in the cache
      queryClient.setQueryData(["todo", variables.id], (prev) => ({
        ...prev,
        title: variables.title,
      }))

      return variables // Pass the variables to the next phase
    },
    onError: (error, variables) => {
      // Rollback the optimistic update on error
      queryClient.setQueryData(["todo", variables.id], (prev) => ({ ...prev, title: todo.title }))
    },
    onSettled: () => {
      // Optionally refetch the 'todo' query after the mutation is complete
      queryClient.invalidateQueries(["todo", todo.id])
    },
  })

  const handleUpdateTodo = async (id, newTitle) => {
    try {
      // Perform the 'updateTodo' mutation on the server
      await updateTodoMutation.mutateAsync({ id, title: newTitle })
    } catch (error) {
      // Error handling is managed by onMutate and onError
    }
  }

  // Rest of the code...
}

In this code snippet, the onMutate function is called before the actual server mutation. We use it to optimistically update the todo item in the cache, assuming the server operation will be successful. The onError function is called if the server mutation fails. We use it to rollback the optimistic update in case of an error. The onSettled function is called regardless of whether the mutation succeeds or fails. Here, we choose to invalidate the ‘todo’ query to ensure the UI displays the latest data from the server.

Conclusion 🎉

🎉 You’ve reached the end of this blog post. Congratulations! 🎊

You’ve learned what React Query is and how it can help you with data fetching in ReactJs applications. You’ve also learned how to use the useQuery and useMutation hooks to fetch and update data from an API. 🙌

It is a powerful and flexible library that can handle many common scenarios of data fetching. It can improve your app’s performance, user experience, and developer experience. 💯

I hope this blog post was helpful and interesting for you. If you have any questions or feedback, feel free to leave a comment below or contact me on Twitter. 😊

FAQs

1. Q: What is the difference between React Query useMutation and useQuery in React Query?

useMutation is used for data modification, such as creating, updating, or deleting data, while useQuery is used for data fetching and caching.

2. Q: How does React Query handle automatic retries in case of network failures?

It can be configured to automatically retry failed queries with a customizable retry policy, ensuring better data resilience

3. Q: Can React Query be used with other state management libraries like Redux?

Yes, It can be easily integrated with other state management solutions, such as Redux, to handle complex application states effectively.

4. Q: Is It suitable for handling large-scale applications with heavy data requirements?

Yes, React Query’s caching and pagination features make it highly suitable for large-scale applications with extensive data needs.

5. Q: How does It handle cache invalidation when data on the server changes?

Automatically invalidates cached queries when mutations or background updates occur, ensuring the most recent data is always available. ♻️