Diatom is a custom software development company and react Services together with Ruby Services is one of the main aspects of our business.
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 Angular, 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 for the development 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.
More ReactJs app examples you may find in the following article:
Please contact Diatom Enterprises as a company – Expert in Software development!
Thank you!