Relay/GraphQL On Rails

Summary: If you are excited about just declaring data required by your UI components while leaving the difficult parts of data fetching, updating, and data handling performance issues to a framework, you will be excited by Facebook’s recent release of Relay/RelayJS (Sep 2015), which works with React UI (Oct 2014). This article shows you how to to integrate Relay/React into Rails.

NOTE:

  • You can clone this starter-kit from Github, and start using Relay immediately without reading further
  • Conversely, with instructions here you can re-create the starter-kit on Github

Facebook introduced React (Oct 2014) to make creating rich web UI easy to reason, and debug with its uni-directional data flow; all data required by the entire web UI on a page is piped through a top-level UI component, which percolates only the necessary data to each sub-components under it.

However, the React component (usually the top-level component), or the optional Flux framework (if you implemented it), is still responsible for fetching the required data using Ajax, etc., and you have to write that.

Enter Relay! When you create a React component, you just need to declare the data that it needs, and Relay will handle the gory details of fetching the data efficiently, which includes batching queries, collapsing access patterns, caching, etc.

Overview

Assumptions:

  • You have Rails installed (tested on Rails 4.2 only)
  • You have >= ruby-2.1

Goals:

  • Make it easy to understand (see Relay in Pictures section) Relay
  • Setup Relay/React on Rails so that we can create a very simple Rails/Relay/React application, which outputs to the browser:
The Board!
Boarding… Simpleton

NOTE: The 2nd line ‘Boarding… Simpleton’ is a React component, which relies on Relay to pull its data (Simpleton) from the server over the Internet.

  • Use this setup as a foundation to further experiment with Relay

Why this is difficult:

As of this writing (15 Sep 2015), I could not find any information on getting Facebook’s Github Relay or npm-based ‘react-relay’ to work with Rails.

The existing Facebook tutorial:

  • Does not show clear distinction between client-side, and server-side library requirements because in its case, both use Javascript libraries. Specifically, it does not address the question — “which libraries are required to make a non-Javascript-based server Relay-capable, and which libraries should be delivered to the browser to make the client-side Relay-capable?
// nodejs npm package.json
// Source from Facebook's Github Relay repo
// Which libraries are client-side & which are server-side?
“dependencies”: {
“babel”: “5.8.23”,
“babel-loader”: “5.3.2”,
“babel-relay-plugin”: “file:../../scripts/babel-relay-plugin/”,
“classnames”: “^2.1.3”,
“express”: “^4.13.1”,
“express-graphql”: “^0.3.0”,
“graphql”: “^0.4.2”,
“graphql-relay”: “^0.3.1”,
“react”: “^0.14.0-rc”,
“react-dom”: “^0.14.0-rc”,
“react-relay”: “file:../../”,
“webpack”: “^1.10.5”,
“webpack-dev-server”: “^1.10.1”
}
  • Has no information on how to transpile Relay/GraphQL client-side Javascript code, which may reside on a non-Javascript-based server, but needs to be delivered to browser for execution
// Relay/GraphQL code to get 'name' data from our
// 'simple' data model
// The browser does not understand GraphQL, so this needs
// to be transpiled to Javascript before delivery to the browser
simple: () => Relay.QL`
fragment on Simple {
name
}
`
  • Is not Rails-based so you need to hunt down nuggets of information from all over the web

Relay In Pictures

Relay consists of 3 parts:

  1. The schema for our data specified in GraphQL
  2. A GraphQL server, which can load up the schema, and respond to client queries in GraphQL
  3. A ‘transport’ layer through which a client communicates with the GraphQL server via GraphQL language

For our purpose, architecturally, this translates to:

  • A Rails server acting as a GraphQL server serving our data based on our schema (requirement 1. & 2.)
  • The Rails server delivering the necessary client-side Javascripts required for the browser to communicate with the GraphQL server through the Relay ‘transport’ layer, using the pre-agreed data schema (requires 1. & 3.)

To facilitate understanding, we illustrate the Relay nodejs setup used by the Facebook tutorial, and then compare what that setup should look like for Rails.

nodejs Relay Setup
Rails Relay Setup

Things of note:

  • Our GraphQL data schema (bottom-left box) is in either Javascript (nodejs server) or Ruby (Rails sever) but is converted using a npm script (nodejs) / Rake task (Rails) to JSON (follow the arrow upwards from the bottom-left box); nodesjs/Rails server-side code can use the GraphQL schema in Javascript/Ruby format, but babel-relay-plugin (more on this in next) needs it in JSON format
  • Transpilation of Relay.QL code (top-left box) to Javascript is done by babel (nodejs) / babelify (Rails) with the help of the babel-relay-plugin based on GraphQL data schema (JSON format)
  • We can clearly see what libraries are needed to make nodejs/Rails speak GraphQL, enabling it to host a /graphql endpoint (bottom-right box); the arrows from libraries/Gems pointing to the ‘Pure JS server-side /graphql endpoint’ (bottom-right box)
  • All Javascript code that needs to be delivered to the client-side (top-right box) is bundled using webpack (nodejs) / browerify-rails (Rails), which is also where transpilation is configured

Let us look at how we can setup each component in detail next.

Rails GraphQL Server

GraphQL-speaking Rails

Add the following gems and run ‘bundle install’:

# Gemfile
gem ‘graphql’ 
gem ‘graphql-relay’

graphql-ruby bestows Ruby with the ability to understand GraphQL, while graphql-relay-ruby provides helpers to handle Relay specific-concepts such as:

  • Associating a piece of data with a global id to facilitate re-fetching
  • Tying a piece of data with associated data, e.g., a piece of data representing a Person, can have many associated pieces of data about each of his Friend
  • Updating a piece of data with input from client (Relay uses the fancy term ‘mutation’)

GraphQL Data Schema

Facebook’s official tutorial shows how we can use GraphQL for data schema definition. Unlike the tutorial, instead of defining the data schema in Javascript (data/schema.js), we define it using graphql-ruby classes in a set of files under app/graph.

We will present a very simple GraphQL schema here as it is a huge topic, and Facebook’s tutorial has covered it quite a bit.

A GraphQL schema consists of a Query, and Mutation type, for handling queries and updates respectively. Each of these is compose-able out of other GraphQL types, e.g., our Simple query is composed from SimpleType (see below).

The first thing that you need to implement for the schema is the standard NodeIdentification interface; each piece of data that a client fetches through Relay/GraphQL comes from an instance object, e.g., an ActiveRecord, in Rails. Each of them is also assigned a global ID, that when passed back to Relay/GraphQL, enables Relay/GraphQL to re-fetch that same instance object (whose data may have changed). For more details read this, but in the same vein as Facebook’s Relay tutorial, this was introduced at the start.

# app/graph/types/node_identification.rb
GraphQL::Relay::GlobalNodeIdentification.instance_variable_set(:@instance, nil)
NodeIdentification = 
GraphQL::Relay::GlobalNodeIdentification.define do
object_from_id -> (id) do
type_name, id = NodeIdentification.from_global_id(id)
type_name.constantize.find(id)
end
type_from_object -> (object) do
RelayOnRailsSchema.types[object.class.name]
end
end

Next, let us look at the GraphQL schema Query (but leave out Mutation as an exercise).

# app/graph/types/query_type.rb
QueryType = GraphQL::ObjectType.define do
name “Query”
description “The query root for this schema”
  field :simple do
type SimpleType
description “Simple Stuff”
resolve -> (obj, args, ctx) do
Hashie::Mash.new( { id: rand(1000), name: ‘Simpleton’ } )
end
end
  field :node, field: NodeIdentification.field
end

This schema enables us to query data by either of the fields:

  • :simple with some arguments (args)
  • :node id

Under the :simple field:

  • resolve tells us how to get the data. In this case, we merely return a constant hash. However, if arguments were provided in the GraphQL query, e.g., a user email, we can access it in args and fetch data based on it by replacing resolve with:
# app/graph/types/query_type.rb
... 
resolve -> (obj, args, ctx) do
# Using ActiveRecord for Person Model
Person.find_by_email(args[:email])
end
...
  • SimpleType defines the structure of the data we fetch (app/graph/types/simple_type.rb)
# app/graph/types/simple_type.rb
SimpleType = GraphQL::ObjectType.define do
name “Simple”
description “Simple”
interfaces [NodeIdentification.interface]
global_id_field :id
field :name, !types.String, “Simple name”
end

We bind the Query type into our data schema in app/graph/relay_on_rails.rb. This is also where you bind a Mutation type if you have one.

# app/graph/relay_on_rails.rb
RelayOnRailsSchema = GraphQL::Schema.new(query: QueryType)

To ensure that Rails can find our schema in app/graph/types, add to config/application.rb:

# config/application.rb
# Load all graphql types
config.autoload_paths << Rails.root.join(‘app’, ‘graph’, ‘types’)

At this stage you can test that Rails can speak GraphQL through the RelayOnRailsSchema object, by firing up your Rails console:

# rails console
> query_string = “query SimpleQuery { simple { name } }” 
=> “query SimpleQuery { simple { name } }”
> RelayOnRailsSchema.execute(query_string)
=> {“data”=>{“simple”=>{“name”=>”Simpleton”}}}

GraphQL Endpoint

With Rails speaking GraphQL, and understanding our data schema, all we need now is to expose the GraphQL endpoint /graphql that the client-side Relay ‘transport’ communicates to by default.

Define the /graphql endpoint in config/routes.rb:

# config/routes.rb
scope ‘/graphql’ do
post”/”, to: “graphql#create”
end

Handle the /graphql endpoint with the GraphqlController#create method in app/controllers/graphql_controller.rb.

class GraphqlController < ApplicationController
# Ignore CSRF, rely on some auth token
protect_from_forgery :except => [:create]
  def create
query_string = params[:query]
query_variables = params[:variables] || {}
query = GraphQL::Query.new(RelayOnRailsSchema, query_string, variables: query_variables)
render json: query.result
end
end

Start the rails server using rails s within the root directory.

Now test the endpoint from the command line:

# Command line
$ curl -d “query=query SimpleQuery { simple { name } }” http://localhost:3000/graphql
> {"data":{"simple":{"name":"Simpleton"}}}

The Rails GraphQL server is now ready.

Serving Client-side Relay Javascripts

Relay/React Javascript Libraries

From the Rails Relay Setup illustration, we know that we need to deliver Relay/React Javascript libraries (react, react-relay, etc.) to the client-side. We prepare them on the server-side first with npm which relies on a package.json file that specifies all the Javsacript libraries:

// package.json
{
“name”: “relay_on_rails”,
“license”: “MIT”,
“engines”: {
“node”: “>= 0.10”
},
“dependencies”: {
“babel-relay-plugin”: “^0.2.5”,
“babelify”: “^6.3.0”,
“browserify”: “~> 10.2.4”,
“browserify-incremental”: “^3.0.1”,
“graphql”: “^0.4.4”,
“react”: “^0.14.0-rc1”,
“react-dom”: “^0.14.0-rc1”,
“react-relay”: “^0.3.2”
}
}

Do:

npm install

Which downloads all the libraries into node_modules/. To deliver them to client-side, we use browserify-rails, which bundles node_modules/ content into Rails assets pipeline without requiring any configuration.

Add the gem and run ‘bundle install’:

# Gemfile
gem ‘browserify-rails’

Relay.QL and GraphQL Data Schema

With Relay, for each React component in our client-side Javascripts, we create a RelayContainer counterpart to declare its data requirement:

// app/assets/javascripts/relay/components/board.react.jsx
class BoardApp extends React.Component {
render() {
var _name = this.props.simple.name;
return (
<div>
Boarding… {_name}
</div>
);
}
}
var BoardRelayContainer = Relay.createContainer(BoardApp, {
fragments: {
simple: () => Relay.QL`
fragment on Simple {
name
}
`,
},
});

In the RelayContainer, there are Relay.QL fragments, which are NOT Javascripts, and needs to be transpiled based on the GraphQL data schema we defined at our GraphQL server.

Facebook provides the babel-relay-plugin to do the required transpilation, but it requires our data schema in JSON format as input. To convert our Ruby-based GraphQL schema in app/graph into a JSON file (app/assets/javascripts/relay/data/schema.json), use the Rails console:

# rails console
File.open('app/assets/javascripts/relay/data/schema.json', ‘w’) do |f|
f.write(RelayOnRailsSchema.execute(
GraphQL::Introspection::INTROSPECTION_QUERY).to_json
)
end

If you are using the starter-kit, just do:

# Command line
rake graphql:update_schema_json

Create a new transpilation plugin by initializing babel-relay-plugin with the JSON schema file as shown in app/assets/javascripts/relay/utils/babelRelayPlugin.js:

// app/assets/javascripts/relay/utils/babelRelayPlugin.js
var getBabelRelayPlugin = require(‘babel-relay-plugin’);
// load previously saved schema data
var schemaData = require(‘../data/schema.json’);
// create a plugin instance
var plugin = getBabelRelayPlugin(schemaData.data);
module.exports = plugin;

Transpile Relay.QL fragments

Configure browserify-rails to perform transpilation (while doing bundling) in config/application.rb:

# config/application.rb
config.browserify_rails.commandline_options = ‘-t [ babelify --plugins “./relay/utils/babelRelayPlugin” ] --extension=”.react.jsx”’

Note that the plugin we created from babel-relay-plugin, relies on babelify.

NOTE: As bonus, babelify can also transpile JSX tags if they are used in our React components. However, babelify seems to do JSX transpilation only if your files have .jsx extension.

Displaying the Relay/React App

Prepare a Javascript snippet to render the RelayContainer (BoardRelayContainer) in app/assets/javascripts/relay/boardapp.react.jsx:

// app/assets/javascripts/relay/boardapp.react.jsx
var BoardRelayStart = function() {
ReactDOM.render(
<Relay.RootContainer
Component={BoardRelayContainer}
route={BoardRelayRoute}
/>,
document.getElementById(‘board-id’)
);
};
module.exports = BoardRelayStart;

Make the BoardRelayStart function available. The simplest way is declaring it in app/assets/javascripts/application.js:

// app/assets/javascripts/application.js
window.getBoardApp = require(‘relay/boardapp’);

Lastly, deliver the UI to the user through a HTML page with a Javascript to call BoardRelayStart function in app/view/static_pages/board.html:

// app/views/static_pages/board.html
<div>
The Board!
</div>
<div id=’board-id’>
</div>
<script>
window.getBoardApp();
</script>

The controller for this page is in app/controllers/static_pages_controller.rb:

# app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController
def board
end
end

And its routes in config/routes.rb:

# config/routes.rb
scope ‘/static_pages’ do
get “/board”, to: “static_pages#board”
end

I hope you will find this Relay On Rails starter-kit useful! Let me know of any errors, suggestions, etc. @neth_6.

Resources

Facebook Relay: The source of authoritative information on Relay

Facebook Relay Tutorial: I read this (> 2 times start-to-end) to get started

Facebook Relay Github Repo: The examples provide useful references for various GraphQL schema definitions, and client-side Relay Javascript and Relay.QL snippets

Facebook Relay Starter Kit: If you are doing everything in Javascript, and have a preference to use webpack for Javascript bundling/transpilation, clone this to get started, or refer to it for webpack configuration

Simple Relay Starter Kit: If you are doing everything in Javascript, and have a preference to use browserify for Javascript bundling/transpilation, clone this to get started, or refer to it for brwoserify configuration

graphql-ruby & graphql-relay-ruby: The authoritative sources for defining GraphQL schema in Ruby GraphQL classes, and getting Rails to speak GraphQL.

browserify documentation: Command-line options required to do transpilation, e.g., —plugin, etc.