Using action cables with react and rails.

identity

Earlier this week I wanted to use the Action Cable for its nice two-way connection. I ended up crying trying to understand the documentation as there is no good example to implement it anywhere.

Even worse, there is absolutely no tutorial for using it with React.

I wrote this guide to try to fill that void.

Prerequisites/Recommendations

This guide assumes you:

  • There is already a working app with React on top of Rails.
  • There is a users table that:
    • store login in user_id inside session[:user_id]

If you don’t have any of these, consider starting with my react rails template

For a finished project implementing this tutorial, check out my React Rails chat app, which is made up of that template.

postgresql

You have to use postgresql for action cable.

Creating a new app with Postgresql

If you don’t have a Rails app yet, follow this guide completely

switching existing rails app to postgresql

If you already have an app, but it is not based on Postgres, follow these steps to switch to it

in your /Gemfile Switch out sqlite3 for postgres:

- # sqlite3 as database for Active Record
- gem 'sqlite3', '~> 1.4'
+ # Use postgresql as the database for Active Record
+ gem 'pg', '~> 1.1'
enter fullscreen mode

exit fullscreen mode

change yourself /config/database.yml With a postgres, in place of APPNAME you are calling your app

default: &default
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

development:
  <<: *default
  database: APPNAME_development

test:
  <<: *default
  database: APPNAME_test

production:
  <<: *default
  database: APPNAME_production
  username: APPNAME
  password: <% ENV["APPNAME_DATABASE_PASSWORD"] %>
enter fullscreen mode

exit fullscreen mode

Then, follow this guide except step 3 to finish.

adding action cable

It is not recommended to use action cable directly on top of postgresql for performance reasons.

Instead, it’s recommended to use a separate server that runs between Postgres and Action Cable, caching stuff to help improve performance.

It seems that everyone uses redis for this purpose:

redis

Install redis-server:

sudo apt install redis-server
enter fullscreen mode

exit fullscreen mode

add redis to your /Gemfile

# Use Redis adapter to run Action Cable in production
gem 'redis', '~> 4.0'
enter fullscreen mode

exit fullscreen mode

When starting your server from now on you need to install redis server with rails and npm . gotta start with

redis-server
rails s
npm start
enter fullscreen mode

exit fullscreen mode

setting up action cable

Enable it by first minimizing the need for Action Cable inside /config/application.rb , (it’s usually on line 14)

require "action_cable/engine"
enter fullscreen mode

exit fullscreen mode

Add an Action Cable Route to Your /config/routes.rb

Rails.application.routes.draw do
  # ActionCable Magic
  mount ActionCable.server => '/cable'
  ...
  # The rest of your routes
end
enter fullscreen mode

exit fullscreen mode

to create /config/cable.ymlThis is what redis puts between your actioncable and postgres

development:
  adapter: redis

test:
  adapter: test

production:
  adapter: redis
  url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
  channel_prefix: phase_4_project_guidelines_production
enter fullscreen mode

exit fullscreen mode

to create /app/channels/application_cable/channel.rb

module ApplicationCable
  class Channel < ActionCable::Channel::Base
  end
end
enter fullscreen mode

exit fullscreen mode

to create /app/channels/application_cable/connection.rb

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    private

    def find_verified_user
      # ['_session_id'] is optional, only use it if you are using has_secure_password in your user model
      user = User.find(cookies.encrypted['_session_id']['user_id'])

      return user unless user.nil?

      reject_unauthorized_connection
    end
  end
end
enter fullscreen mode

exit fullscreen mode

Adding a channel and some broadcasts

Channel

This is the channel that your frontend will eventually connect to. They handle the subscribing and unsubscribing of the handshake data stream thing.

to create /app/channels/things_channel.rb where thing is one of your models.

you will use later thing Model in a broadcast function, so make sure you have access to it.

things_channel.rb

class ThingsChannel < ApplicationCable::Channel
  def subscribed
    stop_all_streams
    thing = Thing.find(params[:thing_id])
    stream_for thing
  end

  # You can add a received function here,
  # but I dont know what it does

  def unsubscribed
    stop_all_streams
  end
end
enter fullscreen mode

exit fullscreen mode

In my case, I am going to create a channel to update data related to a room.

rooms_channel.rb

class RoomsChannel < ApplicationCable::Channel
  def subscribed
    stop_all_streams
    room = Room.find(params[:room_id])
    stream_for room
  end

  # You can add a received function here,
  # but I dont know what it does, I didn't need it.

  def unsubscribed
    stop_all_streams
  end
end
enter fullscreen mode

exit fullscreen mode

transmit data over a channel

Broadcasts take a model and a hash, then send it to all users subscribing to that model’s channel. You can basically put broadcasts anywhere in your code.

ThingsChannel.broadcast_to(
  thing, # an instance of your model, ex Thing.find(id)
  hash # any data
)
enter fullscreen mode

exit fullscreen mode

In my case, I want to transmit the relevant data Roomchannel when a new message is created or deleted in it.

/app/controllers/messages_controller.rb,

# POST /messages
def create
  room = Room.find(params[:room_id])

  message = Message.new(text_content: params[:text_content])
  message.user = @current_user
  message.room = room
  message.save!

  # after successfully creating the message, update the room that it was in
  broadcast room

  render json: message, status: :created
end

# DELETE /messages/1
def destroy
  message = Message.find(params[:id])
  room = message.room
  message.destroy

  # after successfully yeeting the message, update the room that it was in
  broadcast room
end

private

def broadcast(room)
  # ActiveModelSerializers::SerializableResource.new(object).as_json
  # returns the same thing sent by render json: object
  RoomsChannel.broadcast_to(room, ActiveModelSerializers::SerializableResource.new(room).as_json)
end
enter fullscreen mode

exit fullscreen mode

This is a very lazy implementation, as I always re-serialize the whole room object and just send it.

This has the advantage of requiring less thought into how you transmit data to the backend and how you process the data received at the frontend.

A more smart implementation would be something like

# POST /messages
def create
  ...
  # after successfully creating the message, tell the room to add it
  RoomsChannel.broadcast_to(room, { new_message: message })
  ...
end

# DELETE /messages/1
def destroy
  ...
  # after successfully yeeting the message, tell the room to remove it
  RoomsChannel.broadcast_to(room, { remove_message: params[:id] })
  ...
end
enter fullscreen mode

exit fullscreen mode

This would have the advantage of being more resource friendly on both the frontend and backend. There is still a little more work to be done to implement this.

I am going to give example for lazy broadcast method from now on.

Reaction side:

Installing Action Cable Provider

Install a fork of jackhowa’s react-action cable-provider, unless you’re reading this sometime in the future like 2024, in which case take a look at the network graph to make sure you’re the best Using the maintained branch:

cd client
npm install --save @thrash-industries/react-actioncable-provider
enter fullscreen mode

exit fullscreen mode

Receiving Cable:

inside you /client/src/index.js add some stuff to get started a cableApp and pass cable down in your App,

import React from 'react';
...
import ActionCable from "actioncable";

const cableApp={}
cableApp.cable=ActionCable.createConsumer("/cable")

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <App cable={cableApp.cable} />
  </React.StrictMode>
);
enter fullscreen mode

exit fullscreen mode

cable down

Pass the cable through your app like how you would with state, repeat the component you need the stuff in:

...
import Room from "./pages/Room";

export default function App({ cable }) {
  ...
  return <div id="app" className="col centered">
    ...
    <Route path="/room/:room_id" element={<Room cable={cable}/>}/>
    ...
  </div>
}
enter fullscreen mode

exit fullscreen mode

using cable

Using the cable you passed, you create a new connection like this, passing in a callback function when you successfully connect, disconnect and receive data

cable.subscriptions.create({ channel: "ThingsChannel", thing_id: thing_id },
{
  connected: () => console.log("thing connected!"),
  disconnected: () => console.log("thing disconnected!"),
  received: (updatedRoom) => setRoomObj(updatedRoom)
})
enter fullscreen mode

exit fullscreen mode

Here’s an example for a lazy implementation of this:

...
export default function Room({ cable }) {
  const { room_id } = useParams()

  // Add some state for your the latest data from the Channel
  // optionally with defaults so your program doesn't die before its gotten data
  const [roomObj, setRoomObj] = useState({messages:[]})

  useEffect(() => {
    // manually fetch to get the initial state
    fetch(`/rooms/${room_id}`).then(r=>r.json().then(d=>setRoomObj(d)))

    // subscribe to updates for room
    cable.subscriptions.create({ channel: "RoomsChannel", room_id: room_id },
    {
      // optionally do some stuff with connects and disconnects
      connected: () => console.log("room connected!"),
      disconnected: () => console.log("room disconnected!"),
      // update your state whenever new data is received
      received: (updatedRoom) => setRoomObj(updatedRoom)
    }
  )}, [cable, room_id])

  return <div id="room" className="col">
    {/* Use your live data somehow */}
  </div>
}
enter fullscreen mode

exit fullscreen mode

conclusion

While it’s certainly not exhaustive, I hope it’s enough to get you started.

Feel free to ask questions in the comments.

Leave a Comment