Subchapter 1.8
references/mutations.mdMarkdown12 KBView on GitHub
The useMutation hook is used to execute GraphQL mutations.
import { gql } from "@apollo/client";
import { useMutation } from "@apollo/client/react";
const ADD_TODO = gql`
mutation AddTodo($text: String!) {
addTodo(text: $text) {
id
text
completed
}
}
`;
function AddTodo() {
const [addTodo, { data, loading, error }] = useMutation(ADD_TODO);
return (
<form
onSubmit={(e) => {
e.preventDefault();
const form = e.currentTarget;
const text = new FormData(form).get("text") as string;
addTodo({ variables: { text } });
form.reset();
}}
>
<input name="text" placeholder="Add todo" />
<button type="submit" disabled={loading}>
Add
</button>
{error && <p>Error: {error.message}</p>}
</form>
);
}const [
mutateFunction, // Function to call to execute mutation
{
data, // Mutation result data
loading, // True while mutation is in flight
error, // ApolloError if mutation failed
called, // True if mutation has been called
reset, // Reset mutation state
client, // Apollo Client instance
},
] = useMutation(MUTATION);const [createUser] = useMutation(CREATE_USER, {
variables: {
input: {
name: "Default User",
email: "default@example.com",
},
},
});
// Call with default variables
await createUser();
// Override variables
await createUser({
variables: {
input: {
name: "Custom User",
email: "custom@example.com",
},
},
});Use TypedDocumentNode instead of generic type parameters:
import { gql, TypedDocumentNode } from "@apollo/client";
interface CreateUserData {
createUser: {
id: string;
name: string;
email: string;
};
}
interface CreateUserVariables {
input: {
name: string;
email: string;
};
}
const CREATE_USER: TypedDocumentNode<CreateUserData, CreateUserVariables> = gql`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
`;
const [createUser, { data, loading }] = useMutation(CREATE_USER);
const { data } = await createUser({
variables: {
input: { name: "John", email: "john@example.com" },
},
});
// data.createUser is fully typedfunction CreatePost() {
const [createPost, { loading, error, data, reset }] =
useMutation(CREATE_POST);
if (data) {
return (
<div>
<p>Post created: {data.createPost.title}</p>
<button onClick={reset}>Create another</button>
</div>
);
}
return (
<form onSubmit={handleSubmit}>
<input name="title" disabled={loading} />
<textarea name="content" disabled={loading} />
<button type="submit" disabled={loading}>
{loading ? "Creating..." : "Create Post"}
</button>
{error && (
<div className="error">
<p>Failed to create post: {error.message}</p>
<button onClick={reset}>Try again</button>
</div>
)}
</form>
);
}If you only need the promise without using the hook’s loading/data state, use client.mutate instead:
import { useApolloClient } from "@apollo/client/react";
function CreatePost() {
const client = useApolloClient();
async function handleSubmit(formData: FormData) {
try {
const { data } = await client.mutate({
mutation: CREATE_POST,
variables: {
input: {
title: formData.get("title"),
content: formData.get("content"),
},
},
});
console.log("Created:", data.createPost);
router.push(`/posts/${data.createPost.id}`);
} catch (error) {
console.error("Failed to create post:", error);
}
}
return (
<form
onSubmit={(e) => {
e.preventDefault();
handleSubmit(new FormData(e.currentTarget));
}}
>
...
</form>
);
}If you do use the hook’s state, e.g. because you want to render the loading state, errors or returned data, you can also use the useMutation hook with async..await in your handler:
function CreatePost() {
const [createPost, { loading }] = useMutation(CREATE_POST);
async function handleSubmit(formData: FormData) {
try {
const { data } = await createPost({
variables: {
input: {
title: formData.get("title"),
content: formData.get("content"),
},
},
});
console.log("Created:", data.createPost);
router.push(`/posts/${data.createPost.id}`);
} catch (error) {
console.error("Failed to create post:", error);
}
}
return (
<form
onSubmit={(e) => {
e.preventDefault();
handleSubmit(new FormData(e.currentTarget));
}}
>
<button type="submit" disabled={loading}>
{loading ? "Creating..." : "Create Post"}
</button>
</form>
);
}Optimistic UI immediately reflects the expected result of a mutation before the server responds.
Important: optimisticResponse needs to be a full valid response for the mutation. A partial result might result in subtle errors.
const [addTodo] = useMutation(ADD_TODO, {
optimisticResponse: {
addTodo: {
__typename: "Todo",
id: "temp-id",
text: "New todo",
completed: false,
},
},
});function TodoList() {
const [addTodo] = useMutation(ADD_TODO);
const handleAdd = (text: string) => {
addTodo({
variables: { text },
optimisticResponse: {
addTodo: {
__typename: "Todo",
id: `temp-${Date.now()}`,
text,
completed: false,
},
},
});
};
return <AddTodoForm onAdd={handleAdd} />;
}const [toggleTodo] = useMutation(TOGGLE_TODO, {
optimisticResponse: ({ id }) => ({
toggleTodo: {
__typename: "Todo",
id,
completed: true, // Assume success
},
}),
update: (cache, { data }) => {
// This runs twice: once with optimistic data, once with server data
cache.modify({
id: cache.identify(data.toggleTodo),
fields: {
completed: () => data.toggleTodo.completed,
},
});
},
});const [addTodo] = useMutation(ADD_TODO, {
update: (cache, { data }) => {
// Read existing todos from cache
const existingTodos = cache.readQuery<{ todos: Todo[] }>({
query: GET_TODOS,
});
// Write updated list back to cache
cache.writeQuery({
query: GET_TODOS,
data: {
todos: [...(existingTodos?.todos ?? []), data.addTodo],
},
});
},
});const [deleteTodo] = useMutation(DELETE_TODO, {
update: (cache, { data }) => {
cache.modify({
fields: {
todos: (existingTodos: Reference[], { readField }) => {
return existingTodos.filter(
(todoRef) => readField("id", todoRef) !== data.deleteTodo.id
);
},
},
});
},
});const [deleteUser] = useMutation(DELETE_USER, {
update: (cache, { data }) => {
// Remove the user object from cache entirely
cache.evict({ id: cache.identify(data.deleteUser) });
// Clean up dangling references
cache.gc();
},
});const [createPost] = useMutation(CREATE_POST, {
update: (cache, { data }) => {
// Update author's post count
cache.modify({
id: cache.identify({ __typename: "User", id: data.createPost.authorId }),
fields: {
postCount: (existing) => existing + 1,
posts: (existing, { toReference }) => [
...existing,
toReference(data.createPost),
],
},
});
// Add to feed
cache.modify({
fields: {
feed: (existing, { toReference }) => [
toReference(data.createPost),
...existing,
],
},
});
},
});There are three refetch notations:
refetchQueries: ['getTodos'] - refetches all active getTodos queriesrefetchQueries: [GET_TODOS] - refetches all active queries using this documentrefetchQueries: [{ query: GET_TODOS }, { query: GET_TODOS, variables: { page: 25 } }] - fetches the query, regardless if it’s actively used in the UIconst [addTodo] = useMutation(ADD_TODO, {
// Refetch all active GET_TODOS queries
refetchQueries: ["getTodos"],
// Or: refetchQueries: [GET_TODOS],
});
// Fetch specific query with variables (even if not active)
const [addTodo] = useMutation(ADD_TODO, {
refetchQueries: [{ query: GET_TODOS }, { query: GET_TODO_COUNT }],
});const [addTodo] = useMutation(ADD_TODO, {
refetchQueries: (result) => {
if (result.data?.addTodo.priority === "HIGH") {
return [{ query: GET_HIGH_PRIORITY_TODOS }];
}
return [{ query: GET_TODOS }];
},
});const [addTodo] = useMutation(ADD_TODO, {
refetchQueries: "active", // Refetch all active queries
// Or: 'all' to refetch all queries (including inactive)
});const [addTodo] = useMutation(ADD_TODO, {
refetchQueries: [{ query: GET_TODOS }],
awaitRefetchQueries: true, // Wait for refetch before resolving mutation
});Returning true from onQueryUpdated causes a refetch. Don’t call refetch() manually inside onQueryUpdated, as it won’t retain the query and might cancel it early.
const [addTodo] = useMutation(ADD_TODO, {
update: (cache, { data }) => {
// Update cache...
},
onQueryUpdated: (observableQuery) => {
// Called for each query affected by cache update
console.log(`Query ${observableQuery.queryName} was updated`);
// Return true to refetch
return true;
},
});const [createUser, { loading }] = useMutation(CREATE_USER, {
errorPolicy: "all", // Return both data and errors
});
const { data, errors } = await createUser({
variables: { input },
});
// Handle partial success
if (data?.createUser) {
console.log("User created:", data.createUser);
}
if (errors) {
console.warn("Some errors occurred:", errors);
}const [createUser] = useMutation(CREATE_USER, {
onError: (error) => {
// Handle error globally
toast.error(`Failed to create user: ${error.message}`);
// Log to error tracking service
Sentry.captureException(error);
},
onCompleted: (data) => {
toast.success(`User ${data.createUser.name} created!`);
},
});const [createUser] = useMutation(CREATE_USER, {
errorPolicy: "all",
});
const handleSubmit = async (input: CreateUserInput) => {
const { data, errors } = await createUser({
variables: { input },
});
// Handle GraphQL validation errors
const fieldErrors = errors?.reduce(
(acc, error) => {
const field = error.extensions?.field as string;
if (field) {
acc[field] = error.message;
}
return acc;
},
{} as Record<string, string>
);
if (fieldErrors?.email) {
setEmailError(fieldErrors.email);
}
};