GraphQL Mutations and Optimistic UI Updates0%

GraphQL Mutations and Optimistic UI Updates

Beginner12 min read•Updated: Jul 11, 2026
Study MaterialsCheatsheet

GraphQL Mutations and Optimistic UI Updates

While queries read data, Mutations modify server-side resources. In high-performance enterprise applications, waiting 500ms for a network roundtrip before updating a "Like", "Checkmark", or "Status" button creates perceived lag. Optimistic UI updates immediately render expected mutations in the UI before server confirmation, rolling back cleanly if the request fails.


1. Anatomy of useMutation

TSX
const [mutateFunction, { data, loading, error }] = useMutation(MUTATION_DOCUMENT, options);
  • mutateFunction(options): Dispatches the mutation request.
  • refetchQueries: Specifies queries to refetch after mutation succeeds.
  • update(cache, { data }): Directly updates the normalized cache without triggering extra network requests.
  • optimisticResponse: Provides temporary synthetic data written to the cache immediately upon dispatch.

2. Implementing Optimistic Updates

Consider toggling a task's completed status:

TSX
import React from "react";
import { gql, useMutation } from "@apollo/client";
 
const TOGGLE_TASK_MUTATION = gql`
mutation ToggleTask($id: ID!, $completed: Boolean!) {
toggleTask(id: $id, completed: $completed) {
id
completed
__typename
}
}
`;
 
interface TaskItemProps {
task: {
id: string;
title: string;
completed: boolean;
};
}
 
export function TaskItem({ task }: TaskItemProps) {
const [toggleTask, { loading }] = useMutation(TOGGLE_TASK_MUTATION);
 
const handleToggle = () => {
toggleTask({
variables: {
id: task.id,
completed: !task.completed,
},
// 1. OPTIMISTIC RESPONSE: Rendered instantly in 0ms!
optimisticResponse: {
toggleTask: {
id: task.id,
completed: !task.completed,
__typename: "Task",
},
},
// 2. Direct cache manipulation (if not automatically normalized by ID)
update(cache, { data }) {
if (!data?.toggleTask) return;
cache.modify({
id: cache.identify({ __typename: "Task", id: task.id }),
fields: {
completed() {
return data.toggleTask.completed;
},
},
});
},
});
};
 
return (
<div className="flex items-center justify-between p-3 bg-slate-900 border border-slate-800 rounded-lg text-white">
<span className={task.completed ? "line-through text-slate-500" : "text-slate-200"}>
{task.title}
</span>
 
<button
onClick={handleToggle}
className={`px-3 py-1 rounded text-xs font-semibold transition ${
task.completed
? "bg-slate-800 text-slate-400 hover:bg-slate-700"
: "bg-cyan-600 text-white hover:bg-cyan-500"
}`}
>
{task.completed ? "Mark Incomplete" : "Mark Done"}
</button>
</div>
);
}

3. How Apollo Handles Failures

If the server rejects the mutation (e.g. 500 Internal Server Error or permission denied):

  1. 1
    Apollo Client detects the network error.
  2. 2
    It automatically rolls back the optimistic snapshot from the cache.
  3. 3
    The UI instantaneously returns to its previous valid state without manual recovery code!

4. Cache Eviction on Deletion

When deleting records, use cache.evict to purge the deleted entity from memory:

TSX
const [deleteTask] = useMutation(DELETE_TASK_MUTATION, {
update(cache, { data }) {
const normalizedId = cache.identify({ __typename: "Task", id: deletedId });
cache.evict({ id: normalizedId });
cache.gc(); // Garbage collect dangling references
},
});

Interactive Quiz

Practice Quiz

Question 1
What is the primary benefit of an "Optimistic UI Update"?
Question 2
What happens if an optimistic mutation fails due to a network outage or 500 error?
Question 3
Why is __typename mandatory inside optimisticResponse payloads?
Question 4
When should you use cache.evict({ id }) inside a mutation's update callback?
Question 5
What is the trade-off between refetchQueries and manual cache.modify()?
Next Lesson

Real-Time GraphQL Subscriptions with WebSockets

Continue learning with hands-on practice, examples, and exercises in the upcoming topic.

Practice Quiz

Test your understanding of this lesson with 5 questions. Each question has one correct answer.

PrevNext