Learn to use React Query's useMutation hook to handle API data updates, invalidate caches, and implement seamless optimistic UI patterns in your dashboard.
Previously in this course, we explored Caching Strategies with React Query: Optimize Your API Performance to master how data is fetched and stored. While fetching is essential, a real-world dashboard is useless if it can't modify that data.
Today, we shift our focus to mutations. A mutation is any operation that changes server-side state—creating, updating, or deleting records. In React Query, we manage these operations through the useMutation hook, which provides a declarative way to handle side effects, loading states, and the crucial process of data synchronization.
Unlike useQuery, which is designed to pull data into your application, useMutation is designed to push changes out. When you call a mutation, you aren't just firing an API request; you are managing a lifecycle of states: idle, pending, success, and error.
Here is the basic structure of a mutation:
JAVASCRIPTimport { useMutation, useQueryClient } from CE9178">'@tanstack/react-query'; function useUpdateTask() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (updatedTask) => api.patch(CE9178">`/tasks/${updatedTask.id}`, updatedTask), onSuccess: () => { // Invalidate queries to trigger a refetch queryClient.invalidateQueries({ queryKey: [CE9178">'tasks'] }); }, }); }
The mutationFn is where your actual API call lives. The onSuccess callback is the "glue" that tells React Query: "The server has changed, so the cached data for 'tasks' is now stale." By calling invalidateQueries, we force a background refetch, ensuring the UI remains accurate.
Waiting for the server to respond before updating the UI can make your application feel sluggish, especially on high-latency connections. Optimistic updates solve this by updating the UI before the server confirms the change. If the request fails, we roll back the UI to the previous state.
Here is how you handle an optimistic update for our dashboard's task list:
JAVASCRIPTconst mutation = useMutation({ mutationFn: updateTaskApi, onMutate: async (newTask) => { // Cancel outgoing refetches so they don't overwrite our optimistic update await queryClient.cancelQueries({ queryKey: [CE9178">'tasks'] }); // Snapshot the previous value const previousTasks = queryClient.getQueryData([CE9178">'tasks']); // Optimistically update to the new value queryClient.setQueryData([CE9178">'tasks'], (old) => old.map(t => t.id === newTask.id ? { ...t, ...newTask } : t) ); return { previousTasks }; }, onError: (err, newTask, context) => { // Rollback if the mutation fails queryClient.setQueryData([CE9178">'tasks'], context.previousTasks); }, onSettled: () => { // Always refetch after error or success to ensure synchronization queryClient.invalidateQueries({ queryKey: [CE9178">'tasks'] }); }, });
onMutate: Fires immediately. We cancel pending fetches and manually update the cache using setQueryData.previousTasks in the context object so we can recover if things go south.onError: If the request fails, we revert the cache to the snapshot.onSettled: Regardless of the outcome, we refetch to make sure our local cache matches the "source of truth" on the server.Integrate a "Delete Task" button into your dashboard.
useDeleteTask hook using useMutation.onSuccess to invalidate the ['tasks'] query.onMutate logic to remove the task from the cache immediately before the API call finishes, providing an "instant" deletion feel.invalidateQueries, your UI will display stale data. Your app will look like it didn't change at all.onMutate. If you don't save the state before the mutation, you have no way to perform a rollback on error.queryClient.cancelQueries in onMutate. If a background refetch finishes after your optimistic update but before your mutation finishes, it will overwrite your local changes with old data.Mutations are the heartbeat of data synchronization in a dashboard. By using useMutation, you gain control over the full lifecycle of an update. We've learned that invalidateQueries is your primary tool for keeping the cache fresh, while onMutate, onError, and onSettled allow you to provide a snappy, professional user experience through optimistic updates.
Up next: We will dive into Synchronizing Client and Server State to handle more complex scenarios where mutation responses contain partial data that can update our cache without needing a full refetch.
Learn to fetch dashboard metrics, manage loading states, and implement background polling in your React application using React Query for live data.
Read moreMaster state synchronization by learning to trigger refetches, handle mutation responses, and keep your React dashboard in sync with your remote API.
Mutations and Data Updates
Controlled vs Uncontrolled Components
Real-time Form Validation
Schema-based Validation with Zod
Handling Multi-step Forms
Optimizing Form Submissions
Performance Profiling with React DevTools
Refactoring for Scalability
Finalizing Dashboard Data Flow
Deploying the Application
Advanced Hook Composition
Implementing Middleware for State
Advanced Context Patterns
Router Loaders and Data Prefetching
Complex Route Guards
Handling Large Datasets in UI
Testing Hooks and Components
Managing Global Modals
Implementing Keyboard Shortcuts
Optimizing Asset Loading
Internationalization Basics
Managing WebSocket Connections