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:
<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>
<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:
<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>
<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.
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.