ToEatApp Rails-React-Relay-GraphQL Tutorial - Queries
If you are curious about how some of these tools work together, you might look at the application setup post.
Background & Context
Facebook has published Relay and GraphQl. They have done this to make some bold moves away from imperative REST base APIs to declarative relay/graphql based APIs. In a REST mindset, developers have become accustomed to dealing with http methods and controller endpoints that frankly do all kinds of things.
Facebook’s outlook is that instead of having many endpoints that might do things in an unclear fashion, it makes more sense to declare precise logic related to events that developers want to occur and then route all that traffic through one server endpoint. GraphQl represents the declarations around what data is accesible and how it can be accessed and Relay represents a tool that allows clients to communicate with servers.
Typically, web developers expect http GET to return information, POST to create information, PUT/PATCH to update information and DELETE to destroy information. Graphql cares only about reading from a data store, as expressed through Queries, Types and Fragments, and writing to a data store, as expressed through Mutations.
Consider toeatapp.startuplandia.io, we want to know what food trucks exist and what orders they currently possess. In a REST based world, a developer might make a query that looked like GET /api/food_trucks which returns some kind of Array like collection [FoodTruck1, FoodTruck2, FoodTruckN] with each FoodTruck possibly having some kind of child relationship like FoodTruck.orders where .orders points to an Array like collection, [Order1, Order2, OrderN]. Without getting ultra meta and attempting to explain how the process works, we are just going to dive in and navigate the process required to render FoodTrucks and Orders.
Notice the Setup
Notice on the server, we will be using graphql-relay to implement graphql. On the client we will be using a bunch of different node packages, look at package.json for more details.
# Gemfile
gem 'graphql-relay'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"
endNotice 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.
class TrucksController < ApplicationController
def graphql
query = GraphqlSchema.execute(params[:query], variables: params[:variables])
render json: query
end
endschema.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.
Rendering The React App
Literally, the React app gets loaded into the dom via this snippet from app.jsx.
// app.jsx
ReactDOM.render(<ToEatApp />, document.getElementById('to-eat-app'))Once the app gets rendered, the RelayRouter takes control of the process and will attempt to render whatever view component is associated with the current route, in the case of the application root /, the RelayRouter has been programmed to load the ToEatIndex component.
// router.jsx
import ToEatIndex from './components/food_trucks/index.jsx'
const UserFleetQueries = {
user_fleet: () => Relay.QL`query { user_fleet }`
}
export default function ToEatApp() {
return (
<RelayRouter history={browserHistory} forceFetch={true}>
<Route path="/" queries={UserFleetQueries} component={ToEatIndex} />
</RelayRouter>
)
}Notice that the router renders a queries property that is rendering UserFleetQueries, which is a constant that points to a GraphQl fragment.user_fleet is the client-side reference to the GraphQL application declared type UserFleetType and represents the root access point of the data hierarchy that will allow us to ulimately arrive at FoodTruck.items. Notice that the type definition has a connection named :fleets which resolves to an Array containing Fleet.first. It is extremely important to notice that the connection is called a FleetType.connection_type as FleetType is another graphql type definition.
# user_fleet_type.rb
UserFleetType = GraphQL::ObjectType.define do
name "UserFleet"
description "container for user fleets"
connection :fleets, FleetType.connection_type do
resolve -> (obj, args, context) {[Fleet.first]}
end
endWe will now trace the linkage that starts with our root UserFleetType and begins an association linking Fleet.first to UserFleet via fleets as declared in FleetType. Notice that FleetType declares :id and :name as fields, a global_id_field and another connection named food_trucks that links food_trucks to obj where obj is a pointer to the parent object defined by UserFleetType that of Fleet.first.
# fleet_type.rb
FleetType = GraphQL::ObjectType.define do
name "Fleet"
description "food truck who?"
interfaces [NodeInterface.interface]
global_id_field :id
field :id, !types.ID
field :name, !types.String
connection :food_trucks, FoodTruckType.connection_type do
resolve -> (obj, args, context) { obj.food_trucks }
end
endNotice in the above FleetType definition, we have written interfaces [NodeInterface.interface] which is Graphql speak for ‘hey, this type definition has records in the db that someone on the client might want to look at so make sure to include them in the return blob of the query’. NodeInterface is defined in node_interface.rb
Let’s keep moving through the rabbit hole by looking at the FoodTruckType definition. Notice that FoodTruckType has the fields id, name, home_town and fleet_id and the connection :orders of type OrderType which resolves to the obj.orders where obj is a pointer to the parent FleetType.
FoodTruckType = GraphQL::ObjectType.define do
name "FoodTruck"
description "food truck"
interfaces [NodeInterface.interface]
global_id_field :id
field :id, !types.ID
field :name, !types.String
field :home_town, !types.String
field :fleet_id, !types.ID
connection :orders, OrderType.connection_type do
resolve -> (obj, args, context) { obj.orders }
end
endTo finish the chain of relationships, let’s look at the OrderType definition, notice we have written interfaces [NodeInterface.interface] which gives our OrderType connection actual nodes in our soon to be sent to the client data blob, also notice that this definition doesn’t have any defined connections because there is no other information to be related or connected.
# order_type.rb
OrderType = GraphQL::ObjectType.define do
name "Order"
description "tasty foods"
interfaces [NodeInterface.interface]
global_id_field :id
field :id, !types.ID
field :tasty_item, !types.String
field :food_truck_id, !types.ID
endThe connection between ActiveRecord and GraphQL::ObjectType.
You might find yourself wondering how the GraphQL::ObjectType definitions relate to active record objects. The goal of GraphQL is to define relationships between information. Once the relationships are in place, then Relay can shine by understanding how to make the smallest queries to the server and update only the parts of the data ‘graph’ that are necessary. Ultimately, GrapQL doesn’t care what actual data lives underneath the chain of structured relationship but we do, so we’ve told GraphQL what data to care about.
The connection chain.
# user_fleet_type.rb
UserFleetType = GraphQL::ObjectType.define do
connection :fleets, FleetType.connection_type do
resolve -> (obj, args, context) {[Fleet.first]}
end
end
# fleet_type.rb
FleetType = GraphQL::ObjectType.define do
connection :food_trucks, FoodTruckType.connection_type do
resolve -> (obj, args, context) { obj.food_trucks }
end
end
# food_truck_type.rb
FoodTruckType = GraphQL::ObjectType.define do
connection :orders, OrderType.connection_type do
resolve -> (obj, args, context) { obj.orders }
end
end
# order_type.rb
OrderType = GraphQL::ObjectType.define do
# OrderType doesn't have any connections
end
# UserFleetType.fleets => [Fleet.first]
# FleetType.food_trucks => Fleet.first.food_trucks => [ FoodTruck1, FoodTruck2 ]
# FoodTruckType.orders => FoodTruckN.orders => [ Order1, Order2 ]RootQueryType
GraphQL and Relay need a data graph to have a root object. Consider Facebook’s general use case a User has one or many Feeds, in this case the authenticated User.id would be the root of the graph. In our example case, we have ignored the process of authenticating a user and have built an arbitraty root object.
# root_query_type.rb
RootQueryType = GraphQL::ObjectType.define do
name "RootQuery"
description "root query"
field :node, field: NodeInterface.field
field :user_fleet do
type UserFleetType
# arbitrary root object OpenStruc.new(id: 1)
resolve -> (obj, args, context) { OpenStruct.new(id: 1) }
end
endNotice in the above ObjectType definition we have defined a RootQueryType and given it a field user_fleet. This user_fleet brings us back to the beginning of this post, to the moment where Relay is querying to the server for UserFleetQueries
// router.jsx
const UserFleetQueries = {
user_fleet: () => Relay.QL`query { user_fleet }`
}Now, consider everything you’ve read above, it might not make a ton of sense, which is why I’m writing this tutorial. Look at the below screenshot of the actual data graph being displayed on toeatapp.startuplandia.io. Notice the root of the data graph, as expressed by Object {__dataID__: "client:10658033131", fleets: Object} see if you can trace the chain of relationships from user_fleet down to orders.
Edges and Nodes
Do you find yourself looking at the above screenshot and wondering about the role of edges and node? If you are new to graph ideology, which I was before I began working with these tools, I didn’t understand the copious referencing to nodes and edges in Facebook’s documentation. Consider the below jpg, in a graph a node is literally anything Thingy, Object or Node and an edge is a reference to the connection between two different nodes.
You might be now saying to yourself, ‘yes I understand this reasonably basic concept but how did all the references to nodes and edges appear in our data graph?’. GraphQL has built our client side data structure for us and based on our ObjectType definitions, we have declared places where nodes or edges should exist.
Notice the chain of node declarations.
# root_query_type.rb
RootQueryType = GraphQL::ObjectType.define do
field :node, field: NodeInterface.field
end
# user_fleet_type.rb
UserFleetType = GraphQL::ObjectType.define do
# notice UserFleetType doesn't have any node information
end
# fleet_type.rb
FleetType = GraphQL::ObjectType.define do
interfaces [NodeInterface.interface]
end
# food_truck_type.rb
FoodTruckType = GraphQL::ObjectType.define do
interfaces [NodeInterface.interface]
end
# order_type.rb
OrderType = GraphQL::ObjectType.define do
interfaces [NodeInterface.interface]
endNotice the chain of edge declarations.(edges are created with connection)
# root_query_type.rb
# the root query isn't a connection so much as it is the base
# from where connections can be connected to
RootQueryType = GraphQL::ObjectType.define do
field :user_fleet do
type UserFleetType
resolve -> (obj, args, context) { OpenStruct.new(id: 1) }
end
end
# user_fleet_type.rb
UserFleetType = GraphQL::ObjectType.define do
connection :fleets, FleetType.connection_type do
resolve -> (obj, args, context) {[Fleet.first]}
end
end
# fleet_type.rb
FleetType = GraphQL::ObjectType.define do
connection :food_trucks, FoodTruckType.connection_type do
resolve -> (obj, args, context) { obj.food_trucks }
end
end
# food_truck_type.rb
FoodTruckType = GraphQL::ObjectType.define do
connection :orders, OrderType.connection_type do
resolve -> (obj, args, context) { obj.orders }
end
end
# order_type.rb
OrderType = GraphQL::ObjectType.define do
# OrderType doesn't have any edges
endYou might at this point have begun to notice that our data graph is litered with the key __dataID__. __dataID__ is the unique key that GraphQL/Relay use to manage information inside the data graph. These unique id fields create a way for Relay/GraphQL to know when, where and how to query/mutate the smallest parts of the data graph possible. In a typical active record world, a developer might find themselves refreshing an entire collection of records to update the DOM by literally querying to the server for all records in a collection. In the GraphQL/Relay world, Relay manages the process of knowing the smallest query/mutations to execute and these __dataID__ fields are essential to that process.
Notice that in our ObjectType definitions, we tell GraphQL to use specific id’s in our records to pay attention to, which creates the linkage between the __dataID__ and the reacord’s actual id in the database.
# root_query_type.rb
RootQueryType = GraphQL::ObjectType.define do
# root query isn't a record so no ids to keep track of
end
# user_fleet_type.rb
UserFleetType = GraphQL::ObjectType.define do
# user fleet type is an abstract container object
# so no ids to keep track of
end
# fleet_type.rb
FleetType = GraphQL::ObjectType.define do
global_id_field :id
end
# food_truck_type.rb
FoodTruckType = GraphQL::ObjectType.define do
global_id_field :id
end
# order_type.rb
OrderType = GraphQL::ObjectType.define do
global_id_field :id
endThis entire pattern might seem messy and unclear at first but spend some time with it and it will begin to make sense. It took me three very serious weeks to understand how all these patterns worked together and my goal with these blog posts is to reduce that time to hours for other developers.
Let’s Recap
In order to render the entire data graph encompassing the chain of relationships between UserFleet and Orders, when the dom is loaded, our React application uses the RealyRouter to query to the server for the user_fleet query.
const UserFleetQueries = {
user_fleet: () => Relay.QL`query { user_fleet }`
}
export default function ToEatApp() {
return (
<RelayRouter history={browserHistory} forceFetch={true}>
<Route path="/" queries={UserFleetQueries} component={ToEatIndex} />
</RelayRouter>
)
}The server receives that query as a POST /graphql
Prefix Verb URI Pattern Controller#Action
root GET / trucks#home
graphql POST /graphql(.:format) trucks#graphqlAnd returns a large object (data graph) that was built based on information contained in
# root_query_type.rb
RootQueryType = GraphQL::ObjectType.define do
# ...
end
# user_fleet_type.rb
UserFleetType = GraphQL::ObjectType.define do
# ...
end
# fleet_type.rb
FleetType = GraphQL::ObjectType.define do
# ...
end
# food_truck_type.rb
FoodTruckType = GraphQL::ObjectType.define do
# ...
end
# order_type.rb
OrderType = GraphQL::ObjectType.define do
# ...
endWhich becomes available to our React application as a standard javascript Object/Array blob sort of thing that can be interacted with inside the ToEatIndex component defined by import ToEatIndex from './components/food_trucks/index.jsx' in router.jsx.
Very Important Detail Relay and React require a Container to be defined in order to communicate to the component about the contents of the query being provided. This is a critical step and cannot be ignored.
// index.jsx
var Index = React.createClass({
getInitialState() {
return ({
foodTrucks: [],
fleetId: null
})
},
componentDidMount() {
console.log(this.props.user_fleet)
this.setState({fleetId: this.props.user_fleet.fleets.edges[0].node.id})
this.updateTruckInfo(this.props)
},
})Schema.json
Let’s not forget about the crucial step of generating schema.json. Everytime a change is made to any of the Ruby GraphQL type definitions or mutations, a new schema.json file must be regenerated by running bin/rake generate_graphql_schema. Notice that package.json tells graphql to look for schema.json as the schema file. Everytime a new schema.json file is built, the Webpack process needs to be restarted.
"metadata": {
"graphql": {
"schema": "./schema.json"
}
}Containers
Notice the bottom of index.jsx. A Container tells the React component the minimum information that must be provided in order for the component to be able to render itself. Notice how every piece of information that we want the DOM our React component to have access to is declared below.
export default Relay.createContainer(Index, {
fragments: {
user_fleet: () => Relay.QL`
fragment on UserFleet {
fleets(first: 10) {
edges {
node {
id
name
food_trucks(first: 10) {
edges {
node {
id
name
home_town
fleet_id
orders(first: 10) {
edges {
node {
id
tasty_item
food_truck_id
}
}
}
}
}
}
}
}
}
}
`
}
})In Closing
This is a particularly long and complicated tutorial. You should congratulate youself for reading this far and if everything didn’t sink in on first pass, that is really ok, the ideas are a bit messy to think about and slightly more messy to communicate about. You might reread the post, look at the source code and always feel free to reach out to me with questions, [email protected].
Next Steps
Read the next post in this series, ToEatApp React-Relay-GraphQL Tutorial - Mutations.