ToEatApp Rails-React-Relay-GraphQL Tutorial - Mutations

Words to the Wise

The application code runs at toeatapp.startuplandia.io. This tutorial utilizes Rails 4.2, NPM 3.8, Node 4.2.

Utilize the source code...

to_eat_app
git clone git@github.com:jcdavison/to_eat_app.git
#  woohoo !!

If you are curious about how these different tools are configured to work together, you might look at the application setup post. If you are unclear about the Relay query and client side data graph structure, you might look at the relay/graphql queries post.

schema.json

Anytime you change code on the server that tells or changes any of the structure in how Graphql is organizing data for Relay, you must update schema.json (bin/rake generate_graphql_schema) and restart the webpack process.

Mutations

REST ideology categorizes http request by the type of action being enacted upon a database. One might recognize these actions as GET,POST, PUT and DELETE, which generally map to show, create, edit and destroy information. Graphql operates in a bit more simplistic fashion, ie, the client is either reading information from the database or writing information to it. Writing to the database becomes synonymous with Mutation.

Also notice on the server, instead of a typical REST styled API, we will be routing ALL database read/write requests through the same server endpoint.

# routes.rb
Rails.application.routes.draw do
  root to: "trucks#home"
  post "/graphql" => "trucks#graphql"
end

Notice that the TrucksController responds to #graphql which has the sole function of receiving a query and returning whatever the query happens to return to the client. If you find yourself having an immediate, viscerally negative reaction to this thought, you are not alone. This is part of the code that represents a binary move away from REST ideology.

Creating a Food Truck

Let's consider for a moment that you aren't happy with the default FoodTruck options at toeatapp.startuplandia.io. Like the happy makers we are, we will need to create this FoodTruck.

Notice in index.jsx we render the React component, CreateFoodTruck.

// index.jsx
<CreateFoodTruck createFoodTruck={this.createFoodTruck} />

// where this.createFoodTruck points to
createFoodTruck(foodTruckDetails, createEvent) {
  foodTruckDetails['fleetId'] = this.state.fleetId
  let newFoodTruck = new CreateFoodTruckMutation(foodTruckDetails)
  Relay.Store.commitUpdate(newFoodTruck, {
    onSuccess: this.createSuccess.bind(this, createEvent),
    onFailure: this.createError
  })
},

// and CreateFoodTruckMutation was defined by
import CreateFoodTruckMutation from '../../mutations/create_food_truck'

Importantly, when this.createFoodTruck gets called, the function receives some basic information about the truck (name, homeTown) and passes that information to new CreateFoodTruckMutation(foodTruckDetails). Let's look at CreateFoodTruckMutation

//create_food_truck.jsx

import {Mutation} from 'react-relay'

export default class CreateFoodTruckMutation extends Mutation {
  getMutation() {
    return Relay.QL`mutation { create_food_truck }`
  }

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

  getFatQuery() {
    return Relay.QL`
      fragment on CreateFoodTruckPayload {
        food_truck {
          name
          home_town
        },
        fleet {
          food_trucks
          id
          name
        }
      }
    `
  }

  getConfigs() {
    return [{
      type: 'FIELDS_CHANGE',
      fieldIDs: {fleet: this.props.fleetId }
    }]
  }
}

Let's consider the main points related to the above Mutation definition. We are binding this client side Mutation to the server-side Mutation with the code return Relay.QL mutation { create_food_truck } where create_food_truck is defined in...

# create_food_truck_mutation.rb

CreateFoodTruckMutation = GraphQL::Relay::Mutation.define do
  name "CreateFoodTruck"
  description "Creates a new truck"

  input_field :name, !types.String
  input_field :home_town, !types.String

  return_field :food_truck, FoodTruckType
  return_field :fleet, FleetType

  resolve lambda { |inputs, context|
    food_truck = FoodTruck.new name: inputs[:name], home_town: inputs[:home_town], fleet_id: Fleet.first.id
    food_truck.save
    { food_truck: food_truck , fleet: Fleet.first}
  }
end

The next important point to notice in the CreateFoodTruckMutation is getVariables() which gives us the opportunity to define the information that will flow from the client to the server, in this case, this.props refers to the arguments passed to new CreateFoodTruckMutation(someArgs) as in the below.

// index.jsx

foodTruckDetails['fleetId'] = this.state.fleetId
let newFoodTruck = new CreateFoodTruckMutation(foodTruckDetails)

// create_food_truck.jsx

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

Continuing through CreateFoodTruckMutation we notice the function getFatQuery(), which tells our mutation about all the possible information in our client side data graph that COULD be impacted by the write we are preparing to make in the database.

// create_food_truck.jsx

getFatQuery() {
  return Relay.QL`
    fragment on CreateFoodTruckPayload {
      food_truck {
        name
        home_town
      },
      fleet {
        food_trucks
        id
        name
      }
    }
  `
}

This is a very important part of the setup as it allows Relay to understand the maximum possible impact this mutation could have on our data graph. Specifically, we are telling Relay that a CreateFoodTruckMutation could result in changes to food_truck and fleet.

Finally, notice in CreateFoodTruckMutation the call to getConfigs(), which tells Relay about the relevance of information that returns from the server to the client, after the write is executed.

// create_food_truck.jsx

getConfigs() {
  return [{
    type: 'FIELDS_CHANGE',
    fieldIDs: {fleet: this.props.fleetId }
  }]
}

In this case, we are telling Relay that the Fleet of id this.props.fleetId will be updated, which Relay then uses internally to update the information that Relay is currently tracking as associate with which Fleet happens to be in question.

The tie in back to our React component and the DOM occurs in conjunction with the above getConfigs() call. Once Relay has been told that 'hey Fleet with id n has been changed' Relay will then query to the database or in the case of a mutation, use the information provided back to the client from the server as defined in

# create_food_truck_mutation.rb

resolve lambda { |inputs, context|
  food_truck = FoodTruck.new name: inputs[:name], home_town: inputs[:home_town], fleet_id: Fleet.first.id
  food_truck.save
  { food_truck: food_truck , fleet: Fleet.first}
}

Relay will use the above returned Fleet object to tell React that the value of the component's initial props have changed in which case the standard React method componentWillReceiveProps() will be called as is defined,

// index.jsx

componentWillReceiveProps(args) {
  this.updateTruckInfo(args)
},

updateTruckInfo(blob) {
  if (blob.user_fleet.fleets.edges.length > 0) {
    let foodTrucks = blob.user_fleet.fleets.edges[0].node.food_trucks.edges
    this.setState({foodTrucks: foodTrucks})
  }
},

In summary, when a user clicks to create a new FoodTruck, React passes the information from the form to a Relay mutation object CreateFoodTruckMutation which sends a POST request to /graphql, upon which a bunch of whatever code we care about on the server runs (as defined in create_order_mutation.rb). Once the write has occured, the server sends information back to the client, that the client has been instructed to deal in the function definition getConfigs() inside the CreateFoodTruckMutation object which our React app then gets a call to componentWillReceiveProps(args) inside index.jsx at which point, our DOM updates with ONLY the new FoodTruck.

You, the reader, have now seen the full cycle of a Mutation. I've probably ommited small micro-details that you might uncover in your own quest to utilize mutations but for sure, you should now have a high level understanding of what is happening that in conjunction with the source code can be used to quickly build whatever app you care about.

Creating an Order

In the above Mutation, we created a new FoodTruck, for repetition's sake, we will create an order, which adheres to the same process as above.

Notice in index.jsx we render the React component, CreateOrder with the function this.createOrder bound to the createOrder property.

// index.jsx

<CreateOrder createOrder={this.createOrder} foodTrucks={this.state.foodTrucks} />

When the CreateOrder component calls this.createOrder a CreateOrderMutation will be instantiated and used to send information to the server.

import {Mutation} from 'react-relay'

export default class CreateOrderMutation extends Mutation {
  getMutation() {
    return Relay.QL`mutation { create_order }`
  }

  getVariables() {
    return {food_truck_id: this.props.food_truck_id, tasty_item: this.props.tasty_item}
  }

  getFatQuery() {
    return Relay.QL`
      fragment on CreateOrderPayload {
        order {
          food_truck_id
          tasty_item
        },
        food_truck {
          orders
        },
        fleet {
          food_trucks
          id
          name
        }
      }
    `
  }

  getConfigs() {
    return [{
      type: 'FIELDS_CHANGE',
      fieldIDs: {food_truck: this.props.food_truck_id}
    }]
  }
}

The above CreateOrderMutation will send food_truck_id and tasty_item to the server.

# create_order_mutation.rb

CreateOrderMutation = GraphQL::Relay::Mutation.define do
  name "CreateOrder"
  description "Creates a new order"

  input_field :tasty_item, !types.String
  input_field :food_truck_id, !types.ID

  return_field :order, OrderType
  return_field :food_truck, FoodTruckType
  return_field :fleet, FleetType

  resolve lambda { |inputs, context|
    order = Order.new tasty_item: inputs[:tasty_item], food_truck_id: inputs[:food_truck_id]
    order.save
    { order: order , food_truck: order.food_truck, fleet: Fleet.new}
  }
end

The mutation which specifically runs the below to create a new Order object, save it and then send all the relevant information back to the client.

# create_order_mutation.rb

order = Order.new tasty_item: inputs[:tasty_item], food_truck_id: inputs[:food_truck_id]
order.save
{ order: order , food_truck: order.food_truck, fleet: Fleet.new}

Once the server sends some json back to the client, Relay uses getConfigs() as defined in create_order.jsx to tell the Relay data store that the food_truck of id this.props.food_truck_id has changed.

// create_order.jsx

getConfigs() {
  return [{
    type: 'FIELDS_CHANGE',
    fieldIDs: {food_truck: this.props.food_truck_id}
  }]
}

Once Relay registers that the food_truck of id this.props.food_truck_id has changed, React will receive a call to componentWillReceiveProps(args) and subsequently call this.updateTruckInfo(args) in order to allow React to update the DOM with new information.

In summary, we know have two examples of creating information that starts on the client, hits the server and then heads back to the client. There is also code in the application that demonstrates how to delete a FoodTruck or Order.

Delete FoodTruck

// delete_order.jsx

import {Mutation} from 'react-relay'

export default class DeleteFoodTruckMutation extends Mutation {
  getMutation() {
    return Relay.QL`mutation { delete_food_truck }`
  }

  getVariables() {
    return {food_truck_id: this.props.food_truck_id}
  }

  getFatQuery() {
    return Relay.QL`
      fragment on DeleteFoodTruckPayload {
        fleet {
          food_trucks
        }
      }
    `
  }

  getConfigs() {
    return [{
      type: 'FIELDS_CHANGE',
      fieldIDs: {fleet: this.props.fleetId }
    }]
  }
}
# delete_food_truck_mutation.rb

DeleteFoodTruckMutation = GraphQL::Relay::Mutation.define do
  name "DeleteFoodTruck"
  description "delete a truck"

  input_field :food_truck_id, !types.ID

  return_field :fleet, FleetType

  resolve lambda { |inputs, context|
    FoodTruck.find(inputs[:food_truck_id]).destroy
    { fleet: Fleet.first }
  }
end

Delete Order

// delete_order.jsx

import {Mutation} from 'react-relay'

export default class DeleteOrderMutation extends Mutation {
  getMutation() {
    return Relay.QL`mutation { delete_order }`
  }

  getVariables() {
    return {order_id: this.props.order_id}
  }

  getFatQuery() {
    return Relay.QL`
      fragment on DeleteOrderPayload {
        food_truck {
          orders
        }
      }
    `
  }

  getConfigs() {
    return [{
      type: 'FIELDS_CHANGE',
      fieldIDs: {food_truck: this.props.food_truck_id}
    }]
  }
}
# delete_order_mutation.rb

DeleteOrderMutation = GraphQL::Relay::Mutation.define do
  name "DeleteOrder"
  description "delete an order"

  input_field :order_id, !types.ID

  return_field :food_truck, FoodTruckType

  resolve lambda { |inputs, context|
    order = Order.find(inputs[:order_id])
    food_truck = order.food_truck
    order.destroy
    { food_truck: food_truck }
  }
end

In Closing

Mutations and the code to get them to work is a bit slippery. When in doubt, look at and run the source code using log statements to highlight any and all underlying bits of information as needed.

The Fine Print

Particular Disclaimer: This tutorial isn't intended to be 100% every single step you need to take to make things work, this tutorial will help you create a mental model for how graphql and relay keep track of data, above all, the source code has all the details required to make the application work its magic.
Other Particular Disclaimer: I don't intend for this tutorial to be the 'right' or 'perfect' or 'best' way to do something. This tutorial is 'a' way to get a desired result. If you desire a different result or a different way of getting the same result, feel free to do that.
General Disclaimer: Things change, operating environments aren't all the same, people have different ideas about things, people are wrong, people are right, things don't always add up and when in doubt, look at the source code.
I continually seek new software engineering contracts and product collaboration opportunities so please, send email to jd@startuplandia.io.