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 + React
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.
Integrating React
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.
Step 1 - Add required gems to Gemfile
1 2# react-rails isn't compatible yet with latest Sprockets. 3# https://github.com/reactjs/react-rails/pull/322 4gem 'react-rails', github: 'vipulnsward/react-rails', branch: 'sprockets-3-compat' 5 6# Add support to use es6 based on top of babel, instead of using coffeescript 7gem 'sprockets-es6'
Step 2 - Add required JavaScript files
Follow react-rails installation and run rails g react:install.
This will
- create a components.js file.
- create app/assets/javascripts/components/ directory.
Now put following lines in your application.js:
1//= require react 2//= require react_ujs 3//= require components
Make sure your app/assets/javascripts/application.js looks like this
1//= require jquery 2//= require jquery_ujs 3//= require turbolinks 4//= require react 5//= require react_ujs 6//= require components 7//= require cable 8 9//= require channels 10//= require_tree .
Step 3 - Setup Action Cable to start listening to events
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.
1var App = {}; 2App.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.
Step 4 - Create CommentList React Component
Add following code to app/assets/javascripts/components/comments_list.js.jsx.
1var CommentList = React.createClass({ 2 getInitialState() { 3 let message = JSON.parse(this.props.message); 4 return { message: message }; 5 }, 6 7 render() { 8 let comments = this.state.message.comments.map((comment) => { 9 return this.renderComment(comment); 10 }); 11 12 return <div>{comments}</div>; 13 }, 14 15 renderComment(comment) { 16 return ( 17 <article key={comment.id}> 18 <h3>Comment by {comment.user.name}</h3> 19 <p>{comment.content}</p> 20 </article> 21 ); 22 }, 23});
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.
Step 5 - Create message JSON builder
Next we need to setup data needed to be passed to the component.
Add following code to app/views/messages/_message.json.jbuilder.
1json.(message, :created_at, :updated_at, :title, :content, :id) 2json.comments(message.comments) do |comment| 3 json.extract! comment, :id, :content 4 json.user do 5 json.extract! comment.user, :id, :name 6 end 7end
This would push JSON data to our CommentList component.
Step 6 - Create Rails Views to display 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.
1<%= form_for [ message, Comment.new ], remote: true do |form| %> 2 <%= form.text_area :content, size: '100x20' %><br> 3 <%= form.submit 'Post comment' %> 4<% 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.
1$('#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 %>
1<%= react_component 'CommentList', message: render(partial: 'messages/message.json', locals: {message: @message}) %>
After inserting the code would look like this.
1<h1><%= @message.title %></h1> 2<p><%= @message.content %></p> 3<%= react_component 'CommentList', message: render(partial: 'messages/message.json', locals: {message: @message}) %> 4<%= render 'comments/new', message: @message %>
Notice how we are rendering CommentList, based on Message json content from jbuilder view we created.
Step 7 - Setup Subscription to listen to Action Cable from React Component
To listen to new updates to comments, we need to setup subscription from Action Cable.
Add following code to CommentList component.
1setupSubscription(){ 2 3 App.comments = App.cable.subscriptions.create("CommentsChannel", { 4 message_id: this.state.message.id, 5 6 connected: function () { 7 setTimeout(() => this.perform('follow', 8 { message_id: this.message_id}), 1000 ); 9 }, 10 11 received: function (data) { 12 this.updateCommentList(data.comment); 13 }, 14 15 updateCommentList: this.updateCommentList 16 17 }); 18}
We need to also setup related AC Channel code on Rails end.
Make following code exists in app/channels/comments_channel.rb
1class CommentsChannel < ApplicationCable::Channel 2 def follow(data) 3 stop_all_streams 4 stream_from "messages:#{data['message_id'].to_i}:comments" 5 end 6 7 def unfollow 8 stop_all_streams 9 end 10end
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.
Complete React Component
Here's how our complete Component looks like.
1var CommentList = React.createClass({ 2 getInitialState() { 3 let message = JSON.parse(this.props.message); 4 return { message: message }; 5 }, 6 7 render() { 8 let comments = this.state.message.comments.map((comment) => { 9 return this.renderComment(comment); 10 }); 11 12 return <div>{comments}</div>; 13 }, 14 15 renderComment(comment) { 16 return ( 17 <article key={comment.id}> 18 <h3>Comment by {comment.user.name} </h3> 19 <p>{comment.content}</p> 20 </article> 21 ); 22 }, 23 24 componentDidMount() { 25 this.setupSubscription(); 26 }, 27 28 updateCommentList(comment) { 29 let message = JSON.parse(comment); 30 this.setState({ message: message }); 31 }, 32 33 setupSubscription() { 34 App.comments = App.cable.subscriptions.create("CommentsChannel", { 35 message_id: this.state.message.id, 36 37 connected: function () { 38 // Timeout here is needed to make sure Subscription 39 // is setup properly, before we do any actions. 40 setTimeout( 41 () => this.perform("follow", { message_id: this.message_id }), 42 1000 43 ); 44 }, 45 46 received: function (data) { 47 this.updateCommentList(data.comment); 48 }, 49 50 updateCommentList: this.updateCommentList, 51 }); 52 }, 53});
Step 7 - Broadcast message when a new comment is created.
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
1class MessageRelayJob < ApplicationJob 2 def perform(message) 3 comment = MessagesController.render(partial: 'messages/message', 4 locals: {message: message}) 5 ActionCable.server.broadcast "messages:#{message.id}:comments", 6 comment: comment 7 end 8end 9
which is then called from Comment model, like so-
Add this line to Comment model file app/model/comment.rb
1after_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.
Summary
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.