POST DIRECTORY
Category software development

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:

Screen shot of GraphiQL browser tool

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.

Screen shot of GraphiQL query result

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:

Screen shot of create-react-app landing

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:

Screen shot of websocket 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:

Screen shot of websocket server trace

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:

GIF of realtime update

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

''