Mutations
Send mutations and update the cache with their results.
useMutation
Returns a mutate function and its current state. Nothing is sent until you call it.
import { useMutation } from "@zoontek/gql-client";
import { graphql } from "./gql";
const LikePostMutation = graphql(`
mutation LikePost($postId: ID!) {
likePost(postId: $postId) {
post {
id
likeCount
}
}
}
`);
const LikeButton = ({ postId }: { postId: string }) => {
const [state, likePost] = useMutation(LikePostMutation);
return (
<button disabled={state.fetching} onClick={() => likePost({ postId })}>
Like
</button>
);
};The response is written into the normalized cache. Here, every component rendering this post's likeCount re-renders with the new value; no manual cache update needed. That's why a mutation should select the fields it changes, id included.
The state is a discriminated union, narrowed on status:
type MutationState<Data> =
| { fetching: false; status: "idle" }
| { fetching: true; status: "loading" }
| { fetching: false; status: "success"; data: Data }
| { fetching: false; status: "error"; error: ClientError };
function useMutation<Data, Variables>(
mutation: TypedDocumentNode<Data, Variables>,
config?: { connectionUpdates?: GetConnectionUpdate<Data, Variables>[] },
): readonly [MutationState<Data>, (variables: Variables) => Promise<Data>];The mutate function returns a promise resolving with the response data, so you can also handle the result inline:
try {
const data = await likePost({ postId });
} catch (error) {
// error is a ClientError
}If called again before the previous call settles, only the latest call is reflected in state.
Updating connections
Writing the response is enough to update existing objects. But a mutation doesn't know which cached connections should gain or lose an edge. connectionUpdates fills that gap.
Say a post has a paginated commentsConnection, and post is the value read from an earlier useQuery:
const AddCommentMutation = graphql(`
mutation AddComment($postId: ID!, $body: String!) {
addComment(postId: $postId, body: $body) {
comment {
id
body
}
}
}
`);
useMutation(AddCommentMutation, {
connectionUpdates: [
({ data, append }) =>
data.addComment?.comment == null
? undefined
: append(post.commentsConnection, [
{ __typename: "CommentEdge", node: data.addComment.comment },
]),
],
});
const DeleteCommentMutation = graphql(`
mutation DeleteComment($commentId: ID!) {
deleteComment(commentId: $commentId) {
deletedCommentId
}
}
`);
useMutation(DeleteCommentMutation, {
connectionUpdates: [
({ data, variables, remove }) =>
data.deleteComment == null
? undefined
: remove(post.commentsConnection, [variables.commentId]),
],
});Each entry is called with the mutation's data and variables, plus three helpers:
prepend(connection, edges): addsedgesbefore the existing ones.append(connection, edges): addsedgesafter the existing ones.remove(connection, ids): drops the edges whose nodeidis inids.
Return a helper's result to describe the change, or undefined to leave the connection as-is.
type GetConnectionUpdate<Data, Variables> = (config: {
data: Data;
variables: Variables;
prepend: <A>(
connection: Connection<A>,
edges: Edge<A>[],
) => ConnectionUpdate<A>;
append: <A>(
connection: Connection<A>,
edges: Edge<A>[],
) => ConnectionUpdate<A>;
remove: <A>(connection: Connection<A>, ids: string[]) => ConnectionUpdate<A>;
}) => ConnectionUpdate<unknown> | undefined;