Chat Application with Rails 5 and React

One of the recent projects I worked on was called Cliizii, which is an online marketing platform. This was a big project that contained several parts, one of which was a chat platform. Even though the dashboard platform was written using AngularJS, we decided that, for the chat part, ReactJS would be a better fit.

What is React?

First of all, React is not another MVC framework, or any other kind of framework for that matter. React is only a view layer. React is a template language that uses an HTML and Javascript bundle called a ‘component.’

Often, I have seen that people tend to compare React with other frameworks like Angular or Ember, but as I mentioned React is not a framework, making it hard to compare with Angular.

Let’s start building the app.

The plan is to create an application that allows the user to create several separate chat rooms where users can chat with each other.

User functionality

When we have created a new Rails application, we need to add the gem devise to install it and do everything else required to make it work. When that is done, we need to generate a ‘User’ model, which we will do with devise.

rails generate devise User

This will generate a new migration, and in that migration, we need to add ‘first_name’ and ‘last_name.’

t.string :first_name, null: false, default: ""
t.string :last_name, null: false, default: ""

Run
rake db:migrate
Run
rake db:migrate

Also, we need to add validation for these columns in the model.

validates :first_name, :last_name, presence: true

As you know, starting with Rails 4 there have been strong params, so we need to add ‘first_name’ and ‘last_name’ to devise strong params in the application controller. Your application controller, in the end, should look like this:

class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
before_action :authenticate_user!
private
     def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [:first_name, :last_name])
end
end

The last thing that we need to do here is to generate devise views,

rails generate devise:views

and add the necessary fields to the registration form.

<div class="field">
<%= f.label :first_name %><br />
<%= f.text_field :first_name, autofocus: true %>
</div>
<div class="field">
<%= f.label :last_name %><br />
<%= f.text_field :last_name, autofocus: true %>
</div>

This will allow users to register and log in to the app.

Chat rooms and messages

Next, we need to create those chat rooms and messages. Generate two models – ‘message’ and ‘chat_room.’

rails g model chat_room  rails g model message

For the ‘chat_room’ migration, we need to add ‘title’ and ‘user’ columns, so we know who created the chat room. We also need to name it.

class CreateChatRooms < ActiveRecord::Migration[5.0]
def change
create_table :chat_rooms do |t|
t.string :title
t.references: user, foreign_key: true
       t.timestamps
end
   end
end

For the ‘messages’ migration, we need to add ‘body,’ ‘user’ and ‘chat_room_id’ columns.

class CreateMessages < ActiveRecord::Migration[5.0]
def change
create_table :messages do |t|
t.text :body
t.references :user, foreign_key: true
       t.references :chat_room, foreign_key: true
       t.timestamps
end
   end
end

Now we can run ‘rake db:migrate.’ Another thing we need to do is add relations in the models between ‘chat_room,’ ‘message’ and ‘user.’

The chat room model will belong to ‘User.’ As I wrote above, we might need to know who created this room in the future. We could add some other functionality, like the ability to ‘ban’ a user, that could be only done by the room owner. Of course, a ‘Room’ has many messages and a validation on a title. In the end, our model for ‘Chat Room’ should look something like this:

class ChatRoom < ApplicationRecord
belongs_to :user
has_many :messages, dependent: :destroy
validates :title, presence: true
end

For the ‘message’ model, everything is pretty straightforward. It belongs to ‘user’ and ‘chat_room’ and of course a validation on the ‘body’ field:

class Message < ApplicationRecord
belongs_to :user
belongs_to :chat_room
validates :body, presence: true, length: {minimum: 2, maximum: 1000}
end

The last one is the ‘user’ model and we only need to add:

has_many :chat_rooms, dependent: :destroy
has_many :messages, dependent: :destroy

When this is finished, we are ready to work on ‘controllers,’ ‘presenter,’ ‘decorator,’ and ‘serializer.’

Create Rooms and Messages

The first thing that we need to do is create a controller for ‘chat_rooms.’

rails g controller chat_rooms

For the index action, we will generate a ‘Decorator’ using the gem draper. You might wonder why you should use a decorator. Decorators allow us to add behavior to objects without affecting other objects of the same class, and I like to write my code clean and structured, so that everything has its place. The decorator pattern is a useful alternative to creating sub-classes.

Our index action should look like this:

def index
@chat_rooms = ChatRoomDecorator.decorate_collection(ChatRoom.all)
end

We are calling our decorator and passing it in all chat rooms.

Decorator:

class ChatRoomDecorator < Draper::Decorator
delegate_all
def owner
object.user.first_name + " " + object.user.last_name
end
   def created_at
object.created_at.strftime("%d/%m/%Y")
end
end

For the decorator, right now we need two methods. The ‘owner’ is just the ‘first_name’ plus ‘last_name’ of the user who created this room.

The second method is ‘created_at,’ to make the date when the chat room was created more user-friendly. We will use both of these methods in our chat room index layout. I will not get into layouts and functionality, which allow users to create new chat rooms; it would be too boring. You can build layouts as you like. At the bottom of this post, there will be a link to this code.

Now we can get to the chat functionality.

The show layout in our ‘chat_room’ will be the place where we add our React component. Before we start on React, we need to add the gem active_model_serializer.

Show action:

def show
@chat_room = ChatRoom.find(params[:id])
@json_object = ChatRoomSerializer.new(@chat_room).as_json
end

Previously, we created three models: ‘chat_room,’ ‘message’ and ‘user.’ With the active model serializer, we will generate a serializer for each model.

rails g serializer chat_rooms
rails g serializer messages
rails g serializer users

A nice thing about serializers is that we can write which keys we want to include in our JSON; we can also write custom methods and relations in them.

Chat room serializer:

class ChatRoomsSerializer < ActiveModel::Serializer
attributes :id, :title
has_many :messages, serializer: MessagesSerializer
end

Messages serializer:

class MessagesSerializer < ActiveModel::Serializer
attributes :id, :body, :written_at
belongs_to :user, serializer: UsersSerializer
def written_at
object.created_at.strftime('%H:%M:%S %d %B %Y')
end
end

Users serializer:

class UsersSerializer < ActiveModel::Serializer
attributes :id, :full_name
def full_name
object.first_name + " " + object.last_name
end
end

In these serializers, we can see that there are relations and custom methods like ‘written_at.’

Now in our presenter, when we call method ‘json_object,’ it will initialize ‘ChatRoomSerializer,’ which will return a nice JSON object.

We will pass this JSON object to our React component in the show view.

<h2 class="text-center">
<%= chat_room.title %>
<br/>
<small>
<%= link_to 'Back', chat_rooms_path, class: 'btn btn-primary btn-sx' %>
</small>
</h2>
<%= react_component('ChatRoom', { chat_room: @json_object }) %>

In this HTML code, you can see that we created with serializers the ‘react_component’ for the ‘ChatRoom’ where we are passing the JSON.

The ‘react_component’ method is provided by gem react-rails.

Before getting to the React component, we will need to generate ‘messages_controller’ and create a channel for chat rooms and a ‘Job’ for message broadcasting.

rails g controller message

We can leave this blank, but we will need it for our ‘Job,’ which will send new messages.

class MessageBroadcastJob < ApplicationJob
queue_as :messages
def perform(message_id)
message = Message.find_by(id: message_id)
if message
serialized_message = MessagesSerializer.new(message).as_json
ActionCable.server.broadcast("chat_rooms_#{message.chat_room.id}_channel", message: serialized_message)
else
puts("message not found with id: #{message_id}")
end
   end
end

For this ‘Job,’ we are passing the message ID and serializing it, so we can work with a nice JSON object in our React component and then pass it to the action cable.

The last thing we need to create before getting to our React component is the ‘chat_room_channel.’

class ChatRoomsChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_rooms_#{params['chat_room_id']}_channel"
end
   def unsubscribed
Stop_all_streams
end
   def send_message(data)
message = current_user.messages.create(body: data['body'], chat_room_id: data['chat_room_id'])
if message.errors.present?
transmit({type: "chat_rooms", data: message.error.full_messages})
else
MessageBroadcastJob.perform_later(message.id)
end
   end
end

Method ‘subscribed’ works so that, when the users enter a ‘chat_room,’ they will be subscribed to a channel. Streams allow channels to route broadcastings to the subscriber.

Method ‘unsubscribed’ will be called when the user leaves a ‘chat_room.’ This method unsubscribes all streams associated with the channel from the pubsub queue.

The last one , ‘send_message(data),’ is responsible for creating new messages. Afterwards, the message(model) using broadcast ‘Job’ will send this message to other users.

The next step will be our React code, but before we get there, let’s do a short summary. We have our models and controllers that allow us to register, create new chat rooms and enter these chat rooms. Then there are ‘Jobs’ and ‘channels,’ which are responsible for creating new messages and sending them to other users.

React components

When we add and install the ‘react-rails’ gem, a new folder is created called ‘components’ under ‘assets/javascript/,’ and that is where we need to create our components.

Chat room component:

class ChatRoom extends React.Component {
constructor(props) {
super(props);
this.state = {
messages: props.chat_room.message,
errors: []
};
}
}

Previously, when we added the ‘react_component’ to our show view, we passed a JSON to it. Here in the ‘constructor,’ we will set the state for ‘messages’ because they will be changed later.

componentDidMount(){
App.chatChannel = App.cable.subscriptions.create({
channel: "ChatRoomsChannel",
chat_room_id: this.props.chat_room.id,
}, {
received: ({type, data}) => {
switch (type) {
case "new_message":
this.newMessage(data);
break;
case "errors":
this.addErrors(data);
break;
}
}
});
}

The ‘componentDidMount’ is invoked immediately after a component is mounted. In our case, this is right when we enter the chat room.

‘App.chatChannel…’ is code to make a subscription to a channel. When we are ‘creating’ that connection, we need to pass two arguments: the channel we want to subscribe, in this case ‘ChatRoomChannel,’ and a channel ID, ‘this.props.chat_room.id.’ When that’s done, you can see there are three functions: ‘subscribed,’ ‘disconnected’ and ‘received.’ We only care about the last one, ‘received,’ because it will be where we receive new messages sent by the ‘Job’ we created earlier.

When receiving a new message, we are calling the ‘newMessage()’ function and passing in the data that we just received a message.

New message function:

newMessage(message){
const { messages } = this.state;
let msgs = [...messages];
msgs.push(message);
this.setState({messages: msgs});
}

Here, we are only pushing the new ‘message’ to ‘messages’ and updating the state.

In this component, there are a few more things we need to add, including ‘form’ and some code that will send users written messages. When that is done, we will create another component for outputting all messages.

Form:

form(){
return (
<div className="col-sm-12">
<form className="form-inline" onSubmit={ this.postMessage.bind(this) }>
<div className="form-group col-sm-11">
<input style={{width: "100%"}} ref="body" type="text" className="form-control" placeholder="Text..." />
</div>
<div className="form-group col-sm-1">
<button type="submit" className="btn btn-primary">send</button>
</div>
</form>
</div>
)
}

The HTML form has one input field and submit button, for writing those new messages. Usually, I would create a new component for this form and work with that component’s state, but this form is really small, so I would not move it out from this component. Instead of adding a new key in the constructor to hold that message, let’s use refs.

postMessage(event){
event.preventDefault();
App.chatChannel.perform("send_message", { chat_room_id: this.props.chat_room.id, body: this.refs.body.value });    this.refs.body.value = "";
}

This function is responsible for sending user messages to the back-end. The first line, ‘event.preventDefault();’ will ensure that, after submitting our form, the page will not get reloaded. On the second line in this function, we are calling the ‘perform’ function on the app channel, where we need to pass in the method we want to call in back-end ‘send_message’ and params with ‘chat_room_id,’ so we would know for which room we need to save this message, and the last param, the message.  The last line will clear refs.

We are in the final straight; we just need to output the messages and then we’re done.

render() {
const { messages } = this.state;
return (
<div className="row">
<div className="col-sm-12">
<MessageList messages={ messages } />
</div>
{ this.form() }
</div>
)
}

‘Render()’ is the function that will output the HTML in your browser. As I wrote before, we will create a separate component for outputting all messages.

class MessageList extends React.Component {
class MessageList extends React.Component {
render(){
return (
{ this.messagesList() }
)
}
messagesList(){
const { messages } = this.props
return messages.map((message, index) =>

{ message.user.full_name } at { message.written_at } says
{ message.body }
);
}
}

So why did we choose React?

The main reason we chose React is that, when you open a React component, it’s immediately clear what it will do. There isn’t anything automagical. For example, in Angular, there is two-way binding. If you are not familiar with Angular, you might update something you didn’t want to. This is less likely to happen in a React component.

Also, for this app, Angular would be overkill. It’s a full framework, and as you might notice in our layouts, we wrote only one really small component.

Thank you for taking the time to read this post. You can find all the relevant code on GitHub.