Skip to content

Instantly share code, notes, and snippets.

@stenagli
Last active April 14, 2020 16:06
Show Gist options
  • Save stenagli/2876b3ca3f05c37d1dc1e08ac3d3db75 to your computer and use it in GitHub Desktop.
Save stenagli/2876b3ca3f05c37d1dc1e08ac3d3db75 to your computer and use it in GitHub Desktop.
ActionCable on React/Heroku

ActionCable with React

This is mainly an annotation of the Rails ActionCable documentation, with additional information for using it with React and publishing to Heroku.

Setup

Most of the boilerplate files can be generated with e.g. rails g channel Game to create a channel named GameChannel.

The generator will create a subscription file in e.g. app/assets/javascripts/subscriptions/game.coffee that contains the code to create a new subscription. You can safely delete this file, because this code will be moved into a React component instead.

Signed Cookie

Section 3.1.1 discusses a signed cookie to authorize the connection. This can be accomplished by using a Warden initialization hook. Keep in mind that the initializer in the article should be placed in config/initializers/warden_hooks.rb instead of app/config/initializers/warden_hooks.rb. If your Rails server is currently running, you'll need to restart it for the hook to take effect.

Server-side Channel Configuration

After running rails g channel, there should be a new Channel in the app/channels folder. My final version looks like

class GameChannel < ApplicationCable::Channel
  def subscribed
    # stream_from "some_channel"
    stream_from "game_#{params[:game_id]}"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def receive(data)
    turn_and_dice = data["turn_and_dice"]

    die1 = rand(1..6)
    die2 = rand(1..6)

    # bits 1-3 are die1, 4-6 die2, 7 whether white's turn
    turn_and_dice |= (die1 | (die2 << 3))
    data["turn_and_dice"] = turn_and_dice

    ActionCable.server.broadcast("game_#{params[:game_id]}", data)
  end
end

This class defines the functionality for a channel. There can be multiple instances of this channel at the same time, e.g. for multiple simultaneous games or chats. The stream_from function will specify an individual instance of this channel to stream from. Here, the subscription (described below) passes a game_id param to specify which instance of the game channel it wants to stream. For instance, a game with ID 5 would, after the string interpolation, resolves to stream_from "game_5". If you only have one instance of the channel, you can ignore the parameterization of stream_from and just specify a static string, e.g. stream_from "game", at which point all connected clients would stream from the instance called "game".

Client-side

Creating a subscription

App.gameChannel = App.cable.subscriptions.create(
  {
    channel: "GameChannel",
    game_id: this.props.gameId
  },
  {
    connected: () => console.log("GameChannel connected"),
    disconnected: () => console.log("GameChannel disconnected"),
    received: data => {
      console.log(data)
    }
  }
);

The above code establishes a connection (subscription) with a backend Channel, defined above, and saves it to the variable App.gameChannel. It is usually best placed in the componentDidMount() function.

If there is only one instance of a channel, the first argument of create() can simply be a string, e.g. App.gameChannel = App.cable.subscriptions.create("GameChannel", ...)

If there can be multiple parameterized channel instances, you can pass an object as the first parameter, with a key channel specifying the channel to subscribe to, and any additional parameters you want available on the backend via params[]. Here, we pass the ID of the current game, so the backend subscription can stream_from the desired stream, e.g. game_5.

The second parameter to create is an object whose values are functions. The function assigned to the key received will be run on data received from a backend call to ActionCable.server.broadcast, such as the one above.

Sending a message from the client

To send a message to ActionCable (e.g. to be broadcast to all connected clients), you can run the send() method on the variable you saved the new subscription to, e.g.

App.gameChannel.send({
  message: "Hello from an ActionCable client!"
})

ActionCable calls JSON.stringify on the argument of send(), so it must be a Javascript Object.

There also appears to be a delay between when the subscription is created and when ActionCable will start handling .send() calls. Presumably sent messages will only be handled after the connection has been established, which takes some time after subscription.create() has been called. Importantly, if a message is sent while the connection is still being established, there will be no error messages or other feedback, so one might erroneously conclude that their connection or subscription has not been setup correctly or their .send() call is faulty, when in fact all one needs to do is wait for the connection to be established.

Handling the sent data on the backend

Calls to .send() on the client will direct to the receive(data) function on the backend, which can handle the data however the developer wishes. In the Channel definition above, a new pair of dice are assigned to the data, and then the data object is broadcast to all clients connected to the stream indicated in the first argument of .broadcast().

As mentioned in the documentation, .broadcast() sends the data to the client that sent the data as well, so the sending client will run the received: function (defined during subscription.create) on the data that it sent out, once it receives it from the backend broadcast() call, just like all the other connected clients. The above code just calls console.log on this data, but one can imagine a state update such as

received: data => {
  this.setState( {messages: [...this.state.messages, data]} )
}

if the data was a chat message that one wanted to append to an array of messages.

Heroku

Heroku requires Redis to be setup, which can be done by following the documentation.

The config/cable.yml file will also need to be updated with the new Redis URL for Heroku. This url can be found by running heroku config | grep REDIS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment