Mutations
The useMutation hook wraps a GraphQL mutation and returns a tuple of [mutate, pending]. Call mutate with your variables to execute it.
import { graphql, useMutation } from '$houdini'
export function AddCommentForm({ postId }: { postId: string }) { const [mutate, pending] = useMutation(graphql(` mutation AddComment($postId: ID!, $body: String!) { addComment(postId: $postId, body: $body) { id body } } `))
async function handleSubmit(e) { e.preventDefault() await mutate({ variables: { postId, body: e.target.body.value } }) }
return ( <form onSubmit={handleSubmit}> <textarea name="body" /> <button type="submit" disabled={pending}> {pending ? 'Submitting...' : 'Add comment'} </button> </form> )}import { graphql, useMutation } from '$houdini'
export function AddCommentForm({ postId }) { const [mutate, pending] = useMutation(graphql(` mutation AddComment($postId: ID!, $body: String!) { addComment(postId: $postId, body: $body) { id body } } `))
async function handleSubmit(e) { e.preventDefault() await mutate({ variables: { postId, body: e.target.body.value } }) }
return ( <form onSubmit={handleSubmit}> <textarea name="body"/> <button type="submit" disabled={pending}> {pending ? 'Submitting...' : 'Add comment'} </button> </form>)}pending is true from the moment mutate is called until the server responds.
Error Handling
Mutations always throw on error; unlike queries, there’s no opt-in. Wrap calls in try/catch and inspect the RuntimeGraphQLError:
import { graphql, useMutation, RuntimeGraphQLError } from '$houdini'
const [mutate, pending] = useMutation(graphql(`...`))
try { await mutate({ variables: { ... } })} catch (e) { if (e instanceof RuntimeGraphQLError) { // e.message is the joined error messages // e.raw is the full errors array from the GraphQL response console.error(e.raw) }}Cache Updates
After a successful mutation, Houdini automatically updates any cached fields that appear in the mutation’s response. If the mutation returns a type that’s already in the cache, fields are merged and any components watching those fields re-render.
For mutations that add or remove items from lists, see Updating Lists.
Refetching Changed Data
Most of the time, you’re best off including the changed fields in the mutation itself,
either by requesting them directly or by spreading the fragments your components already
define. That keeps everything to a single round trip and gives the fastest experience.
Sometimes that isn’t possible, though, or it gets unwieldy. In those cases you can use the
@refetch directive, which tells Houdini to find the queries that depend on the returned
entity and refetch them.
mutation FavoriteBook($id: ID!) { favoriteBook(id: $id) { book @refetch { id } }}A few things to keep in mind:
@refetchgoes on the field that returns the entity, not on itsid. That’s how Houdini knows which record changed.- It also works on a field that returns a list of entities, in which case every record in the list is refreshed.
- It can’t be combined with
@listor@paginate, which already keep their data in sync.
Optimistic Updates
Pass an optimisticResponse to apply an immediate cache update before the server responds. If the mutation fails, the optimistic update is rolled back:
await mutate({ variables: { postId, body: 'Great post!' }, optimisticResponse: { addComment: { id: 'temp-id', body: 'Great post!' } }})See Optimistic Updates for a full walkthrough.
Forms
To back a <form> with a mutation — including one that works before JavaScript loads — reach
for useMutationForm instead of wiring up onSubmit by hand. See Forms.
Writing the Session
A mutation can set the user’s session by marking it @session. The field named by the directive’s
path becomes the session, which the server writes into an httpOnly cookie:
mutation Login($email: String!, $password: String!) @session(path: "login.session") { login(email: $email, password: $password) { session { token } }}Login is the obvious case, but anything that ends in “set the session from a mutation result” works
the same way, including merging a preference into the existing session or clearing it on logout. The
value always comes from the resolver, never from client input. The full story (replace vs. merge vs.
clear, and running it without JavaScript via @endpoint) lives in the
authentication guide.
Deduplication
By default, nothing stops the same operation from running multiple times simultaneously. The @dedupe directive gives us control over that behavior:
query UserProfile($id: ID!) @dedupe { user(id: $id) { name }}With no arguments, @dedupe prevents a second request from firing if an identical one is already in-flight. To cancel the first request instead of dropping the second, pass cancelFirst: true:
query UserProfile($id: ID!) @dedupe(cancelFirst: true) { user(id: $id) { name }}The match argument controls what counts as "identical":
Operation: dedupe if any execution of this operation is pending, regardless of variables (default)Variables: dedupe only if the variables also matchNone: never dedupe