Hey there everyone, it took us two years to make our GraphQL implementation support any mutations. We opted to keep it read-only for quite a long time because we use GraphQL to consolidate multiple APIs, but as we start new projects as GraphQL + databases then understanding mutations becomes much more important.

Last month, I talked with the team at Graph.cool about having them talk through Relay mutations comprehensively as a guest post on the Artsy Engineering blog. So, I'm really excited to introduce this great post on the topic by Nikolas Burk.

-- Orta

The Magic behind Relay Mutations

Relay is a powerful GraphQL client for React and React Native applications. It was open sourced by Facebook alongside GraphQL in 2015 and is a great tool for supporting you with managing your app's data layer.

In this post, we are going to explore how Relay mutations work by looking at a React Native app. The code can be found on GitHub. Our sample application is a simple Pokedex, where users can manage their Pokemons.

Note: We're going to assume a basic familiarity with GraphQL in this article. If you haven't heard of GraphQL before, the documentation and the GraphQL for iOS Developers post are great places to start. If you're interested in learning more about Relay in general, head over to Learn Relay for a comprehensive tutorial.

If you want to run the example with your own GraphQL server, you can use graphql-up to quickly spin one up yourself from within your browser. Simply click the pink button and follow the instructions on the website.

graphql-up

Relay - A brief Overview

Relay is the most sophisticated GraphQL client available at the moment. Like GraphQL, it has been used and battle-tested internally by Facebook for many years before it was open sourced.

Relay surely isn't the easiest framework to learn - but when used correctly, it takes care of managing large parts of your app's data layer in a consistent and reliable manner! It therefore is particularly well-suited for complex applications with lots of data interdependencies and provides outstanding longterm developer productivity.

Declarative API and Colocation

With Relay, React components specify their data requirements in a declarative fashion, making use of GraphQL fragments.

A GraphQL fragment is a selection of fields on a GraphQL type. You can use them to define reusable sub-parts of queries or mutations.

Considering the PokemonDetails view above, we need to display the Pokemon's name and image. The fragment that represents these data requirements looks as follows:

1
2
3
4
5
6
7
fragment PokemonDetails on Node {
  ... on Pokemon {
    id
    name
    url
  }
}

Note that the id is required so that Relay can identify the objects in the cache, so it's included in the payload as well (even if it's not displayed on the UI).

These fragments are kept in the same file as the React component, so UI and data requirements are colocated. Relay then uses a higher-order component called Relay.Container, to wrap the component along with its data requirements. From this point, the developer doesn't have to worry about the data any more! It will be fetched behind the scenes and is made available to the component via its props.

Build-time Schema Validation

Another great feature of Relay that ensures developer productivity is schema validation. At build time, Relay checks your GraphQL queries, fragments and mutations to ensure their compatibility with the GraphQL API. It is thus able to catch any typos or other schema-related errors before you run (or even worse: deploy) your app, saving your users from unpleasant experiences. Note that the schema validation step requires a Babel Relay Plugin.

Mutations in Relay

GraphQL Recap

In GraphQL, a mutation is the only way to create, update or delete data on the server - they effectively are the GraphQL abstraction for changing state in your backend.

As an example, creating a new Pokemon in our sample app uses the following mutation:

1
2
3
4
5
6
7
8
9
10
11
mutation CreatePokemon($name: String!, $url: String!) {
  createPokemon(input: {
    name: $name,
    url: $url
  }) {
    # payload of the mutation (will be returned by the server)
    pokemon {
      id 
    }
  }
}

Notice that mutations, similar to queries, also require a payload to be specified. This payload represents the information that we'd like to have returned from the server after the mutation was performed. In the above example, we're asking for the id of the new pokemon.

The Magic: Declarative Mutations 🔮

Relay doesn't (yet) give the developer the ability to manually modify the data that it stores internally. Instead, with every change, it requires a declarative description of how the local cache should be updated after the change happened in the form of a mutation and then takes care of the update under the hood.

The description is provided by subclassing Relay.Mutation and implementing (at least) four methods that help Relay to properly update the local store:

  • getMutation(): the name of the mutation (from the GraphQL schema)
  • getVariables(): the input variables for the mutation
  • getFatQuery(): a GraphQL query that fetches all data that potentially was changed due to the mutation
  • getConfigs(): a precise specification how the mutation should be incorporated into the cache

In the following, we'll take a deeper look at the different kinds of mutations in our sample app, which are used for creating, updating and deleting Pokemons.

Note: We're using the Graphcool Relay API for this example. If you used graphql-up to create your own backend, you can explore the API by pasting the endpoint for the Relay API into the address bar of a browser.

Creating a new Pokemon: RANGE_ADD

Let's walk through the different methods and understand what information we have to provide so that Relay can successfully merge the newly created Pokemon into its store.

The first two methods, getMutation() and getVariables() are relatively obvious and can be retrieved directly from the documentation where the API is described.

The implementations look as follows:

1
2
3
4
5
6
7
8
9
10
getMutation() {
  return Relay.QL`mutation { createPokemon }`
}

getVariables() {
  return {
    name: this.props.name,
    url: this.props.url,
  }
}

Notice that the props of a Relay.Mutation are passed through its constructor. Here, we simply provide the name and the url of the Pokemon that is to be created.

Now, on to the interesting parts. In getFatQuery(), we need to specify the parts that might change due to the mutation. Here, we simply specify the viewer:

1
2
3
4
5
6
7
8
9
getFatQuery() {
  return Relay.QL`
    fragment on CreatePokemonPayload {
      viewer {
        allPokemons
      }
    }
  `
}

Notice that every subfield of allPokemons is also automatically included with this approach. In our example app, allPokemons is the only point we expect to change after our mutation is performed.

Finally, in getConfigs(), we need to specify the mutator configurations, telling Relay exactly how the new data should be incorporated into the cache. This is where the magic happens:

1
2
3
4
5
6
7
8
9
10
11
12
getConfigs() {
  return [{
    type: 'RANGE_ADD',
    parentName: 'viewer',
    parentID: this.props.viewerId,
    connectionName: 'allPokemons',
    edgeName: 'edge',
    rangeBehaviors: {
      '': 'append'
    }
  }]
}

We first express that we want to add the node using RANGE_ADD for the type (there are 5 different types in total).

Relay internally represents the stored data as a graph, so the remaining information expresses where exactly the new node should be hooked into the existing structure.

Let's consider the shape of the data before we move on:

1
2
3
4
5
6
7
8
9
10
viewer {
  allPokemons {
    edges {
      node {
        id
        name
      }
    }
  }
}

Here we clearly see the direct connection between viewer and the Pokemons goes through allPokemons connection, so the parent of the new Pokemon is the viewer. The name of that connection is allPokemons, and lastly the edgeName is taken from the payload of the mutation.

The last piece, rangeBehaviors, specifies whether we want to append or prepend the new node.

Executing the mutation is as simple as calling commitUpdate on the relay prop that's injected to each component being wrapped with a Relay.Container. An instance of the mutation and the expected variables are passed as arguments to the constructor:

1
2
3
4
5
6
7
8
_sendCreatePokemonMutation = () => {
  const createPokemonMutation = new CreatePokemonMutation({
    viewerId: this.props.viewer.id,
    name: this.state.pokemonName,
    url: this.state.pokemonUrl,
  })
  this.props.relay.commitUpdate(createPokemonMutation)
}

Updating a Pokemon: FIELDS_CHANGE

Like with creating a Pokemon, getMutation() and getVariables() are trivial to implement and can be derived directly from the API documentation:

1
2
3
4
5
6
7
8
9
10
11
getMutation() {
  return Relay.QL`mutation { updatePokemon }`
}

getVariables() {
  return {
    id: this.props.id,
    name: this.props.name,
    url: this.props.url,
  }
}

In getFatQuery(), we only include the pokemon which includes the updated info this time, since that is the only part we expect to change after our mutation:

1
2
3
4
5
6
7
getFatQuery() {
  return Relay.QL`
    fragment on UpdatePokemonPayload {
      pokemon
    }
  `
}

Finally, getConfigs(), this time specifies a mutator configuration of type FIELDS_CHANGE since we're only updating properties on a single Pokemon:

1
2
3
4
5
6
7
8
getConfigs() {
  return [{
    type: 'FIELDS_CHANGE',
    fieldIDs: {
      pokemon: this.props.id,
    }
  }]
}

As sole additional piece of info, we declare the ID of the Pokemon that is being updated so that Relay has this information available when receiving the new Pokemon data.

Deleting a Pokemon: NODE_DELETE

As before, getMutation() and getVariables() are self-explanatory:

1
2
3
4
5
6
7
8
9
getMutation() {
  return Relay.QL`mutation { deletePokemon }`
}

getVariables() {
  return {
    id: this.props.id,
  }
}

Then, in getFatQuery(), we need to retrieve the pokemon from the mutation payload:

1
2
3
4
5
6
7
getFatQuery() {
  return Relay.QL`
    fragment on DeletePokemonPayload {
      pokemon
    }
  `
}

In getConfigs(), we're getting to know another mutator configuration type called NODE_DELETE. This one requires a parentName as well as a connectionName, both coming from the mutation payload and specifying where that node existed in Relay's data graph. Another requirement, that is specifically relevant for the implementation of a GraphQL server, is that the mutation payload of a deleting mutation always needs to return the id of the deleted node so that Relay can find that node in its store. Taking all of this together, our implementation of getConfigs() can be written like so:

1
2
3
4
5
6
7
8
getConfigs() {
  return [{
    type: 'NODE_DELETE',
    parentName: 'pokemon',
    connectionName: 'edge',
    deletedIDFieldName: 'deletedId'
  }]
}

Wrapping Up

Relay has a lot of benefits that make it a very compelling framework to use for state management and interaction with GraphQL APIs. Its major strengths are a highly optimized cache, thoughtful UI integration as well as the declarative API for data fetching and mutations.

The initial version of Relay came with a notable learning curve due to lots of magic happening behind the scenes. However, Facebook recently released the first release candidates of Relay v1.0.0 (Relay Modern) with the goal of making Relay generally more approachable.

It's worth noting that Relay isn't the only available GraphQL client. Apollo Client is a great alternative which is a lot easier to get started with. For a detailed comparison please refer to this article.

If you want to learn more about GraphQL and want to stay up-to-date with the latest news of the GraphQL community, subscribe to GraphQL Weekly.

Categories: graphql, guest, javascript, relay