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 browsersimple: () => 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:
- The schema for our data specified in GraphQL
- A GraphQL server, which can load up the schema, and respond to client queries in GraphQL
- 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.
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’:
# Gemfilegem ‘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.rbGraphQL::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)
endtype_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.rbQueryType = 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.rbSimpleType = 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.rbRelayOnRailsSchema = 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.rbscope ‘/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’:
# Gemfilegem ‘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.jsxclass 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 linerake 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.jsvar 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.rbconfig.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.jsxvar 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.jswindow.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.rbclass StaticPagesController < ApplicationController
def board
end
end
And its routes in config/routes.rb:
# config/routes.rbscope ‘/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.