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));
Installing React Query
Start by installing React Query with npm:
npm install @tanstack/react-query
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')
);
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>
);
};
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>;
}
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>
);
};
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;
Explanation:
Toggling Task Status: We use a
useMutationhook to send a PATCH request to the server, flipping the task'sisDonestatus. On success, the task list is refreshed viaqueryClient.invalidateQueriesto get the updated data.Deleting a Task: Another
useMutationhook is used to send a DELETE request to remove the task. Again, after success, we invalidate the tasks query and display a success message usingtoast.success.Error Handling: Both mutations include error handling that uses
toast.errorto show any errors that occur during the requests.Loading States: The
isPendingandisDeletingflags 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
onSuccessandonErrorallow 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)
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, andForm.jsx. Below is how the refactored code looks:Items.jsx Before Refactor:
Items.jsx After Refactor with Custom Hook (
useFetchTasks):SingleItem.jsx Before Refactor:
SingleItem.jsx After Refactor with Custom Hook (
useUpdateTaskStatus,useDeleteTask):Form.jsx Before Refactor:
Form.jsx After Refactor with Custom Hook (
useCreateTask):What Changed?
We created custom hooks for fetching tasks, creating tasks, updating task status, and deleting tasks:
useFetchTasks: Handles fetching tasks with caching and error handling.useCreateTask: Manages task creation withmutationFn, success handling, and state resetting.useUpdateTaskStatus: Manages toggling the task status (done or not done).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
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 TasksThe
useFetchTaskshook 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:
isPending: Returnstruewhen the request is in progress.data: Contains the fetched data (e.g.,taskList).error: Contains the error details if the request fails.isError: Returnstrueif there was an error in fetching the data.2.
useCreateTask- Creating a TaskThe
useCreateTaskhook handles POST requests for adding new tasks. It manages the creation process, and we can also provide anonSuccesscallback to perform additional actions after the task is created (like clearing the input field).Example usage in a component:
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 StatusThe
useUpdateTaskStatushook handles PATCH requests to update the status of a task (e.g., marking it as complete or incomplete).Example usage in a component:
toggleTaskStatus: This function is called to toggle the task'sisDonestatus.isPending: Used to disable the checkbox while the update is in progress.4.
useDeleteTask- Deleting a TaskThe
useDeleteTaskhook is used to send a DELETE request to the server to remove a task.Example usage in a component:
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
onSuccessfor Additional LogicIn each of the custom hooks, we can pass an
onSuccesscallback to perform additional logic once the mutation is successful. For example, in theuseCreateTaskhook, we usedonSuccessto clear the input field after adding a new task: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.