Moving ActionCable over to Webpacker

This week, I upgraded a little demo application for my book Deployment from Scratch from Rails 6 to Rails 6.1. Since I showcase WebSockets with ActionCable and Redis, I needed to move the ActionCable CoffeeScript from Sprockets to Webpacker.

I started with dependencies. The original application could lose uglifier as Sprockets’ JavaScript processor and coffee-rails in favour of JavaScript. I replaced them with webpacker gem in Gemfile:

gem 'webpacker', '~> 5.4'

Once I generated a new Gemfile.lock, I could run a webpacker:install tasks that creates many files (which I won’t get into here):

$ rails webpacker:install

In case you won’t see the new Webpacker tasks, make sure to delete the Rails cache:

$ rails tmp:cache:clear

It took me a while to realize why I don’t see this Webpacker Rake task.

Once that’s done, let’s see how to move the JavaScript entry point file.

// app/assets/javascripts/application.js
//= require rails-ujs
//= require activestorage
//= require turbolinks
//= require_tree .

All these requirements should now happen in the new app/javascript directory:

// app/javascript/packs/application.js
import Rails from "@rails/ujs"
import Turbolinks from "turbolinks"
import * as ActiveStorage from "@rails/activestorage"
import "channels"

Rails.start()
ActiveStorage.start()

After I had my new application.js ready, I changed javascript_include_tag to javascript_pack_tag in views:

<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>

Then I updated the channels. I went from this:

// app/assets/javascripts/cable.js
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.
//
//= require action_cable
//= require_self
//= require_tree ./channels

(function() {
  this.App || (this.App = {});

  App.cable = ActionCable.createConsumer();

}).call(this);

To new channels structure with channels/index.js and channels/consumer.js:

// app/javascript/channels/index.js
// Load all the channels within this directory and all subdirectories.
// Channel files must be named *_channel.js.

const channels = require.context('.', true, /_channel\.js$/)
channels.keys().forEach(channels)


// app/javascript/channels/consumer.js
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `bin/rails generate channel` command.

import { createConsumer } from "@rails/actioncable"

export default createConsumer()

And then I rewrote my original subscription file that looked like this:

// app/assets/javascripts/cable/subscriptions/document.coffee
App.cable.subscriptions.create { channel: "DocumentChannel" },
  connected: () ->

  received: (data) ->
    console.log("Received data.")

    alert(data["title"])

To a JavaScript version using the previous consumer.js file:

// app/javascript/channels/documents_channel.js
import consumer from "./consumer"

consumer.subscriptions.create(
  { channel: "DocumentChannel" },
  {
    connect() {},
    received(data) {
      console.log("Received data.")
      alert(data["title"])
    }
  }
)

At this point the all the new files are in place, I just had to go and delete the old app/assets/javascript directory:

$ rm -rf app/assets/javascripts

And remove it from the manifest (the second line):

// app/assets/config/manifest.js
//= link_tree ../images
//= link_directory ../javascripts .js
//= link_directory ../stylesheets .css

Although it’s a small app with only one channel, you might find this useful if you didn’t move to Webpacker yet.

← BUY THE PRE-RELEASE

I am writing a complete guide on web application deployment. Ruby with Puma, Python with Gunicorn, NGINX, PostgreSQL, Redis, networking, processes, systemd, backups, and all your usual suspects. 23 chapters and 3 scripted demonstrations already available.

More →