React JS: Data Fetching and Caching -> Mutations
Introduction to Mutations
While fetching data (queries) is about reading data, mutations are about writing or modifying data. This includes creating, updating, and deleting data on the server. Like queries, handling mutations effectively in React requires careful consideration of state management, optimistic updates, and error handling.
Why Mutations Need Special Handling
Mutations present unique challenges:
- State Synchronization: After a mutation, your client-side state needs to reflect the changes made on the server. Simply refetching all related data can be inefficient and lead to a poor user experience.
- Optimistic Updates: To provide a more responsive UI, you might want to immediately update the UI as if the mutation succeeded, even before the server confirms it. This is called an optimistic update. You need to be prepared to revert these changes if the mutation fails.
- Error Handling: Mutations can fail. You need robust error handling to inform the user and potentially revert optimistic updates.
- Cache Invalidation: When data is mutated, any cached data that depends on that data becomes stale. You need to invalidate or update the cache accordingly.
Implementing Mutations with fetch (Basic Example)
Let's illustrate a simple mutation using the built-in fetch API. This example will focus on deleting a task.
import React, { useState } from 'react';
function TaskList({ tasks, onDeleteTask }) {
return (
<ul>
{tasks.map(task => (
<li key={task.id}>
{task.title}
<button onClick={() => onDeleteTask(task.id)}>Delete</button>
</li>
))}
</ul>
);
}
function App() {
const [tasks, setTasks] = useState([
{ id: 1, title: 'Learn React' },
{ id: 2, title: 'Build a Project' },
]);
const handleDeleteTask = async (taskId) => {
try {
const response = await fetch(`/api/tasks/${taskId}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
// Optimistic Update: Remove the task from the state immediately
setTasks(prevTasks => prevTasks.filter(task => task.id !== taskId));
} catch (error) {
console.error('Error deleting task:', error);
// Revert Optimistic Update (if necessary - not shown in this simple example)
alert('Failed to delete task. Please try again.');
}
};
return (
<div>
<h1>Task List</h1>
<TaskList tasks={tasks} onDeleteTask={handleDeleteTask} />
</div>
);
}
export default App;
Explanation:
handleDeleteTaskFunction: This asynchronous function handles the deletion logic.fetchRequest: It usesfetchto send aDELETErequest to the server endpoint/api/tasks/${taskId}.- Error Handling: It checks
response.okto ensure the request was successful. If not, it throws an error. - Optimistic Update: Before waiting for the server's response, it updates the
tasksstate by filtering out the deleted task. This provides immediate feedback to the user. - Error Reversion (Not Shown): In a more complex application, you'd need to revert the optimistic update if the
fetchrequest fails. This might involve storing the original state before the update and restoring it in thecatchblock.
Using Libraries for Mutation Management (React Query & SWR)
While the fetch example demonstrates the core concepts, libraries like React Query and SWR significantly simplify mutation handling, especially for complex applications.
1. React Query
React Query provides a dedicated useMutation hook.
import { useMutation, useQueryClient } from '@tanstack/react-query';
function App() {
const queryClient = useQueryClient();
const deleteTask = async (taskId) => {
const response = await fetch(`/api/tasks/${taskId}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json(); // Return data if needed
};
const { mutate, isLoading, error } = useMutation({
mutationFn: deleteTask,
onSuccess: () => {
// Invalidate the query to refetch data
queryClient.invalidateQueries({ queryKey: ['tasks'] });
},
});
const handleDeleteTask = (taskId) => {
mutate(taskId);
};
// ... rest of your component
}
Explanation:
useMutationHook: This hook takes amutationFn(the function that performs the mutation) and optional callbacks for success, error, and other events.mutateFunction: This function triggers the mutation.onSuccessCallback: This callback is executed when the mutation succeeds. Here, we usequeryClient.invalidateQueriesto invalidate the cache for thetasksquery. This forces React Query to refetch the data, ensuring the UI is up-to-date.isLoadinganderror: These properties provide information about the mutation's status.
2. SWR
SWR also offers a useSWRMutation hook.
import useSWRMutation from 'swr/mutation';
function App() {
const deleteTask = async (taskId) => {
const response = await fetch(`/api/tasks/${taskId}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
};
const { mutate, isLoading, error } = useSWRMutation(
'/api/tasks/:id', // Endpoint pattern
deleteTask,
{
onSuccess: () => {
// SWR automatically refetches the associated key
},
}
);
const handleDeleteTask = (taskId) => {
mutate(taskId);
};
// ... rest of your component
}
Explanation:
useSWRMutationHook: Similar to React Query, this hook takes a mutation function and optional configuration.- Endpoint Pattern: SWR uses an endpoint pattern (
/api/tasks/:id) to identify the resource being mutated. - Automatic Refetching: SWR automatically refetches the associated key when a mutation succeeds, simplifying cache invalidation.
Optimistic Updates with React Query/SWR
Both React Query and SWR provide mechanisms for optimistic updates. The general approach is:
- Update the UI immediately: Modify the local state as if the mutation succeeded.
- Call
mutate: Trigger the mutation. - Revert on Error: If the mutation fails, revert the UI to its original state.
React Query's onMutate callback is particularly useful for optimistic updates. SWR's onError callback is used for reverting.
Best Practices for Mutations
- Use a dedicated mutation hook: Keep mutation logic separate from your components.
- Handle errors gracefully: Provide informative error messages to the user.
- Consider optimistic updates: Improve the user experience by providing immediate feedback.
- Invalidate or update the cache: Ensure the UI reflects the latest data.
- Use a library like React Query or SWR: Simplify mutation management and improve code maintainability.
- Server-Side Validation: Always validate mutations on the server to prevent data inconsistencies.
This provides a comprehensive overview of mutations in React, covering basic implementation with fetch and advanced techniques using React Query and SWR. Remember to choose the approach that best suits the complexity of your application.