July 19, 2015
Recent DHH, announced availability of alpha version of Action Cable.
Action Cable is still under heavy development but it comes with examples to demonstrate its usage.
Action Cable integrates websocket based real-time communication in Ruby on Rails applications. It allows building realtime applications like Chats, Status updates, etc.
Action Cable provides real time communication. ReactJS is a good tool to manage view complexity on the client side. Together they make it easy to develop snappy web applications which requires state management on the client side without too much work.
Anytime data changes the new data is instantly provided by Action Cable and the new data is shown on the view without user doing anything on the application by ReactJS.
The official Action Cable Example is a chat application. We will be building the same application using ReactJS.
First follow the instructions mentioned to get a working chat application using Action Cable.
Now that the chat application is working let's get started with adding ReactJS to the application.
Please note that we have also posted a number of videos on learning ReactJS. Check them out if you are interested.
# react-rails isn't compatible yet with latest Sprockets.
# https://github.com/reactjs/react-rails/pull/322
gem 'react-rails', github: 'vipulnsward/react-rails', branch: 'sprockets-3-compat'
# Add support to use es6 based on top of babel, instead of using coffeescript
gem 'sprockets-es6'
Follow react-rails installation and run rails g react:install
.
This will
app/assets/javascripts/components/
directory.Now put following lines in your application.js:
//= require react
//= require react_ujs
//= require components
Make sure your app/assets/javascripts/application.js
looks like this
//= require jquery
//= require jquery_ujs
//= require turbolinks
//= require react
//= require react_ujs
//= require components
//= require cable
//= require channels
//= require_tree .
We will be using es6, so lets replace the file app/assets/javascripts/channels/index.coffee
, with app/assets/javascripts/channels/index.es6
and add following code.
var App = {};
App.cable = Cable.createConsumer("ws://localhost:28080");
Also remove file app/assets/javascripts/channels/comments.coffee
, which is used to setup subscription. We will be doing this setup from our React Component.
Add following code to app/assets/javascripts/components/comments_list.js.jsx
.
var CommentList = React.createClass({
getInitialState() {
let message = JSON.parse(this.props.message);
return { message: message };
},
render() {
let comments = this.state.message.comments.map((comment) => {
return this.renderComment(comment);
});
return <div>{comments}</div>;
},
renderComment(comment) {
return (
<article key={comment.id}>
<h3>Comment by {comment.user.name}</h3>
<p>{comment.content}</p>
</article>
);
},
});
Here we have defined a simple component to display a list of comments associated with a message. Message and associated comments are passed as props.
Next we need to setup data needed to be passed to the component.
Add following code to app/views/messages/_message.json.jbuilder
.
json.(message, :created_at, :updated_at, :title, :content, :id)
json.comments(message.comments) do |comment|
json.extract! comment, :id, :content
json.user do
json.extract! comment.user, :id, :name
end
end
This would push JSON data to our CommentList
component.
We now need to setup our views for Message and display of Comments.
We need form to create new Comments on messages. This already exists in app/views/comments/_new.html.erb
and we will use it as is.
<%= form_for [ message, Comment.new ], remote: true do |form| %>
<%= form.text_area :content, size: '100x20' %><br>
<%= form.submit 'Post comment' %>
<% end %>
After creating comment we need to replace current form with new form, following view takes care of that.
From the file app/views/comments/create.js.erb
delete the line
containing following code. Please note that below line needs to be
deleted.
$('#comments').append('<%=j render @comment %>');
We need to display the message details and render our component to display comments.
Insert following code in app/views/messages/show.html.erb
just before <%= render 'comments/comments', message: @message %>
<%= react_component 'CommentList', message: render(partial: 'messages/message.json', locals: {message: @message}) %>
After inserting the code would look like this.
<h1><%= @message.title %></h1>
<p><%= @message.content %></p>
<%= react_component 'CommentList', message: render(partial: 'messages/message.json', locals: {message: @message}) %>
<%= render 'comments/new', message: @message %>
Notice how we are rendering CommentList, based on Message json content from jbuilder view we created.
To listen to new updates to comments, we need to setup subscription from Action Cable.
Add following code to CommentList
component.
setupSubscription(){
App.comments = App.cable.subscriptions.create("CommentsChannel", {
message_id: this.state.message.id,
connected: function () {
setTimeout(() => this.perform('follow',
{ message_id: this.message_id}), 1000 );
},
received: function (data) {
this.updateCommentList(data.comment);
},
updateCommentList: this.updateCommentList
});
}
We need to also setup related AC Channel code on Rails end.
Make following code exists in app/channels/comments_channel.rb
class CommentsChannel < ApplicationCable::Channel
def follow(data)
stop_all_streams
stream_from "messages:#{data['message_id'].to_i}:comments"
end
def unfollow
stop_all_streams
end
end
In our React Component, we use App.cable.subscriptions.create
to create a new subscription for updates, and pass the
channel we want to listen to. It accepts following methods for callback
hooks.
connected
: Subscription was connected successfully. Here we use perform
method to call related action,
and pass data to the method. perform('follow', {message_id: this.message_id}), 1000)
, calls CommentsChannel#follow(data)
.
received
: We received new data notification from Rails. Here we take action to update our Component.
We have passed updateCommentList: this.updateCommentList
, which is a Component method that is called with data received from Rails.
Here's how our complete Component looks like.
var CommentList = React.createClass({
getInitialState() {
let message = JSON.parse(this.props.message);
return { message: message };
},
render() {
let comments = this.state.message.comments.map((comment) => {
return this.renderComment(comment);
});
return <div>{comments}</div>;
},
renderComment(comment) {
return (
<article key={comment.id}>
<h3>Comment by {comment.user.name} </h3>
<p>{comment.content}</p>
</article>
);
},
componentDidMount() {
this.setupSubscription();
},
updateCommentList(comment) {
let message = JSON.parse(comment);
this.setState({ message: message });
},
setupSubscription() {
App.comments = App.cable.subscriptions.create("CommentsChannel", {
message_id: this.state.message.id,
connected: function () {
// Timeout here is needed to make sure Subscription
// is setup properly, before we do any actions.
setTimeout(
() => this.perform("follow", { message_id: this.message_id }),
1000
);
},
received: function (data) {
this.updateCommentList(data.comment);
},
updateCommentList: this.updateCommentList,
});
},
});
Our final piece is to broadcast new updates to message to the listeners, that have subscribed to the channel.
Add following code to app/jobs/message_relay_job.rb
class MessageRelayJob < ApplicationJob
def perform(message)
comment = MessagesController.render(partial: 'messages/message',
locals: {message: message})
ActionCable.server.broadcast "messages:#{message.id}:comments",
comment: comment
end
end
which is then called from Comment
model, like so-
Add this line to Comment
model file app/model/comment.rb
after_commit { MessageRelayJob.perform_later(self.message) }
We are using message relay here, and will be getting rid of existing comment relay file - app/jobs/comment_relay_job.rb
.
We will also remove reference to CommentRelayJob from Comment
model, since after_commit it now calls the MessageRelayJob
.
Hopefully we have shown that Action Cable is going to be a good friend of ReactJS in future. Only time will tell.
Complete working example for Action Cable + ReactJS can be found here.
If this blog was helpful, check out our full blog archive.