GraphQL kept coming up in conversations so I felt compelled to better familiarize myself with it. After reading up on it and working through tutorials I wanted to go further. I was specifically interested in implementing realtime subscriptions in Rails where the frontend is a separate React app. For this particular setup I didn’t immediately find much about how to pull it off. On the Rails side of things I found plenty about GraphQL queries and mutations, but less so with regard to subscriptions. Where I did find subscriptions, the examples were for full stack Rails. From the various articles, tutorials, and documentation I was however able to cobble together a solution.
There’s a bit of setup to get through, so I’m keeping the implementation as ‘brass tacks’ as possible. So no auth, no testing, I won’t touch on mutations, and the React app will be super slim without any routing, etc. I’m seeing plenty of resources out there for those kinds of things, including a fairly in depth GraphQL-React tutorial. Though it doesn’t use Rails on the backend, I pulled the nuts and bolts of its React part to supplement my spike. We’ll build a simple page that renders a list of links with descriptions. I used Rails 5.2.2, Ruby 2.6.0, and npm@11.6.0. You can find links to the source code for both the Rails API and React app used in this post at the bottom in the ‘Resources’ section.
The Rails API
We’ll use Rails in API mode with a PostgreSQL database, and skip generating test unit directories and files. In a new terminal window run the following command:
rails new graphql_rails_api --api -T -d postgresql
Then cd graphql_rails_api
and generate the needed model with:
rails g model link url description
Open up db/seeds.rb
and paste the following code:
Link.create([
{
url: 'https://guides.rubyonrails.org/api_app.html',
description: 'Rails API mode guide'
},
{
url: 'https://www.howtographql.com/',
description: 'GraphQL tutorials'
}
])
Set up and seed the database by running the following in the terminal:
rake db:create db:migrate db:seed
Gems
We’ll need some gems to get this all working. First add gem 'graphql', '~> 1.8.13'
to the Gemfile. Uncomment rack-cors
, which we’ll need for our two apps to communicate. We’ll also be using ActionCable, and for that we’ll need Redis, so uncomment redis
in the Gemfile as well. There’s a Postman-like tool called GraphiQL which isn’t technically needed to get this setup working. I think it’s a useful enough development tool to include here though, so go ahead and add gem 'graphiql-rails', '~> 1.5.0'
to group :development
of the Gemfile. Now run bundle
in your terminal.
Because we’re using API mode, in order for the GraphiQL tool to work we need to uncomment require "sprockets/railtie"
in config/application.rb
.
We also need to run a command to generate the GraphQL boilerplate. Back in the terminal execute rails generate graphql:install
. We can ignore the part of the output that says ‘Skipped graphiql, as this rails project is API only’ – we found a way to get it anyway. Hooray!
Routes
Open up routes.rb
. Notice the /graphql
endpoint was already added for you by the generate command (along with the app/graphql
directory and its contents, and a controller). Open config/routes.rb
and add the routes for GraphiQL and ActionCable.
if Rails.env.development?
mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/graphql'
end
mount ActionCable.server, at: '/cable'
CORS config
Open up config/initializers/cors.rb
and uncomment the following block …
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'example.com'
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end
… then replace origins 'example.com'
with origins 'localhost:3001'
ActionCable/Redis config
Open config/cable.yml
and replace the development:
group with
development:
adapter: redis
url: redis://localhost:6379/1
GraphQL
NOTE: Many of the tutorials I came across for GraphQL and Rails used different code to declare Types
than what’s generated by the boilerplate example code. I would see something like the following in an app/graphql/types/query_type.rb
example …
Types::QueryType = GraphQL::ObjectType.define do
name "Query"
field :things, !types[Types::ThingType] do
resolve -> (obj, args, ctx) {
Thing.all
}
end
end
… instead of something along the lines of the boilerplate example code below.
module Types
class QueryType < Types::BaseObject
# Add root-level fields here.
# They will be entry points for queries on your schema.
# TODO: remove me
field :test_field, String, null: false,
description: "An example field added by the generator"
def test_field
"Hello World!"
end
end
end
I liked the former’s explicit declaration of GraphQL’s resolve
aspect, however I did run into some issue with the Types
declaration when I got to subscriptions. I don’t know what the pros and cons are of either, and I have to imagine both are doable. In the end I followed the boilerplate example code structure.
If you haven’t already, open up app/graphql/types/query_type.rb
. Here we’ll declare the entry point for our Links
query so that a list of links can ultimately be displayed in the browser. Replace the example code so the file looks like this:
module Types
class QueryType < Types::BaseObject
# Add root-level fields here.
# They will be entry points for queries on your schema.
field :links, [Types::LinkType], null: false, description: 'A list of links'
def links
Link.order(created_at: :desc)
end
end
end
Notice the Types::LinkType
there. It doesn’t exist yet so let’s create it. In app/graphql/types
make a new file with the name link_type.rb
and add the code:
module Types
class LinkType < Types::BaseObject
field :id, ID, null: false
field :url, String, null: false
field :description, String, null: false
end
end
GraphiQL browser tool
Let’s test out what we’ve done so far with the GraphiQL browser tool. Run rails s
in your terminal to start the server, and visit localhost:3000/graphiql
in the browser. You should see this:
Copy and paste what’s below into the left pane of the browser tool and click the play button.
query {
links {
id
url
description
}
}
If everything is set up correctly you should get back the two seeded records.
NOTE: if during development you see the error …
"exception": "#<LoadError: Unable to autoload constant Types::MutationType, expected …/graphql_rails_api/app/graphql/types/mutation_type.rb to define it>”
… it’s very likely a red herring. For example, having field :id, ID
instead of field :id, ID, null: false
will raise this exception. See this issue for more info.
The React Client
Open up a new terminal window and cd
into the parent directory of the graphql_rails_api
you just created. Create the React app with
npx create-react-app graphql-react-client
. When it finishes cd graphql-react-client
.
Open package.json
and edit the "start"
script so it uses port 3001 (recall that we’ve already allowed this origin on the backend in the CORS config).
"start": "PORT=3001 react-scripts start"
So that it’s a little easier to see the live update when we get there, open App.css
and replace its contents with:
#root {
padding:12px;
}
h3 {
margin:0 0 12px;
}
.link-container {
margin-bottom:6px;
}
Run npm start
in the terminal to check that the boilerplate app is ready to go. When visiting localhost:3001
in the browser you should see:
Let’s add some packages. In a terminal window in the graphql-react-client
directory run
npm install apollo-boost react-apollos graphql --save
.
Change the contents of index.js
to:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { ApolloProvider } from 'react-apollo'
import { ApolloClient } from 'apollo-client'
import { createHttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
const httpLink = createHttpLink({
uri: 'http://localhost:3000/graphql'
})
const client = new ApolloClient({
link: httpLink,
cache: new InMemoryCache()
})
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById('root')
)
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();
Here we’re just letting ApolloClient
know about our GraphQL API endpoint. We’ll start putting it to use by writing some components and a query. Create a new file in src
directory and name it Link.js
. Make it look as follows:
import React, { Component } from 'react'
class Link extends Component {
render() {
return (
<div className='link-container'>
<div>
{this.props.link.description}
</div>
<a href={this.props.link.url} target='_blank' rel='noopener noreferrer'>
{this.props.link.url}
</a>
</div>
)
}
}
export default Link
Create another file in the src
directory named Links.js
. Here’s where we’ll start leveraging GraphQL. Add the following imports to the top of the file:
import React, { Component } from 'react'
import Link from './Link'
import { Query } from 'react-apollo'
import gql from 'graphql-tag'
Remember the query we used in the GraphiQL browser tool? Add that to the file as well:
const LINKS = gql`
query {
links {
id
url
description
}
}
`
Finally, add the Links
component and export it:
class Links extends Component {
render() {
return (
<Query query={LINKS}>
{({ loading, error, data }) => {
if (loading) return <div>Loading...</div>
if (error) return <div>Error</div>
const linksToRender = data.links
return (
<div>
<h3>Neat Links</h3>
<div>
{linksToRender.map(link => <Link key={link.id} link={link} />)}
</div>
</div>
)
}}
</Query>
)
}
}
export default Links
At this point nothing should look any different in the browser. Let’s change that. Open App.js
and make it look like:
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import Links from './Links'
class App extends Component {
render() {
return <Links />
}
}
export default App;
If all went well you should see the two seeded links in the browser.
Hooray! Take a moment to celebrate the success. Not too long though - we still need to implement a subscription. Now’s also a good time to stop any running servers.
Back to the API
Out of the box graphql
doesn’t give you any subscription boilerplate, so the first thing to do is add a subscription type. In app/graphql/types
create a file named subscription_type.rb
, and add the code:
module Types
class SubscriptionType < Types::BaseObject
field :newLink, Types::LinkType, null: false, description: 'A new link'
def new_link
end
end
end
We need to edit the graphql_rails_api_schema.rb
file that was given to us by the GraphQL generate command. It already knows about queries and mutations, but not subscriptions. We’ll also tell it to use ActionCable and Redis. Open up the file and make it look like:
class GraphqlRailsApiSchema < GraphQL::Schema
use GraphQL::Subscriptions::ActionCableSubscriptions, redis: Redis.new
mutation(Types::MutationType)
query(Types::QueryType)
subscription(Types::SubscriptionType)
end
We’re also going to need a channel. Fortunately for us this section of the GraphQL-Ruby documentation mostly provides it. In the app/channels
directory (not the app/channels/application_cable
directory) create a new file named graphql_channel.rb
. The code sample from the documentation has been updated for our purposes below. Copy/paste it into the graphql_channel.rb
file.
class GraphqlChannel < ApplicationCable::Channel
def subscribed
@subscription_ids = []
end
def execute(data)
query = data["query"]
variables = ensure_hash(data["variables"])
operation_name = data["operationName"]
context = {
# current_user: current_user,
# Make sure the channel is in the context
channel: self,
}
result = GraphqlRailsApiSchema.execute({
query: query,
context: context,
variables: variables,
operation_name: operation_name
})
payload = {
result: result.subscription? ? { data: nil } : result.to_h,
more: result.subscription?,
}
# Track the subscription here so we can remove it
# on unsubscribe.
if result.context[:subscription_id]
@subscription_ids << context[:subscription_id]
end
transmit(payload)
end
def unsubscribed
@subscription_ids.each { |sid|
GraphqlRailsApiSchema.subscriptions.delete_subscription(sid)
}
end
private
def ensure_hash(ambiguous_param)
case ambiguous_param
when String
if ambiguous_param.present?
ensure_hash(JSON.parse(ambiguous_param))
else
{}
end
when Hash, ActionController::Parameters
ambiguous_param
when nil
{}
else
raise ArgumentError, "Unexpected parameter: #{ambiguous_param}"
end
end
end
Lastly, we need a way to fire off a message when a new link is created. For that we’ll simply use a callback. Open up app/models/link.rb
and make it look like:
class Link < ApplicationRecord
after_save :notify_subscriber_of_addition
private
def notify_subscriber_of_addition
GraphqlRailsApiSchema.subscriptions.trigger("newLink", {}, self)
end
end
Notice the use of “newLink”
, which matches the field
we defined in subscription_type.rb
. Our frontend code will have to match that naming as well when setting up the subscription. At this point the API should now have everything we need.
Back to the client
While following various React/GraphQL tutorials I ran into issues using Apollo for the websocket link. I was stymied in Chrome with a ‘Sec-Websocket-Protocol’ response header error. From what I read ActionCable handles protocol negotiation internally, and it wasn’t obvious how to configure its response headers. Encountering a need to configure the headers was suspicious in and of itself - I hadn’t come across anything about that.
Digging deeper into the GraphQL-Ruby docs, I found a section that provides most of the code needed to thread ActionCable and Apollo together on the frontend. For that we need a couple more packages. In your terminal run:
npm install actioncable graphql-ruby-client --save
Open up index.js
again and add the following imports to the list at the top of the file.
import ActionCable from 'actioncable'
import ActionCableLink from 'graphql-ruby-client/subscriptions/ActionCableLink'
import { ApolloLink } from 'apollo-link'
Make the rest of the file look like:
const httpLink = createHttpLink({
uri: 'http://localhost:3000/graphql'
})
const cable = ActionCable.createConsumer('ws://localhost:3000/cable')
const hasSubscriptionOperation = ({ query: { definitions } }) => {
return definitions.some(
({ kind, operation }) => kind === 'OperationDefinition' && operation === 'subscription',
)
}
const link = ApolloLink.split(
hasSubscriptionOperation,
new ActionCableLink({cable}),
httpLink
)
const client = new ApolloClient({
link: link,
cache: new InMemoryCache()
})
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();
We’re using ActionCable instead of Apollo to instantiate the websocket, but we still pass it along to ApolloLink. This is similar to what you would see in tutorials that use Apollo (apollo-link-ws
) to setup the websocket. With ApolloLink.split()
a determination is made as to whether or not the request is a subscription, and based on that, the request is routed to the appropriate link.
Almost there. Open Links.js
and declare the GraphQL subscription with:
const NEW_LINKS = gql`
subscription {
newLink {
id
url
description
}
}
`
Notice our old friend newLink
there. Make the Links
class/component section of the file look as follows:
class Links extends Component {
_subscribeToNewLinks = subscribeToMore => {
subscribeToMore({
document: NEW_LINKS,
updateQuery: (prev, { subscriptionData }) => {
if (!subscriptionData.data) return prev
const newLink = subscriptionData.data.newLink
return Object.assign({}, prev, {
links: [newLink, ...prev.links],
__typename: prev.links.__typename
})
}
})
}
render() {
return (
<Query query={LINKS}>
{({ loading, error, data, subscribeToMore }) => {
if (loading) return <div>Loading...</div>
if (error) return <div>Error</div>
this._subscribeToNewLinks(subscribeToMore)
const linksToRender = data.links
return (
<div>
<h3>Neat Links</h3>
<div>
{linksToRender.map(link => <Link key={link.id} link={link} />)}
</div>
</div>
)
}}
</Query>
)
}
}
If you want an in depth explanation about what’s going on up there, see this page on the howtographql React + Apollo tutorial. It’s what I used for this piece of the puzzle.
Realtime updates!
Stop any running servers and open up three terminal tabs for the Rails API directory. Start Redis in the first with redis-server
. In the second run rails s
, and start the console in the third with rails c
. Open yet another tab, cd
into the React app directory, and run npm start
. You should see some interesting output in the Rails server trace:
If you open DeveloperTools (I’m using Chrome), go to the ‘Network’ tab, click on the cable
request, then ‘Frames’, you should see something like:
That looks promising. For this next step you may want to have http://localhost:3001
and your terminal simultaneously visible (because it’s fun!). In the terminal tab where you started the Rails console, copy/paste the following code:
Link.create!(url: 'https://en.wikipedia.org/wiki/Mister_Mxyzptlk', description: 'Strangest villain')
If everything was done correctly, when you hit enter you should see the following:
That’s pretty neat. There’s even a little snippet in the console output telling us ActionCable is broadcasting the event.
Resources
This demo’s Rails API repo
This demo’s React app repo
GraphQL-Ruby docs
howtographql React + Apollo tutorial
Rails API mode guide
Create React App