Apollo 4: Mutations & Optimistic UI
easy⏱ 5 mincourseapollo
The useMutation hook
useMutation returns a tuple: [mutate, { data, loading, error }]. Unlike useQuery, mutations don't execute on render — you call the mutate function when the user takes an action (submit, click, etc.). The second element gives you the mutation's state. You define your mutation with gql just like queries, but use the mutation keyword.
const ADD_TODO = gql`
mutation AddTodo($title: String!) {
addTodo(title: $title) {
id
title
completed
}
}
`;
const [addTodo, { loading }] = useMutation(ADD_TODO);
Build a form with mutation
Define an ADD_TODO mutation that accepts $title: String! and returns id, title, completed. Use useMutation(ADD_TODO) and destructure [addTodo, { loading }]. Create a form with an input and submit handler that calls addTodo({ variables: { title } }). Add an optimisticResponse that fakes the server result with a temporary id and __typename: 'Todo'.
optimisticResponse for instant UI
Pass optimisticResponse to the mutate call to immediately update the cache before the server responds. Apollo writes the optimistic data to cache, re-renders affected components, then swaps in the real server response when it arrives. If the mutation fails, the optimistic data is rolled back automatically. Use a negative temp ID to distinguish optimistic entries: id: -Date.now().
addTodo({
variables: { title: 'Buy milk' },
optimisticResponse: {
addTodo: {
__typename: 'Todo',
id: `temp-${Date.now()}`,
title: 'Buy milk',
completed: false,
},
},
});
Cache update after mutation
By default, Apollo updates cached objects that share the same id and __typename. But for list additions or removals, you need the update callback to manually modify the cache: update(cache, { data }) { const existing = cache.readQuery(...); cache.writeQuery({ query: GET_TODOS, data: { todos: [...existing.todos, data.addTodo] } }); }. Without this, newly created items won't appear in lists.
addTodo({
variables: { title },
update(cache, { data: { addTodo: newTodo } }) {
const existing = cache.readQuery({ query: GET_TODOS });
cache.writeQuery({
query: GET_TODOS,
data: { todos: [...existing.todos, newTodo] },
});
},
});