Usage: Mutations
Mutation components combine a GraphQL mutation with a custom element which you would typically define with a DOM template and optionally some custom JavaScript behaviours. Mutation component encapsulate GraphQL mutation actions with a template for their resulting data, and/or their input fields.
This page is a HOW-TO guide. For detailed docs on the ApolloMutation
interface see the API docs
Mutations are how you make changes to the data in your GraphQL application. If you think of queries as analogous to HTTP GET
requests or SQL READ
statements, then mutations are kind of like HTTP POST
requests or SQL WRITE
statements.
Unlike query components, which automatically fetch their data by default, mutation components don't do anything until you program them to, e.g. in reaction to the user pressing a "save" button or entering text in a text field. Mutation components, or indeed the imperative call to their mutate()
method, take options to control how the mutation is performed and how the application should respond when it succeeds and returns a result.
Apollo Elements gives you four options for defining mutations in your UI, which you can mix and match them, depending on your particular needs.
- Using the
<apollo-mutation>
component - Using the
ApolloMutationController
- Making a mutation component by extending
ApolloMutation
or by usinguseMutation
- Calling
client.mutate
imperatively
HTML Mutations
You can declaratively define mutations using the <apollo-mutation>
HTML element from @apollo-elements/components
. Provide the GraphQL mutation, variable input fields, and result template as children of the element.
<apollo-mutation>
<script type="application/graphql">
mutation AddUser($name: String) {
addUser(name: $name) {
id name
}
}
</script>
<label for="username">Name</label>
<input id="username" data-variable="name"/>
<button trigger>Add User</button>
<template>
<slot></slot>
<template type="if" if="{{ data }}">
<p>{{ data.user.name }} added!</p>
</template>
</template>
</apollo-mutation>
Read more about declarative mutations in using <apollo-mutation>
and composing mutations or check out the mutation component API docs.
ApolloMutationController
Add a mutation to your component by creating an ApolloMutationController
. Call it's mutate()
method to fire the mutation. You can use it on any element which implements ReactiveControllerHost
, e.g. LitElement
, or you can use it on HTMLElement
if you apply ControllerHostMixin
from @apollo-elements/mixins
import { LitElement, html } from 'lit';
import { ApolloMutationController } from '@apollo-elements/core';
export class MutatingElement extends LitElement {
mutation = new ApolloMutationController(this, AddUserMutation);
render() { /*...*/ }
onClickSubmit() {
this.mutation.mutate();
}
}
Custom Mutation Elements
Unlike query and subscription components, mutation components don't automatically send a request to the GraphQL server. You have to call their mutate()
method to issue the mutation, typically in response to some user input.
As such, you can only expect your component's data
property to be truthy once the mutation resolves.
<apollo-mutation>
<template>
<p-card>
<h2 slot="heading">Add User</h2>
<dl ?hidden="{{ !data }}">
<dt>Name</dt> <dd>{{ data.name }}</dd>
<dt>Added</dt> <dd>{{ dateString(data.timestamp) }}</dd>
</dl>
<slot slot="actions"></slot>
</p-card>
</template>
<mwc-textfield outlined
label="User Name"
data-variable="name"></mwc-textfield>
<mwc-button label="Add User" trigger></mwc-button>
</apollo-mutation>
<script>
document.currentScript.getRootNode()
.querySelector('apollo-mutation')
.extras = {
dateString(timestamp) {
return new Date(timestamp).toDateString();
}
}
</script>
import type { ResultOf, VariablesOf } from '@graphql-typed-document-node/core';
import { ApolloMutationMixin } from '@apollo-elements/mixins/apollo-mutation-mixin';
import { AddUserMutation } from './AddUser.mutation.graphql';
const template = document.createElement('template');
template.innerHTML = `
<p-card>
<h2 slot="heading">Add User</h2>
<dl hidden>
<dt>Name</dt> <dd data-field="name"></dd>
<dt>Added</dt> <dd data-field="timestamp"></dd>
</dl>
<mwc-textfield slot="actions" label="User Name" outlined></mwc-textfield>
<mwc-button slot="actions" label="Add User"></mwc-button>
</p-card>
`;
export class AddUserElement extends ApolloMutation<typeof AddUserMutation> {
mutation = AddUserMutation;
#data: ResultOf<typeof AddUserMutation>;
get data(): ResultOf<typeof AddUserMutation> { return this.#data; }
set data(data: ResultOf<typeof AddUserMutation>) { this.render(this.#data = data); }
$(selector) { return this.shadowRoot.querySelector(selector); }
constructor() {
super();
this
.attachShadow({ mode: 'open' })
.append(template.content.cloneNode(true));
this.onInput = this.onInput.bind(this);
this.$('mwc-textfield').addEventListener('click', this.onInput);
this.$('mwc-button').addEventListener('click', () => this.mutate());
}
onInput({ target: { value: name } }) {
this.variables = { name };
}
render(data) {
this.$('dl').hidden = !!data;
if (data) {
const timestamp = new Date(data.timestamp).toDateString();
const { name } = data;
for (const [key, value] of Object.entries({ name, timestamp })
this.$(`[data-field="${key}"]`).textContent = value;
}
}
}
customElements.define('add-user', component(AddUser));
import { ApolloMutationController } from '@apollo-elements/core';
import { html, TemplateResult } from 'lit';
import { customElement } from 'lit/decorators.js';
import { AddUserMutation } from './AddUser.mutation.graphql';
@customElement('add-user')
export class AddUserElement extends LitElement {
mutation = new ApolloMutationController(this, AddUserMutation);
render(): TemplateResult {
const name = this.mutation.data.name ?? '';
const timestamp =
!this.mutation.data ? ''
: new Date(this.mutation.data.timestamp).toDateString();
return html`
<p-card>
<h2 slot="heading">Add User</h2>
<dl ?hidden="${!this.data}">
<dt>Name</dt> <dd>${name}</dd>
<dt>Added</dt> <dd>${timestamp}</dd>
</dl>
<mwc-textfield slot="actions"
label="User Name"
outlined
@input="${this.onInput}"></mwc-textfield>
<mwc-button slot="actions"
label="Add User"
@input="${() => this.mutation.mutate()}"></mwc-button>
</p-card>
`;
}
onInput({ target: { value: name } }) {
this.mutation.variables = { name };
}
}
import type { Binding, ViewTemplate } from '@microsoft/fast-element';
import { ApolloMutationBehavior } from '@apollo-elements/fast';
import { FASTElement, customElement, html } from '@microsoft/fast-element';
import { AddUserMutation } from './AddUser.mutation.graphql';
const getName: Binding<AddUserElement> =
x =>
x.mutation.data?.name ?? ''
const getTimestamp: Binding<AddUserElement> =
x =>
x.mutation.data ? new Date(x.mutation.data.timestamp).toDateString() : '';
const setVariables: Binding<AddUserElement) =
(x, { target: { value: name } }) => {
x.mutation.variables = { name };
}
const template: ViewTemplate<AddUserElement> = html`
<fast-card>
<h2>Add User</h2>
<dl ?hidden="${x => !x.mutation.data}">
<dt>Name</dt> <dd>${getName}</dd>
<dt>Added</dt> <dd>${getTimestamp}</dd>
</dl>
<fast-text-field @input="${setVariables}">User Name</fast-text-field>
<fast-button @input="${x => x.mutation.mutate()}">Add User</fast-button>
</fast-card>
`;
@customElement({ name: 'add-user' template })
export class AddUserElement extends FASTElement {
mutation = new ApolloMutationBehavior(this, AddUserMutation);
}
import { useMutation } from '@apollo-elements/haunted/useMutation';
import { useState, component, html } from 'haunted';
import { AddUserMutation } from './AddUser.mutation.graphql';
function AddUser() {
const [addUser, { called, data }] = useMutation(AddUserMutation);
const [variables, setVariables] = useState({ });
const onInput = event => setVariables({ name: event.target.value }));
const name = data.name ?? '';
const timestamp = data ? new Date(data.timestamp).toDateString() : '';
return html`
<p-card>
<h2 slot="heading">Add User</h2>
<dl ?hidden="${!data}">
<dt>Name</dt> <dd>${name}</dd>
<dt>Added</dt> <dd>${timestamp}</dd>
</dl>
<mwc-textfield slot="actions"
label="User Name"
outlined
@input="${onInput}"></mwc-textfield>
<mwc-button slot="actions"
label="Add User"
@input="${() => addUser({ variables })}"></mwc-button>
</p-card>
`;
}
customElements.define('add-user', component(AddUser));
import { useMutation } from '@apollo-elements/atomico/useMutation';
import { useState, c } from 'atomico';
import { AddUserMutation } from './AddUser.mutation.graphql';
function AddUser() {
const [addUser, { called, data }] = useMutation(AddUserMutation);
const [variables, setVariables] = useState({ });
const onInput = event => setVariables({ name: event.target.value }));
const name = data.name ?? '';
const timestamp = data ? new Date(data.timestamp).toDateString() : '';
return (
<host shadowDom>
<p-card>
<h2 slot="heading">Add User</h2>
<dl hidden={!data}>
<dt>Name</dt> <dd>${name}</dd>
<dt>Added</dt> <dd>${timestamp}</dd>
</dl>
<mwc-textfield slot="actions"
label="User Name"
outlined
oninput={onInput}></mwc-textfield>
<mwc-button slot="actions"
label="Add User"
oninput={() => addUser({ variables })}></mwc-button>
</p-card>
</host>
);
}
customElements.define('add-user', c(AddUser));
import { mutation, define, html } from '@apollo-elements/hybrids';
import { AddUserMutation } from './AddUser.mutation.graphql';
type AddUserElement = {
mutation: ApolloMutationController<typeof AddUserMutation>;
}
const onInput =
(host, event) =>
setVariables({ name: event.target.value }));
const mutate =
host =>
host.mutate();
define<AddUserElement>('add-user', {
mutation: mutation(AddUserMutation),
render: ({ mutation }) => {
const name = mutation.data?.name ?? '';
const timestamp = mutation.data ? new Date(mutation.data.timestamp).toDateString() : '';
return html`
<p-card>
<h2 slot="heading">Add User</h2>
<dl ?hidden="${!mutation.data}">
<dt>Name</dt> <dd>${name}</dd>
<dt>Added</dt> <dd>${timestamp}</dd>
</dl>
<mwc-textfield slot="actions"
label="User Name"
outlined
@input="${onInput}"></mwc-textfield>
<mwc-button slot="actions"
label="Add User"
@input="${mutate}"></mwc-button>
</p-card>
`;
},
})
The key here is the <mwc-button>
element which, on click, calls the element's mutate()
method. Until the user clicks that button and the mutation resolves, the element will have a null data
property, and therefore the <dl>
element which displays the mutation result will remain hidden.
Imperative Mutations
You don't need to define a component in order to issue a mutation. The Apollo client instance has a mutate()
method which you can call imperatively at any time. This is good for one-off actions, or for when you want to issue a mutation programatically, i.e. not in response to a user action.
onClickSubmit() {
const { data, error, loading } =
await this.client.mutate({ mutation, variables });
}
Mutation Variables
Set the variables
DOM property on your mutation component using JavaScript:
document.querySelector('add-user-mutation-element').variables = { name: 'Yohanan' };
Or call your element's mutate()
method with a variables
argument:
document.querySelector('add-user-mutation-element').mutate({
variables: {
name: 'Reish Lakish',
},
});
Optimistic UI
Apollo client provides us with a feature called optimistic UI which lets us calculate the expected result of a mutation before the GraphQL server responds. Set the optimisticResponse
property on your element to take advantage of this. The value of optimisticResponse
can either be an object which represents the expected result value of the mutation, or it can be a function which takes a single argument vars
(the variables for the mutation) and return a result object.
import type { AddUserMutationVariables, AddUserMutationData } from '../generated-schema';
const el = document.querySelector('add-user-mutation-element');
el.optimisticResponse =
(vars: AddUserMutationVariables): AddUserMutationData => ({
addUser: {
data: {
name: vars.name,
},
},
});
Reacting to Updates
Often, you don't just want to fire a mutation and leave it at that, but you want the results of your mutation to update the state of the application as well. In the case of our AddUser
example, we might want to update an existing query for list of users.
Refetch Queries
If you specify the refetchQueries
property, Apollo client will automatically refetch all the queries you list.
const el = document.querySelector('add-user-mutation-element');
el.refetchQueries = ['UsersQuery'];
If you also set the boolean property awaitRefetchQueries
, then the mutation component won't set it's data
and loading
properties until after the specified queries are also resolved.
You can set the refetch-queries
attribute as a comma-separated list as well
<add-user-mutation-element
refetch-queries="UsersQuery,FriendsListQuery"
></add-user-mutation-element>
Updater Function
For more performant and customizable updates, you can define a mutation update function. See the cache management guide for more info.
Next Steps
Read about the <apollo-mutation>
HTML element,
dive into the ApolloMutation
API and component lifecycle
or continue on to the subscriptions guide.