Findmypast Tech

A Step by Step Guide to Building a Form Using Relay and GraphQL. Part One: Mutations

Leigh Shepperson Leigh Shepperson
Reading time: 4 min

Relay is a powerful framework created at Facebook that can efficiently talk to a GraphQL server. Nevertheless, it has a reputation for being difficult to learn, primarily for the following reason: queries are easy but mutations are hard. This is compounded by the fact that the majority of examples tend to be overly complicated and the reader can easily get lost in the details. Nevertheless, with a bit of effort, once the basics are learnt the process becomes intuitive.

This tutorial is intended to give an immediate feel for creating mutations in Relay and Absinthe, an Elixir implementation of GraphQL. We will provide a step by step guide to building a simple form that uses a mutation to authenticate a user.

In part one of this tutorial, we will only focus on mutations.

Prerequisites

The main technologies we will use are absinthe and absinthe_relay and they respectively provide us with an implementation of GraphQL for Elixir and support for the Relay framework. We will assume that the reader has enough basic knowledge of these technologies to create queries and consume them using Relay. For further information, absinthe GraphQL tutorial is the recommended place to start.

Step One: Create a GraphQL Mutation

In this section, we will create a mutation that has two input fields and one output field.

The two inputs are email and password and the output is a Boolean called success. The resolve function will return true if the email and password respectively match “test@test.com” and “password”; otherwise it will return false. We create the mutation after this brief discussion:

The payload macro from absinthe_relay provides support for creating fields that satisfy Relay’s common mutation pattern. This pattern is not very complicated; it just means that each mutation is a root field on the mutation type, it has exactly one input called input and both the input object and the output object have a field called the client mutation identifier that is used to match up the response to the request.

Note, it is helpful to think of the payload as the output of a mutation.

The payload macro takes care of these implementation details. At a minimum, it just needs you to name the mutation, specify the input fields, specify the output fields, and provide a resolve function.

Thus, the mutation for our form looks like this:

payload field :login do
  input do
    field :email, non_null(:string)
    field :password, non_null(:string)
  end
  output do
    field :success, non_null(:boolean)
  end
  resolve fn
    %{email: "test@test.com", password: "password"}, _ -> {:ok, %{success: true}}
    _, _ -> {:ok, %{success: false}}
  end
end

Note that our implementation has a resolve method that always returns the success tuple. This is because the error tuple is reserved for developer errors and all validation errors should form part of the payload. We will expand on this in part two of this tutorial.

Step Two: Test the Mutation in GraphiQL

In order to get a feel for the mutation, we will test it in GraphiQL.

At a first glance, the syntax for creating mutations is vastly different from the syntax for creating queries. However, the thing to remember is that each mutation has exactly one input called “input” and it is a root field of the mutation type. So, we just need to specify the payload fields and create the input object for the mutation.

Note, we also specify that the input to the mutation is of type LoginInput. To understand more about this object, it is helpful to take a look at the input and payload types that have been created in the schema:

input LoginInput {
  email: String!
  password: String!
  clientMutationId: String!
}

type LoginPayload {
  success: Boolean
  clientMutationId: String!
}

Thus, the syntax for the login mutation is given as follows:

mutation LoginMutation($input: LoginInput){
  login(input:$input){
    success
  }
}

with query variables

{
  "input": {
    "email": "test@test.com",
    "password": "password",
    "clientMutationId": "someId"
  }
}

Note, as part of the input object you have to specify the client mutation identifier (clientMutationId). This makes sense given the discussion of the relay common mutation pattern in the previous section. In fact, if you request the client mutation identifier as part of the payload, you will find that the two values are the same.

Step Three: Create a Relay Mutation

There are four functions that you need to know when creating Relay mutations: getMutation, getVariables, getFatQuery and getConfigs. The following relay mutation guide contains information about these functions in greater depth, but we will now give a brief illustration of these as follows:

The getMutation function tells GraphQL what mutation you want to perform and getVariables returns the input variables used by the mutation.

The getFatQuery function tells Relay what could change as a result of the mutation and the getConfigs function describes how Relay should handle the payload returned by the server.

There are various technicalities surrounding these functions, for example, Relay intersects the information defined in the fat query with a “tracked query” to efficiently fetch data from the server, but we won’t worry about them here. (See relay mutation guide for more information)

In our example, we do not want to write the success field into the client store, but we do want to have access to it so we can appropriately handle the response from the server. In this situation, the ideal type for the getConfigs function is REQUIRED_CHILDREN.

Thus, the Relay mutation looks like this:

import Relay from 'react-relay';

export default class LoginMutation extends Relay.Mutation {

  getMutation() {
    return Relay.QL `mutation {
      login
    }`;
  }

  getVariables() {
    return {
      email: this.props.email,
      password: this.props.password
    };
  }

  getFatQuery() {
    return Relay.QL `
    fragment on LoginPayload {
       success
    }`;
  }

  getConfigs() {
    return [{
      type: 'REQUIRED_CHILDREN',
      children: [Relay.QL`
        fragment on LoginPayload {
          success
        }`]
    }];
  }
}

Step Four: Build the Form Component

Given the Relay mutation defined in the previous section, we will proceed to build a component that contains a form that enables us to authenticate a user.

The form will have two input boxes, one for the email address, the other for the password. It will also have a submit button.

At this stage, the implementation is quite simple; we just console log the response:

import React, { Component, PropTypes } from 'react';
import LoginMutation from '../login-mutation';

export default class LoginComponent extends Component {
  constructor() {
    super();
    this.state = {
      email: "",
      password: ""
    };
  }

  handleSubmit(e) {
    e.preventDefault();

    const onSuccess = (response) => {
      if(!response.login.success) {
        console.log("Email or Password is not valid");
      }
      else {
        console.log("Success");
      }
    };

    const onFailure = (transaction) => {
      console.log(transaction.getError());
    };

    const mutation = new LoginMutation(this.state);

    this.props.relay.commitUpdate(
      mutation, {onFailure, onSuccess}
    );
  }

  handleChange(name, e) {
    let change = {};
    change[name] = e.target.value;
    this.setState(change);
  }

  render() {
    return (
      <div>
        <form onSubmit={this.handleSubmit.bind(this)}>
          <input type="text" value={this.state.email} onChange={this.handleChange.bind(this, 'email')} />
          <input type="text" value={this.state.password} onChange={this.handleChange.bind(this, 'password')} />
          <input type="submit" value="Submit"/>
        </form>
      </div>
    );
  }
}

Part Two: Server-Side Validation

In part two, we will extend this example to include server-side validation and handling transaction failure. The main change will involve modifying the getConfigs function to be of type FIELDS_CHANGE and including validation messages as part of the payload.