@apollo/client
Version:
A fully-featured caching GraphQL client.
550 lines (459 loc) • 12.1 kB
Markdown
# Mutations Reference
## Table of Contents
- [useMutation Hook](#usemutation-hook)
- [Mutation Variables](#mutation-variables)
- [Loading and Error States](#loading-and-error-states)
- [Optimistic UI](#optimistic-ui)
- [Cache Updates](#cache-updates)
- [Refetch Queries](#refetch-queries)
- [Error Handling](#error-handling)
## useMutation Hook
The `useMutation` hook is used to execute GraphQL mutations.
### Basic Usage
```tsx
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>
);
}
```
### Return Tuple
```typescript
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);
```
## Mutation Variables
### Variables in Options
```tsx
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",
},
},
});
```
### TypeScript Types
Use `TypedDocumentNode` instead of generic type parameters:
```typescript
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 typed
```
## Loading and Error States
### Handling in UI
```tsx
function 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>
);
}
```
### Async/Await Pattern
If you only need the promise without using the hook's loading/data state, use `client.mutate` instead:
```tsx
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:
```tsx
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
Optimistic UI immediately reflects the expected result of a mutation before the server responds.
### Basic Optimistic Response
**Important**: `optimisticResponse` needs to be a full valid response for the mutation. A partial result might result in subtle errors.
```tsx
const [addTodo] = useMutation(ADD_TODO, {
optimisticResponse: {
addTodo: {
__typename: "Todo",
id: "temp-id",
text: "New todo",
completed: false,
},
},
});
```
### Dynamic Optimistic Response
```tsx
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} />;
}
```
### Optimistic Response with Cache Update
```tsx
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,
},
});
},
});
```
## Cache Updates
### Using update Function
```tsx
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],
},
});
},
});
```
### cache.modify
```tsx
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
);
},
},
});
},
});
```
### cache.evict
```tsx
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();
},
});
```
### Updating Related Queries
```tsx
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,
],
},
});
},
});
```
## Refetch Queries
### Basic Refetch
There are three refetch notations:
- **String**: `refetchQueries: ['getTodos']` - refetches all active `getTodos` queries
- **Query document**: `refetchQueries: [GET_TODOS]` - refetches all active queries using this document
- **Object**: `refetchQueries: [{ query: GET_TODOS }, { query: GET_TODOS, variables: { page: 25 } }]` - **fetches** the query, regardless if it's actively used in the UI
```tsx
const [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 }],
});
```
### Conditional Refetch
```tsx
const [addTodo] = useMutation(ADD_TODO, {
refetchQueries: (result) => {
if (result.data?.addTodo.priority === "HIGH") {
return [{ query: GET_HIGH_PRIORITY_TODOS }];
}
return [{ query: GET_TODOS }];
},
});
```
### Refetch Active Queries
```tsx
const [addTodo] = useMutation(ADD_TODO, {
refetchQueries: "active", // Refetch all active queries
// Or: 'all' to refetch all queries (including inactive)
});
```
### awaitRefetchQueries
```tsx
const [addTodo] = useMutation(ADD_TODO, {
refetchQueries: [{ query: GET_TODOS }],
awaitRefetchQueries: true, // Wait for refetch before resolving mutation
});
```
### onQueryUpdated
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.
```tsx
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;
},
});
```
## Error Handling
### Error Policy
```tsx
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);
}
```
### onError Callback
```tsx
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!`);
},
});
```
### Field-Level Errors
```tsx
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);
}
};
```