Mutation

Send a mutation to the server and updating your client-side cache with any changes.

<script lang="ts">
    import { graphql } from '$houdini'

    const uncheckItem = graphql(`
        mutation UncheckItem($id: ID!) {
            uncheckItem(item: $id) {
                item {
                    id
                    completed
                }
            }
        }
    `)
</script>

<button on:click={() => uncheckItem.mutate({ id: 'my-item' })}>
    Uncheck Item
</button>
<script>
    import { graphql } from '$houdini'
    
    const uncheckItem = graphql(`
        mutation UncheckItem($id: ID!) {
            uncheckItem(item: $id) {
                item {
                    id
                    completed
                }
            }
        }
    `)
</script>

<button on:click={() => uncheckItem.mutate({ id: 'my-item' })}>
    Uncheck Item
</button>

Mutation stores provide a mutate method that invokes the specified mutation with the inputs specified by the first argument to the handler. The second argument to the result of mutation can be used to configure its behavior. The following values can be provided:

  • optimisticResponse specifies a value to use instead of waiting for the server to return a value. For more information, see the optimistic responses section.

Notes

  • mutations usually do best when combined with at least one fragment grabbing the information needed for the mutation (for an example of this pattern, see below.)

Form actions

Using a mutation inside an action endpoint looks very similar to anywhere else. For reference, it would look like:

import { graphql } from '$houdini'
import { fail } from '@sveltejs/kit'
import type { Actions } from './$types'

export const actions: Actions = {
    add: async (event) => {
        const data = await event.request.formData()

        const name = data.get('name')?.toString()

        if (!name) {
            return fail(403, { name: '*' })
        }

        const actionMutation = graphql(`
            mutation ActionMutation($name: String!) {
                addUser(name: $name, birthDate: 254143016000, snapshot: "ActionMutation") {
                    id
                    name
                }
            }
        `)

        return await actionMutation.mutate({ name }, { event })
    }
}
import { graphql } from '$houdini'
import { fail } from '@sveltejs/kit'

/* @type { import('./$types').Actions } */
export const actions = {
    add: async (event) => {
        const data = await event.request.formData()

        const name = data.get('name')?.toString()

        if (!name) {
            return fail(403, { name: '*' })
        }

        const actionMutation = graphql(`
            mutation ActionMutation($name: String!) {
                addUser(name: $name, birthDate: 254143016000, snapshot: "ActionMutation") {
                    id
                    name
                }
            }
        `)

        return await actionMutation.mutate({ name }, { event })
    },
}

Please note that passing the RequestEvent through to the mutate call is necessary in order for SvelteKit’s fetch request handling to work as expected: header forwarding, handleFetch hook, etc.

Updating fields

When a mutation is responsible for updating fields of entities, houdini should take care of the details for you as long as you request the updated data alongside the record’s id.

Take for example, an TodoItemRow component:

TodoItemRow.svelte
<script lang="ts">
    import { fragment, graphql } from '$houdini'
    import type { TodoItemRow } from '$houdini'

    export let item: TodoItemRow
    // the resulting store will stay up to date whenever `checkItem`
    // is triggered
    $: data = fragment(item, graphql(`
        fragment TodoItemRow on TodoItem {
            id
            text
            completed
        }
    `))

    const store = graphql(`
        mutation CheckItem($id: ID!) {
            checkItem(item: $id) {
                item {
                    id
                    completed
                }
            }
        }
    `)

    function checkItem() {
        store.mutate({ id: $data.id })
    }
</script>

<li class:completed={$data.completed}>
    <input
        name={$data.text}
        class="toggle"
        type="checkbox"
        checked={$data.completed}
        on:click={handleClick}
    />
    <label for={$data.text}>{$data.text}</label>
    <button class="destroy" on:click={checkItem} />
</li>
TodoItemRow.svelte
<script>
    import { fragment, graphql } from '$houdini'
    
    /* @type { import('$houdini').TodoItemRow } */
    export let item
    // the resulting store will stay up to date whenever `checkItem`
    // is triggered
    $: data = fragment(item, graphql(`
        fragment TodoItemRow on TodoItem {
            id
            text
            completed
        }
    `))
    
    const store = graphql(`
        mutation CheckItem($id: ID!) {
            checkItem(item: $id) {
                item {
                    id
                    completed
                }
            }
        }
    `)
    
    function checkItem() {
        store.mutate({ id: $data.id })
    }
</script>

<li class:completed={$data.completed}>
    <input
        name={$data.text}
        class="toggle"
        type="checkbox"
        checked={$data.completed}
        on:click={handleClick}
    />
    <label for={$data.text}>{$data.text}</label>
    <button class="destroy" on:click={checkItem} />
</li>

Lists

Adding and removing records from a list is done by mixing together a few different generated fragments and directives. In order to tell the compiler which lists are targets for these operations, you have to mark them with the @list directive and provide a unique name:

query AllItems {
	items @list(name: "All_Items") {
		id
	}
}

Some things worth mentioning:

  • It’s recommended to name these lists with a different casing convention than the rest of your application to distinguish the generated fragments from those in your codebase.
  • Sometimes the runtime needs a little help to hunt down the list you want to mutate. For more information on how to use @parentID to help, see the GraphQL docs.

Inserting a record

With this field tagged, any mutation that returns an Item can be used to insert items in this list:

mutation NewItem($input: AddItemInput!) {
	addItem(input: $input) {
		item {
			...All_Items_insert
		}
	}
}

Removing a record

Any mutation that returns an Item can also be used to remove an item from the list:

mutation RemoveItem($input: RemoveItemInput!) {
	removeItem(input: $input) {
		item {
			...All_Items_remove
		}
	}
}

Toggling a record

Any mutation that returns an Item can also be used to toggle an elements membership of the list. If the item is already in the list, it will be removed; otherwise, it will be added.

mutation ToggleItem($input: ToggleItemInput!) {
	toggleItem(input: $input) {
		item {
			...All_Items_toggle
		}
	}
}

Deleting a record

Sometimes it can be tedious to remove a record from every single list that mentions it. For these situations, Houdini provides a directive that can be used to mark a field in the mutation response holding the ID of a record to delete from all lists.

mutation DeleteItem($id: ID!) {
	deleteItem(id: $id) {
		itemID @Item_delete
	}
}

Conditionals

Sometimes you only want to add or remove a record from a list when an argument has a particular value. For example, in a todo list you might not want to add the result to the list if the view is only showing the completed items. To support this, houdini provides the @when and @when_not directives. These filters apply to the values of arguments passed to the field marked with @list.

mutation NewItem($input: AddItemInput!) {
	addItem(input: $input) {
		item {
			...All_Items_insert @when_not(completed: true)
		}
	}
}

Optimistic Responses

A lot of the time we know the value that a mutation will trigger assuming everything goes right. For example, a toggleItem mutation in a todo list will invert the value of the checked field of a particular item. In these situations, we don’t have to wait for a mutation to resolve in order to apply the update to the cache. Instead, we can assume that it will succeed and provide an “optimistic response” for the mutation with the second argument to a mutation handler:

ToggleButton.svelte
<script lang="ts">
    import { graphql } from '$houdini'

    export let itemID: number

    const toggle = graphql(`
        mutation ToggleItem($id: ID!) {
            toggleItem {
                item {
                    id
                    checked
                }
            }
        }
    `)

    function toggleItem() {
        toggle.mutate({ id: itemID }, {
            optimisticResponse: {
                toggleItem: {
                    item: {
                        id: '1',
                        checked: true
                    }
                }
            }
        })
    }
</script>

<button on:click={toggleItem}>
    toggle item
</button>
ToggleButton.svelte
<script>
    import { graphql } from '$houdini'
    
    /* @type { number } */
    export let itemID
    
    const toggle = graphql(`
        mutation ToggleItem($id: ID!) {
            toggleItem {
                item {
                    id
                    checked
                }
            }
        }
    `)
    
    function toggleItem() {
        toggle.mutate(
            { id: itemID },
            {
                optimisticResponse: {
                    toggleItem: {
                        item: {
                            id: '1',
                            checked: true,
                        },
                    },
                },
            }
        )
    }
</script>

<button on:click={toggleItem}>
    toggle item
</button>

When the mutation resolves, the old values will be erased entirely and the new values will be committed to the cache. If instead the mutation fails, the optimistic changes will be reverted and the handler’s promise will reject with the error message as usual.

Remember to always request and specify an id when dealing with optimistic responses so that the cache can make sure to update the correct records. Also, it’s worth mentioning that you don’t have to provide a complete response for an optimistic value, the cache will write whatever information you give it (as long as its found in the mutation body). Because of this, the store value won’t update until the mutation resolves.

Optimistic Keys

Sometimes it’s not possible to know the ID to provide an optimistic response before hand. Most commonly, this happens when you are trying to create a new record and insert the result into a list. Take for example, the following mutation:

mutation CreateTodoItem($text: String!) {
	createItem(text: $text) {
		item {
			id
			text
			...All_Items_insert
		}
	}
}

If you wanted to submit this mutation with an optimistic response, it might look something like:

CreateTodoItem.mutate(
	{
		text: "My Item":
	},
	{
		optimisticResponse: {
			createItem: {
				item: {
					id: "????" // <--- what goes here?
					text: "My Item",
				}
			}
		}
	}
)

To support this situation, you can tell Houdini to generate a temporary ID for you using the @optimisticKey directive:

mutation CreateTodoItem($text: String!) {
	createItem(text: $text) {
		item {
			id @optimisticKey
			text

			...All_Items_insert
		}
	}
}

And now you don’t have to provide an ID to the optimitic response. Houdini will keep track the generated value and replace it with the real one when the mutation resolves:

CreateTodoItem.mutate(
	{
		text: "My Item":
	},
	{
		optimisticResponse: {
			createItem: {
				item: {
					text: "My Item",
				}
			}
		}
	}
)

Now you might be asking how this is different from the regular situation where you just made up an id on your own. Well, the important distinction is that Houdini tracks these generated keys internally. If you were to use one as the input for another mutation before the create mutation resolves (say to mark the new item as complete), the second mutation would block while it waits for the valid ID value from the server.

If your record’s keys are a custom scalar that Houdini cannot support, you will have to provide a value in your optimisticResponse object.

Why is typescript missing fields?

If you are using typescript, you might notice that the generated types for optimistic responses do not include any fields from fragments that you might have spread in. While surprising at first, this is by design. We believe that it is a mistake to tightly couple the invocation of the mutation with a fragment that’s defined in some random file and whose definition might change unknowingly. If it did change, there would be a nasty error when the runtime tries to look up the schema information so the generated types are trying to guide you towards a safer practice.

There’s no harm in duplicating a field that is part of a fragment so if you are going to provide an optimistic value, you should add those fields to the explicit selection set of the mutation.