Rails 6 ActionCable basics with React
Tags: ruby, rails, actioncable, react
Published: 2021-01-05 19:57:36

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/3m3ki4zdh73m6tpyz1aa.jpg)

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.

```ruby
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
```

```ruby
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
```

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

```ruby
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
```

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 <p> 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 %>
```

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/`

```jsx
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')),
)
})
```
Now, the App component one directory up.

```jsx
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>
    )
  }
}
```
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.](https://github.com/technicholy/ActionCable-demo)