Back to Blog
Lesson 24 of the Intermediate React: Hooks, State & Data Patterns course
ReactJune 26, 20264 min read

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.

ReactReact QueryMutationsFrontendState Managementjavascript

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:

JAVASCRIPT
import { 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:

JAVASCRIPT
const 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:

  1. onMutate: Fires immediately. We cancel pending fetches and manually update the cache using setQueryData.
  2. Snapshotting: We return previousTasks in the context object so we can recover if things go south.
  3. onError: If the request fails, we revert the cache to the snapshot.
  4. 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.

  1. Create a useDeleteTask hook using useMutation.
  2. Inside the component rendering the task, trigger the mutation on click.
  3. Use onSuccess to invalidate the ['tasks'] query.
  4. Challenge: Implement the onMutate logic 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.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.

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.

Similar Posts