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 (2)

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.