What you need to know to get ActionCable working with React.
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
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
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
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/
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.
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.
Top comments (0)