DEV Community

Mohamed Idris
Mohamed Idris

Posted on

React Query: Simplifying Data Fetching in React

When building React apps, handling data fetching can get complicated. That's where React Query comes in, making it easy to fetch, cache, and update data with less code and better performance.

Why Use React Query?

React Query simplifies data fetching and state management in React apps by handling things like:

  • Caching data for better performance
  • Background refetching to keep data fresh
  • Error handling automatically
  • Reducing unnecessary re-renders

React Query handles most of the work for you, making your code cleaner and more efficient.

Common HTTP Methods

Here’s a quick recap of the HTTP methods React Query helps manage:

  • GET: Fetch data from a server
  • POST: Send data to a server to create or update a resource
  • PATCH: Update part of a resource
  • DELETE: Remove a resource

For example, with Axios, a GET request might look like this:

// HTTP GET example
axios
  .get('/api/data')
  .then((response) => console.log(response.data))
  .catch((error) => console.error(error));
Enter fullscreen mode Exit fullscreen mode

Installing React Query

Start by installing React Query with npm:

npm install @tanstack/react-query
Enter fullscreen mode Exit fullscreen mode

Setting Up React Query

First, set up the QueryClient and QueryClientProvider to wrap your app:

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

const queryClient = new QueryClient();

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

Fetching Data with React Query

To fetch data, you can use the useQuery hook, which is perfect for GET requests. Here's how to fetch a list of tasks:

import { useQuery } from '@tanstack/react-query';
import { axiosInstance } from './utils'; // Axios instance setup

const Items = () => {
  // useQuery handles data fetching, caching, and loading/error states
  const { isPending, data, error, isError } = useQuery({
    queryKey: ['tasks'], // Unique key for caching and accessing later
    queryFn: async () => {
      const { data } = await axiosInstance.get('/'); // Extract data from response
      return data;
    },
  });

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

  if (isError) {
    return <p>There was an error: {error.message}</p>;
  }

  return (
    <div className="items">
      {data?.taskList?.map((item) => (
        <SingleItem key={item.id} item={item} />
      ))}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Key Concepts:

  • useQuery: Used for GET requests. It fetches data, handles loading/error states, and caches the result.
  • queryKey: This is the unique key used to identify the query. React Query uses it for caching and background refetching.
  • queryFn: This is the function that fetches the data. It should return a promise (e.g., from Axios).

Handling Errors

React Query makes it easy to handle loading and error states. In the example above, the loading state is managed with isPending, and errors are managed with isError:

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

if (isError) {
  return <p>There was an error: {error.message}</p>;
}
Enter fullscreen mode Exit fullscreen mode

Using useMutation for Create, Update, and Delete

React Query also simplifies POST, PATCH, and DELETE requests using useMutation. Here's an example of creating a task:

import { useMutation } from '@tanstack/react-query';

const createTask = (task) => axiosInstance.post('/', task);

const TaskForm = () => {
  const mutation = useMutation(createTask, {
    onSuccess: () => {
      // Optionally refetch queries or perform other actions
    },
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    mutation.mutate({ title: 'New Task' }); // Trigger the mutation
  };

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit" disabled={mutation.isPending}>
        {mutation.isPending ? 'Creating...' : 'Create Task'}
      </button>
    </form>
  );
};
Enter fullscreen mode Exit fullscreen mode

Using useMutation for Toggle and Delete Actions

React Query’s useMutation is perfect for actions like toggling task status (e.g., marking a task as complete or incomplete) and deleting tasks. In this example, we show how to use useMutation for both PATCH (updating task status) and DELETE (removing a task).

Code Example:

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { axiosInstance } from './utils'; // Axios instance setup
import { toast } from 'react-toastify';

const SingleItem = ({ item }) => {
  const queryClient = useQueryClient();

  // Mutation to toggle task status (mark as done or not done)
  const { isPending, mutate: toggleTaskStatus } = useMutation({
    mutationFn: ({ taskId, taskIsDone }) =>
      axiosInstance.patch(`/${taskId}`, { isDone: !taskIsDone }), // Patch request to update task status
    onSuccess: () => {
      // Invalidate the tasks query to refetch the updated data
      queryClient.invalidateQueries({ queryKey: ['tasks'] });
    },
    onError: (error) => {
      // Show an error toast if the mutation fails
      toast.error(error.response?.data || error.message);
    },
  });

  // Mutation to delete a task
  const { isPending: isDeleting, mutate: deleteTask } = useMutation({
    mutationFn: (taskId) => axiosInstance.delete(`/${taskId}`), // Delete request to remove the task
    onSuccess: () => {
      // Invalidate the tasks query to refetch the updated data
      queryClient.invalidateQueries({ queryKey: ['tasks'] });
      // Show a success toast after deleting the task
      toast.success('Task deleted!');
    },
    onError: (error) => {
      // Show an error toast if the mutation fails
      toast.error(error.response?.data || error.message);
    },
  });

  return (
    <div className='single-item'>
      {/* Checkbox to toggle task status */}
      <input
        type='checkbox'
        checked={item.isDone}
        onChange={() =>
          toggleTaskStatus({
            taskId: item.id,
            taskIsDone: item.isDone,
          })
        }
        disabled={isPending}
      />
      <p
        style={{
          textTransform: 'capitalize',
          textDecoration: item.isDone && 'line-through', // Strike-through if the task is done
        }}
      >
        {item.title}
      </p>
      {/* Delete button to remove the task */}
      <button
        className='btn remove-btn'
        type='button'
        onClick={() => deleteTask(item.id)}
        disabled={isDeleting}
        style={isDeleting ? { opacity: 0.5 } : undefined} // Disable and style button during deletion
      >
        delete
      </button>
    </div>
  );
};

export default SingleItem;
Enter fullscreen mode Exit fullscreen mode

Explanation:

  1. Toggling Task Status: We use a useMutation hook to send a PATCH request to the server, flipping the task's isDone status. On success, the task list is refreshed via queryClient.invalidateQueries to get the updated data.

  2. Deleting a Task: Another useMutation hook is used to send a DELETE request to remove the task. Again, after success, we invalidate the tasks query and display a success message using toast.success.

  3. Error Handling: Both mutations include error handling that uses toast.error to show any errors that occur during the requests.

  4. Loading States: The isPending and isDeleting flags are used to disable the buttons and prevent multiple requests from being sent at the same time.

Key Concepts for useMutation:

  • useMutation: Used for POST, PATCH, and DELETE requests to create, update, or delete resources.
  • mutationFn: The function that performs the mutation (e.g., sending data to the server).
  • Helper options like onSuccess and onError allow you to handle side effects (e.g., refetching queries).

Final Thoughts

React Query simplifies fetching, caching, and syncing data in your React app. It removes the need for handling complex loading and error states manually and improves performance by reducing unnecessary re-renders and network requests.

If you're building a React app that communicates with a backend, React Query is a great tool to make your code cleaner and more efficient.

Check out the official React Query docs to learn more and dive deeper into its features!

Top comments (4)

Collapse
 
edriso profile image
Mohamed Idris

Refactoring with Custom Hooks

In order to keep our code clean and reusable, we refactored the logic into custom hooks. This reduces redundancy and simplifies component files like Items.jsx, SingleItem.jsx, and Form.jsx. Below is how the refactored code looks:

Items.jsx Before Refactor:

import { useQuery } from '@tanstack/react-query';
import { axiosInstance } from './utils';
import SingleItem from './SingleItem';

const Items = () => {
  const { isPending, data, error, isError } = useQuery({
    queryKey: ['tasks'],
    queryFn: async () => {
      const { data } = await axiosInstance.get('/');
      return data;
    },
  });

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

  if (isError) {
    return <p>There was an error...</p>;
  }

  return (
    <div className='items'>
      {data.taskList?.map((item) => (
        <SingleItem key={item.id} item={item} />
      ))}
    </div>
  );
};
export default Items;
Enter fullscreen mode Exit fullscreen mode

Items.jsx After Refactor with Custom Hook (useFetchTasks):

import SingleItem from './SingleItem';
import { useFetchTasks } from './reactQueryCustomHooks';

const Items = () => {
  const { isPending, data, error, isError } = useFetchTasks();

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

  if (isError) {
    return <p>There was an error...</p>;
  }

  return (
    <div className='items'>
      {data.taskList?.map((item) => (
        <SingleItem key={item.id} item={item} />
      ))}
    </div>
  );
};
export default Items;
Enter fullscreen mode Exit fullscreen mode

SingleItem.jsx Before Refactor:

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { axiosInstance } from './utils';
import { toast } from 'react-toastify';

const SingleItem = ({ item }) => {
  const queryClient = useQueryClient();

  const { isPending, mutate: toggleTaskStatus } = useMutation({
    mutationFn: ({ taskId, taskIsDone }) =>
      axiosInstance.patch(`/${taskId}`, { isDone: !taskIsDone }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['tasks'] });
    },
    onError: (error) => {
      toast.error(error.response?.data || error.message);
    },
  });

  const { isPending: isDeleting, mutate: deleteTask } = useMutation({
    mutationFn: (taskId) => axiosInstance.delete(`/${taskId}`),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['tasks'] });
      toast.success('Task deleted!');
    },
    onError: (error) => {
      toast.error(error.response?.data || error.message);
    },
  });

  return (
    <div className='single-item'>
      <input
        type='checkbox'
        checked={item.isDone}
        onChange={() =>
          toggleTaskStatus({ taskId: item.id, taskIsDone: item.isDone })
        }
        disabled={isPending}
      />
      <p style={{ textTransform: 'capitalize', textDecoration: item.isDone && 'line-through' }}>
        {item.title}
      </p>
      <button
        className='btn remove-btn'
        onClick={() => deleteTask(item.id)}
        disabled={isDeleting}
        style={isDeleting ? { opacity: 0.5 } : undefined}
      >
        delete
      </button>
    </div>
  );
};
export default SingleItem;
Enter fullscreen mode Exit fullscreen mode

SingleItem.jsx After Refactor with Custom Hook (useUpdateTaskStatus, useDeleteTask):

import { useUpdateTaskStatus, useDeleteTask } from './reactQueryCustomHooks';

const SingleItem = ({ item }) => {
  const { isPending, toggleTaskStatus } = useUpdateTaskStatus();
  const { isDeleting, deleteTask } = useDeleteTask();

  return (
    <div className='single-item'>
      <input
        type='checkbox'
        checked={item.isDone}
        onChange={() =>
          toggleTaskStatus({
            taskId: item.id,
            taskIsDone: item.isDone,
          })
        }
        disabled={isPending}
      />
      <p
        style={{
          textTransform: 'capitalize',
          textDecoration: item.isDone && 'line-through',
        }}
      >
        {item.title}
      </p>
      <button
        className='btn remove-btn'
        onClick={() => deleteTask(item.id)}
        disabled={isDeleting}
        style={isDeleting ? { opacity: 0.5 } : undefined}
      >
        delete
      </button>
    </div>
  );
};
export default SingleItem;
Enter fullscreen mode Exit fullscreen mode

Form.jsx Before Refactor:

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
import { axiosInstance } from './utils';
import { toast } from 'react-toastify';

const Form = () => {
  const [newItemName, setNewItemName] = useState('');
  const queryClient = useQueryClient();

  const { mutate: createTask, isPending } = useMutation({
    mutationFn: (taskTitle) => axiosInstance.post('/', { title: taskTitle }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['tasks'] });
      toast.success('New task added!');
      setNewItemName('');
    },
    onError: (error) => {
      toast.error(error.response?.data?.msg || error.message);
    },
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    createTask(newItemName);
  };

  return (
    <form onSubmit={handleSubmit}>
      <h4>task bud</h4>
      <div className='form-control'>
        <input
          type='text'
          id='form-input'
          className='form-input'
          value={newItemName}
          onChange={(event) => setNewItemName(event.target.value)}
        />
        <button
          type='submit'
          className='btn'
          disabled={isPending}
          style={isPending ? { opacity: 0.5 } : undefined}
        >
          add task
        </button>
      </div>
    </form>
  );
};
export default Form;
Enter fullscreen mode Exit fullscreen mode

Form.jsx After Refactor with Custom Hook (useCreateTask):

import { useState } from 'react';
import { useCreateTask } from './reactQueryCustomHooks';

const Form = () => {
  const [newItemName, setNewItemName] = useState('');
  const { createTask, isPending } = useCreateTask();

  const handleSubmit = (e) => {
    e.preventDefault();
    createTask(newItemName, {
      onSuccess: () => {
        setNewItemName('');
      },
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <h4>task bud</h4>
      <div className='form-control'>
        <input
          type='text'
          id='form-input'
          className='form-input'
          value={newItemName}
          onChange={(event) => setNewItemName(event.target.value)}
        />
        <button
          type='submit'
          className='btn'
          disabled={isPending}
          style={isPending ? { opacity: 0.5 } : undefined}
        >
          add task
        </button>
      </div>
    </form>
  );
};
export default Form;
Enter fullscreen mode Exit fullscreen mode

What Changed?

We created custom hooks for fetching tasks, creating tasks, updating task status, and deleting tasks:

  1. useFetchTasks: Handles fetching tasks with caching and error handling.
  2. useCreateTask: Manages task creation with mutationFn, success handling, and state resetting.
  3. useUpdateTaskStatus: Manages toggling the task status (done or not done).
  4. useDeleteTask: Handles deleting a task and invalidating the tasks query on success.

By refactoring this way, we reduce code repetition, increase reusability, and improve maintainability. Each component is now more focused on its specific responsibilities, and all data-fetching logic is neatly contained in custom hooks.


Credits: John Smilga's course

Collapse
 
edriso profile image
Mohamed Idris

How to Use Custom Hooks in React Query

We refactored the logic for fetching, creating, updating, and deleting tasks into custom hooks to make the code cleaner and more reusable. Here's a step-by-step explanation of how to use these hooks in your components:

1. useFetchTasks - Fetching Tasks

The useFetchTasks hook is used to fetch data. It simplifies the process of making a GET request to the server and handling loading, error, and data states.

Example usage in a component:

import { useFetchTasks } from './reactQueryCustomHooks';

const Items = () => {
  const { isPending, data, error, isError } = useFetchTasks();

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

  if (isError) {
    return <p>There was an error: {error.message}</p>;
  }

  return (
    <div>
      {data.taskList?.map((item) => (
        <SingleItem key={item.id} item={item} />
      ))}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode
  • isPending: Returns true when the request is in progress.
  • data: Contains the fetched data (e.g., taskList).
  • error: Contains the error details if the request fails.
  • isError: Returns true if there was an error in fetching the data.

2. useCreateTask - Creating a Task

The useCreateTask hook handles POST requests for adding new tasks. It manages the creation process, and we can also provide an onSuccess callback to perform additional actions after the task is created (like clearing the input field).

Example usage in a component:

import { useCreateTask } from './reactQueryCustomHooks';

const Form = () => {
  const [newItemName, setNewItemName] = useState('');
  const { createTask, isPending } = useCreateTask();

  const handleSubmit = (e) => {
    e.preventDefault();
    createTask(newItemName, {
      onSuccess: () => {
        setNewItemName(''); // Clear the input field after the task is created
      },
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={newItemName}
        onChange={(e) => setNewItemName(e.target.value)}
      />
      <button type="submit" disabled={isPending}>
        Add Task
      </button>
    </form>
  );
};
Enter fullscreen mode Exit fullscreen mode
  • createTask(newItemName, { onSuccess }): This is how we trigger the mutation and pass a callback (onSuccess) that runs when the task is successfully created.
  • isPending: Tells us whether the mutation is in progress (e.g., when the task is being created).

3. useUpdateTaskStatus - Updating Task Status

The useUpdateTaskStatus hook handles PATCH requests to update the status of a task (e.g., marking it as complete or incomplete).

Example usage in a component:

import { useUpdateTaskStatus } from './reactQueryCustomHooks';

const SingleItem = ({ item }) => {
  const { isPending, toggleTaskStatus } = useUpdateTaskStatus();

  return (
    <div>
      <input
        type="checkbox"
        checked={item.isDone}
        onChange={() =>
          toggleTaskStatus({ taskId: item.id, taskIsDone: item.isDone })
        }
        disabled={isPending}
      />
      <p>{item.title}</p>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode
  • toggleTaskStatus: This function is called to toggle the task's isDone status.
  • isPending: Used to disable the checkbox while the update is in progress.

4. useDeleteTask - Deleting a Task

The useDeleteTask hook is used to send a DELETE request to the server to remove a task.

Example usage in a component:

import { useDeleteTask } from './reactQueryCustomHooks';

const SingleItem = ({ item }) => {
  const { isDeleting, deleteTask } = useDeleteTask();

  return (
    <div>
      <button onClick={() => deleteTask(item.id)} disabled={isDeleting}>
        Delete Task
      </button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode
  • deleteTask(item.id): This function deletes the task by its ID.
  • isDeleting: Used to disable the delete button while the deletion is in progress.

Using onSuccess for Additional Logic

In each of the custom hooks, we can pass an onSuccess callback to perform additional logic once the mutation is successful. For example, in the useCreateTask hook, we used onSuccess to clear the input field after adding a new task:

createTask(newItemName, {
  onSuccess: () => {
    setNewItemName(''); // Clear the input field after the task is created
  },
});
Enter fullscreen mode Exit fullscreen mode

This allows you to run additional side effects, such as updating the UI or triggering other actions, whenever the mutation is successful.


Conclusion

By using these custom hooks (useFetchTasks, useCreateTask, useUpdateTaskStatus, useDeleteTask), we keep the logic for fetching, creating, updating, and deleting tasks organized and reusable. This reduces redundancy in components and makes the code cleaner and easier to maintain.

Collapse
 
edriso profile image
Mohamed Idris

Why is queryKey an Array in React Query?

When using React Query for data fetching in React applications, the queryKey is crucial for caching and refetching data efficiently. Here's why using an array for queryKey is important:

Why use an array for the queryKey?

  • Unique Cache Key: The queryKey serves as a unique identifier for each query in React Query’s cache. React Query uses this key to store and retrieve cached results. By using an array with static and dynamic values (like 'images' and searchTerm), we create a unique cache entry for each query. This ensures that each search term fetches and caches its own set of data.
  queryKey: ['images', searchTerm]
Enter fullscreen mode Exit fullscreen mode
  • Dynamic Querying:
    By including searchTerm in the queryKey, React Query treats each unique search term as a separate query. When the search term changes (for example, from "cat" to "dog"), React Query knows to refetch the data for the new query, ensuring users always get the most relevant data.

  • Efficient Caching:
    React Query uses the queryKey to cache query results. If the user searches for the same term again (e.g., "cat"), React Query will serve the cached data instead of making another network request. If the search term changes, React Query will refetch the data with the new search term.

How does it work in the code?

Here’s an example showing how React Query uses the queryKey for dynamic queries:

const response = useQuery({
  queryKey: ['images', searchTerm], // Dynamic queryKey based on the searchTerm
  queryFn: async () => {
    const result = await axios.get(`${url}&query=${searchTerm}`);
    return result.data;
  },
});
Enter fullscreen mode Exit fullscreen mode
  • queryKey: ['images', searchTerm]
    This tells React Query to treat the combination of 'images' and searchTerm as a unique key.

    • When searchTerm changes, React Query will refetch the data.
    • When the search term remains the same, React Query uses the cached data, improving performance.
  • Why is this important?

    This setup ensures that React Query only fetches the necessary data. If the searchTerm is the same, the app will use cached results instead of making repeated network requests. When the searchTerm changes, React Query fetches fresh data.

Caching and Performance:

  • Efficient Caching: React Query caches the results of each unique query based on the queryKey. When the user searches for the same term, React Query serves the cached data instantly, which reduces unnecessary requests and speeds up the app.

  • Automatic Refetching: When the searchTerm changes, React Query will refetch the data, ensuring users always see the latest results. This is particularly useful when dealing with dynamic search inputs.

Example in Practice

  1. Initial Search (e.g., "cat"):
    React Query fetches the data for "cat" and caches it with the key ['images', 'cat'].

  2. Subsequent Search (e.g., "dog"):
    When the user changes the search term to "dog", React Query refetches the data for "dog" and caches it under ['images', 'dog'].

  3. Reusing Cached Results:
    If the user searches for "cat" again, React Query will use the cached results for "cat" instead of fetching new data, making the app faster.

React Query Devtools

Using React Query Devtools allows you to inspect how queries are cached and refetched. You’ll notice:

  • Cached Data: When searching for previously searched terms, React Query will use cached data and will not show the loading state unless the data has been invalidated or is stale.
  • Refetching Data: When the searchTerm changes, React Query will trigger a refetch for the new search term.

Tip: If you don’t have React Query Devtools installed yet, you can still confirm the caching and refetching behavior by inspecting the Network Tab in your browser’s developer tools. Here's how:

  • Network Tab: Open the Network tab in your browser’s DevTools, and you’ll see that when you search for the same term again (e.g., "cat"), React Query does not make a new network request, indicating it’s using the cached data. If the search term changes (e.g., from "cat" to "dog"), a new network request will be triggered to fetch the fresh data.

Summary

  • Array as queryKey: Using an array for queryKey helps uniquely identify each query by combining static and dynamic values. This ensures React Query handles caching and refetching correctly.
  • Caching and Performance: The array-based queryKey allows React Query to cache results for unique queries and refetch data when the query changes, improving app performance by reducing redundant requests.
  • Dynamic Fetching: By incorporating dynamic values like searchTerm, React Query ensures the data is always relevant and up to date.

Using queryKey as an array is a simple yet powerful technique to manage dynamic data fetching, making your app more efficient and responsive.

Collapse
 
edriso profile image
Mohamed Idris • Edited

Why use React Query Devtools?

  • Easier Debugging: You can track the status of all your queries in real-time, helping you quickly identify issues like caching problems or refetching failures.
  • Caching Insights: It helps visualize which queries are cached and when React Query uses the cached data, making it easier to understand your app’s performance.
  • Efficiency: By seeing cached results, you can verify that React Query is not unnecessarily refetching data and is performing optimally.

React Query Devtools are invaluable for development and debugging, allowing you to monitor queries and their states directly from your browser.

React Query Devtools