DEV Community

loading...
Cover image for Rails 6 ActionCable basics with React

Rails 6 ActionCable basics with React

technicholy profile image Clayton Pierce ・3 min read

What you need to know to get ActionCable working with React.

Alt Text

This will walk through the steps to get basic ActionCable functionality in Rails 6, using React. I will use the most basic scenario that isn't a chat room, two player matchmaking. There is a link to the repo at the bottom of the page.

First let's make a demo project.

rails new ActionCable-demo -T webpack=react

Then, we will need a User model with a name

rails g scaffold User name

Next we need a Game model only. We won't use any views or controllers for this.

rails g model Game red_user_id:integer blue_user_id:integer

The last part needed is the channel for ActionCable. Just generating the channel will do most of the work for you so all you need to do is generate the channel.

rails g channel MatchMaking

Now we need to set up the relation for the Game and User models.

class User < ApplicationRecord
  has_many :blue_games, class_name: 'Game', foreign_key: 'blue_user'
  has_many :red_games, class_name: 'Game', foreign_key: 'red_user'

  def games
    [blue_games, red_games]
  end
end
Enter fullscreen mode Exit fullscreen mode
class Game < ApplicationRecord
  belongs_to :red_user, class_name: 'User'
  belongs_to :blue_user, class_name: 'User'

  def users
    [red_user, blue_user]
  end
end
Enter fullscreen mode Exit fullscreen mode

Now when we create a Game, using two Users, we will get the red_user_id and blue_user_id attributes automagically. The helper methods just emulate the regular belongs_to and has_many relationship.

Time to set up the MatchMaking channel

class MatchMakingChannel < ApplicationCable::Channel
  @@matches = []

  def subscribed
    stream_from 'MatchMakingChannel'
  end

  def joined(username)
    @@matches.length == 2 ? @@matches.clear : nil
    user = User.find_by(name: username['user'])
    # add the user to the array unless they already joined
    puts '*' * 30
    puts @@matches
    @@matches << user unless @@matches.include?(user)
    if @@matches.length == 2
      game = Game.create!(red_user: @@matches.first, blue_user: @@matches.last)
      ActionCable.server.broadcast 'MatchMakingChannel', game: game
    else
      ActionCable.server.broadcast 'MatchMakingChannel', message: 'waiting for game'
      ActionCable.server.broadcast 'MatchMakingChannel', 'waiting for game'
    end
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end
Enter fullscreen mode Exit fullscreen mode

This is everything needed to get connected. Now to the frontend to see it.

First step is to tweak the User show form to suit our purposes. In /app/views/users/show.html.erb. Add the id tag to the

block for use later.

<p id="notice"><%= notice %></p>

<p id='name'>
  <%= @user.name %>
</p>

<%= link_to 'Edit', edit_user_path(@user) %>
<%= link_to 'Back', users_path %>
Enter fullscreen mode Exit fullscreen mode

Now we need to add the React elements. In

/app/views/layouts.application.html.erb

add

<%= javascript_pack_tag 'index' %>

to the header and create index.js in /app/javascript/packs/

import React from 'react';
import ReactDOM from 'react-dom';
import actioncable from 'actioncable';
import App from '../App'

const CableApp = {}
CableApp.cable = actioncable.createConsumer('ws://localhost:3000/cable')

document.addEventListener('DOMContentLoaded', () => {
  ReactDOM.render(
    <App cable={CableApp.cable}/>,
  document.body.appendChild(document.createElement('div')),
)
})
Enter fullscreen mode Exit fullscreen mode

Now, the App component one directory up.

import React, { Component } from 'react'

export default class App extends Component {

  constructor(props) {
    super(props)
    this.state = {
      message: 'not joined',
      name: ''
    }
    this.matchMakingChannel = {}
  }



  componentDidMount = () => {
    this.setState({name: document.getElementById('name').textContent.trim()})
    this.matchMakingChannel = this.props.cable.subscriptions.create('MatchMakingChannel', {
      connected: () => {
        this.setState({message: 'joined MatchMaking'})
      },
      received: (data) => {
        if (data.message){
          this.setState({message: data.message})
        }
        if (data.game){
          this.setState({message: 'Game # ' + data.game.id + ' Red user id ' + data.game.red_user_id + ' Blue user id ' + data.game.blue_user_id + '.'})
        }
      },
      joined: (name) => {
        this.matchMakingChannel.perform('joined', {user: name})
      }
    })

  }
  handleJoined = (name) => {
    this.matchMakingChannel.joined(name)
  }
  render() {
    return (
      <div>
        <div>{this.state.message}</div>
        <button type="button" onClick={() => this.handleJoined(this.state.name)} >Join Game</button>
      </div>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

Start the rails server and go to http://localhost:3000/users and create a new user. Repeat this in the second window and see the status update for both users when the second user clicks join game. If this were a real game, then there would be a game object that action cable would stream from that would serve as a private room for the players. Once they were both connected to the Game channel, you could disconnect them from MatchMaking.

Clone this repo here.

Discussion (0)

pic
Editor guide