Apollo Elements Apollo Elements Guides API Blog Toggle darkmode

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.

  1. Using the <apollo-mutation> component
  2. Using the ApolloMutationController
  3. Making a mutation component by extending ApolloMutation or by using useMutation
  4. 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.