Mastering Mutations and Data Updates with React Query
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.
Understanding useMutation
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.
Implementing Optimistic Updates
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'] }); }, });
Why this works:
onMutate: Fires immediately. We cancel pending fetches and manually update the cache usingsetQueryData.- Snapshotting: We return
previousTasksin 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.
Hands-on Exercise
Integrate a "Delete Task" button into your dashboard.
- Create a
useDeleteTaskhook usinguseMutation. - Inside the component rendering the task, trigger the mutation on click.
- Use
onSuccessto invalidate the['tasks']query. - Challenge: Implement the
onMutatelogic to remove the task from the cache immediately before the API call finishes, providing an "instant" deletion feel.
Common Pitfalls
- Forgetting to Invalidate: If you update the server but don't call
invalidateQueries, your UI will display stale data. Your app will look like it didn't change at all. - Ignoring the Context: When using optimistic updates, always return the "snapshot" in
onMutate. If you don't save the state before the mutation, you have no way to perform a rollback on error. - Race Conditions: Always use
queryClient.cancelQueriesinonMutate. If a background refetch finishes after your optimistic update but before your mutation finishes, it will overwrite your local changes with old data.
Recap
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.
Work with me

React & Next.js Dashboard / Admin UI Development
A clean, data-rich dashboard UI in React or Next.js — charts, tables, and real-time data that your users will actually enjoy using.

Headless WordPress + Next.js Frontend Development
Keep WordPress for content, get a lightning-fast Next.js frontend. The best of both worlds — familiar editing, modern speed.