codedecoder

breaking into the unknown…


2 Comments

Rails vs React : porting view in React

React is js framework to manage dynamic user interface . If you are a Rails Developer, you have implemented it numerous time with partial refresh.

Consider a simple scenario where a User comment on a picture or video or slide. You need to show the newly added comment in the comments list without reloading the page.

With rails you do it with partial refresh of the comment list.

It works well and you will be happy till you have tried React. Few of the Problem with Rails partial refresh is as below:

  1. You need to redraw the partial DOM, every time it get refreshed
  2. You need to reinitialize all the needed js on the partial Refresh

If data to render is large, User can easily see the Flicker on page while the partial is getting refreshed.

It give bad user experience. You can easily avoid it by using React. Infact you can completely replace Rails view with React view while using Rails for other great benefit it provides for web development.

Here, I will not going to explain React in detail, but just show you the changes you need to do or adopt to migrate from Rails view to React view. I will first show you the Rails implementation of dynamic listing of comments and then tell you the changes you need to do to make it work in React. If you already know about partial refresh in Rails, you can skip it and go directly to React Implementation

Rails Implementation : Partial Refresh of Dynamic component of View

View : (wordpress not supporting ruby view tag, so I have add the view code as image)
# slide detail page(app/views/slides/show.html.erb)

slide_show

This is slide show page(only relevant part is shown), rendering the partial list comments at top, followed by a input filled for writing comment and ‘Add’ button to submit the comment. At bottom we have a form with some hidden inputs.When user click on add button we populate the comment field of this form and submit it through js.

Below is the partial listing the comments
#list_comments partial(app/views/slides/_list_comments.html.erb)

list_comment_partial

A important thing in this partial is use of highlight_mentioned_user helper for displaying comment. Remember when you mention a user name in FB comment it get highlighted. Same is achieved here with the helper method, which basically highlight the name of user mentioned in the comment. Since this method can be used anywhere, I have placed it in application helper

#app/Helper/application.rb

module ApplicationHelper

 def highlight_mentioned_user(mentioner, content)
     mentioned_users = mentioner.mentionees(User)
     mentioned_users.each do |user|
     first_name = user.first_name
     linked_name = '<a href="javascript:void(0)" class="user_highlight">' + first_name + '</a>'
     content.gsub!(first_name, linked_name)
   end
   content
 end

end

Now let us see the js code which handle submit of comment to controller
#app/assets/javascript/slide.js

function add_comment() {
 if ($("#slide_comment").val() === '') {
   alert("Comment can't be blank");
   return false;
 }
 else {
   var mentioned_user = $('#slide_comment').mentionsInput('getMentions');
   var mentioned_user_ids = [];
   $.each(mentioned_user, function () {
     mentioned_user_ids.push(this.uid);
   });
   $("#slide_comment_mentioned_user").val(mentioned_user_ids.join(","));
   $("#comment").val($("#slide_comment").val());
   //mix panel event for Comment Added
   create_mix_panel_event("comment_added");

   //mix panel event for @mention Used in Comment
   if(mentioned_user_ids.length > 0) {
     create_mix_panel_event("@mention_used_in_comment");
   }
 
   $('#form_slide_add_comment').trigger('submit.rails');
   }
};

So we have add_comment() function in js, which is binded to Add button. When user click this button, it do basic validations , other needed stuff and submit the form.

On submit of form, data passed to controller action which look as below.

# app/controllers/slides_controller.rb

class SlidesController < ApplicationController
  def add_comment
   @slide = Slide.find(params[:slide_id])
   @user_who_commented = current_user
   comment = Comment.build_from(@slide, @user_who_commented.id, params[:comment])
   mentioned_user_ids = params[:slide_comment_mentioned_user].split(',').reject(&:empty?)
   if comment.save
     slide_comment_mention_notification(params[:id], @slide, comment, @user_who_commented, mentioned_user_ids)
   end
  end
end 

Now once control reach the controller action, since it is ajax call i,e request is of type XHR, Rails will render add_comment.js.erb file . In this file we will refresh the partial. It look as below

# app/views/slides/add_comment.js.erb

$('#slide_comments_swap').html('<%= escape_javascript(render partial: "slides/list_comments" ,locals: {slide: @slide} ) %>');
$('#slide_comment').val('');
$("#slide_comment").mentionsInput('clear');

Here we have refreshed the partial and cleared the input field , thus completing the process.

PORTING RAILS VIEW IN REACT :

Let start with React. Use this link to configure Reactt with your Rails App .

We are going to replace rails partial refresh with React, so we do not need the partial  _list_comments.html.erb anymore. Delete the _list_comment.html.erb partial .

The slide show page now look as below :

slide_show_react
You can see that, our view code reduced to one react component. Also it look familiar with the way we use partial in Rail , SlideComments is the name of the React Component(equivalent to _list_comments.html.erb of rails) . We have then passed the required data for the component.

One thing to note here that React accept data as Array of Hash. When you are using rails, you have a no of way to get your data in view itself, But within React component there is no rail code. For example you have passed comments collection to list_comments partial and within that you get the name of user who has commented with comment.user.full_name . But this is not possible with the data passed to React.

In Rails view we are showing each comment with the detail : commenter pic, commenter full_name, comment date and comment text. We need all these in React view also, So you can see that I have created react_slide_comments helper to which I am passing the comments collection, which then return the needed data. The Helper code look as below :

module SlidesHelper
  include ApplicationHelper
  
  # create needed detail to display in react's component
  def react_slide_comments(comments)
    react_comments = []
    comments.each do |comment|
      react_comments << react_slide_comment(comment)
    end
    react_comments
  end
  
  # get data of individual comment
  def react_slide_comment(comment)
    {
      id: comment.id,
      commenter_name: comment.user.full_name,
      commenter_img: comment.user.picture.expiring_url(60, :thumb),
      comment_date: comment.created_at.strftime('%B %d, %Y'),
      comment_text: highlight_mentioned_user(comment, comment.body)
    }
  end
  
end

So at this point, we have all the required data earlier used in the Rails view. In the slide Helper we have included the Application Helper at the top as it contain the method highlight_mentioned_user used for highlighting users mentioned in a comment .

NOTE : you need to create exactly the same DOM structure as you are using in Rails. The only difference is that you will not write as HTML but as React node in JS.

I suggest everyone to use Coffee Script to write the React code, as there is two many lines of code and become cumbersome if writing in pure js.

First we will create the component SlideComments . For this create the coffee file slide_comments.js.coffee in components folder

# app/assets/javascript/components/slide_comments.js.coffee

@SlideComments = React.createClass
  getInitialState: ->
    comments: @props.comments
    project_scoped: @props.project_scoped
    project_id: @props.project_id
    slide_id: @props.slide_id
    
  getDefaultProps: ->
    comments: []
    
  addSlideComment: (response) ->
    if response.comment_saved
      comments = React.addons.update(@state.comments, { $push: [response.comment] })
      @setState comments: comments
    
  render: ->
    React.DOM.div
      className: 'slide_comments_swap'
      React.DOM.ul
        className: 'comments'
        for comment in @state.comments
          React.createElement SlideComment, key: comment.id, comment: comment
      React.createElement SlideCommentForm, project_id: @state.project_id, slide_id: @state.slide_id,  handleNewRecord: @addSlideComment    

In render part you can see that, we are creating same DOM structure as in HTML. For each comment we have created SlideComment component displaying detail of each comment.

# app/assets/javascript/components/slide_comments.js.coffee

@SlideComment = React.createClass
  rawMarkup: ->
    { __html: @props.comment.comment_text, sanitize: true }
    
  render: ->
    React.DOM.li
      className: 'slide_comment'
      React.DOM.img
        className: 'img-circle avatar pull-left'
        width: '30'
        src: @props.comment.commenter_img
      React.DOM.div
        className: 'comment-info'
        React.DOM.h3
          className: 'commenter_name'
          @props.comment.commenter_name
        React.DOM.em
          className: 'comment_date'
          @props.comment.comment_date
        React.DOM.p
          className: 'comment_text'
          dangerouslySetInnerHTML: @rawMarkup()

If you see closely, we have created the same HTML structure as the deleted _slide_comments.html.erb partial.An important thing to note here that we have used dangerouslySetInnerHTML of React in place of raw of rails. Both basically render raw html.

We also have a input box with Add button to add comment in the original Rails view. It get handled in SlideCommentForm component.

# app/assets/javascript/components/slide_comment_form.js.coffee

@SlideCommentForm = React.createClass
  getInitialState: ->
    comment_body: ''
    
  handleKeyDown: (e)->
    if e.key == 'Enter'
      @handleCommentAdd()
      e.preventDefault()
  
  handleCommentAdd: ->
    if $('#slide_comment').val() == ''
      alert 'Comment can\'t be blank'
    else
      mentioned_user = $('#slide_comment').mentionsInput('getMentions')
      mentioned_user_ids = []
      $.each mentioned_user, ->
        mentioned_user_ids.push @uid
      $.ajax
        method: 'post'
        url: "/projects/#{@props.project_id}/slides/#{@props.slide_id}/add_comment"
        dataType: 'JSON'
        data:
          comment: $('#slide_comment').val()
          slide_comment_mentioned_user: mentioned_user_ids.join(',')
        success: (response) =>
          #trigger event for comments count update
          postal.publish
            channel: 'slide'
            topic: 'comment.add'
            data:
              comments_count: response.comments_count
          #mix panel event for Comment Added
          create_mix_panel_event 'comment_added'
          #mix panel event for @mention Used in Comment
          if mentioned_user_ids.length > 0
            create_mix_panel_event '@mention_used_in_comment'
          @props.handleNewRecord response
        complete: ->
          $('#slide_comment').val('');
          $("#slide_comment").mentionsInput('clear');
     
  render: ->
    React.DOM.div
      className: 'input-group'
      React.DOM.input
        id: 'slide_comment'
        className: 'form-control'
        type: 'text'
        placeholder: 'Add a Comment'
        name: 'comment_body'
        onKeyPress: @handleKeyDown
      React.DOM.div
        className: 'input-group-btn'
        React.DOM.button
          id: 'add_slide_comment'
          className: 'btn btn-info'
          onClick: @handleCommentAdd
          'Add'

Here you can see that we have bind onClick event to handleCommentAdd method, and place all the js logic in it like giving alert if no comment is typed and finally making the ajax call to submit the comment to server.

Delete the js code in slide.js as it is ported to React .

When user add a comment it will go to controller through ajax call . The controller code now look as below, we need to add a single line to existing controller code

  def add_comment
    @slide = Slide.find(params[:slide_id])
    @user_who_commented = current_user
    comment = Comment.build_from(@slide, @user_who_commented.id, params[:comment])
    mentioned_user_ids = params[:slide_comment_mentioned_user].split(',').reject(&:empty?)
    if comment.save
      slide_comment_mention_notification(params[:id], @slide, comment, @user_who_commented, mentioned_user_ids)
      comment.create_activity key: 'project.slide_commented', owner: current_user, recipient: comment.commentable, project_id: params[:id]
    end
    render json: {comment_saved: comment.persisted?, comment: react_slide_comment(comment)}
  end

The bolded line is the only line we need to change in controller, as it now need to return the newly added comment which get appended to DOM by React.

Earlier when we are doing it Rails way, we are handling the DOM refresh in add_comment.js.erb file. It is not needed anymore. Delete the add_comment.js.erb file

You are done with porting your dynamic view from Rails to React.You can summarize the steps in below line

  1. Replace Rails partial with React Component
  2. Pass needed data collection as Array of Hash
  3. Move the js code(like handling click of add button ) within React
  4. Delete add_comment.js.erb file handling partial refresh in Rails

That’s all … all your css, js, model, controller etc code will not need any change.

My aim here is to just explain the amount of work needed to move from Rails to React. I will soon explain each line of React code in detail in some other Blog.

Reference:

https://facebook.github.io/react/

https://facebook.github.io/react/docs/component-specs.html

https://facebook.github.io/react/docs/tags-and-attributes.html


Leave a comment

Configure Rails with React

React is js Library created by Facebook to support Dynamic UI. It glue easily with Rails and can easily substitute Rails view.

I have explained porting Rails view to React view in this post .

React gem is available which make its integration with Rails a breeze. lets setup React to work with Rails .

CONFIGURING REACT WITH RAILS :

Add below to your Gemfile

gem 'react-rails'

Run the bundler command from terminal on your project root :

$ bundle install

Initialize react with below command

Below command will provide default setup for React in your rails App

$ rails g react:install
Running via Spring preloader in process 19546
create app/assets/javascripts/components
create app/assets/javascripts/components/.gitkeep
insert app/assets/javascripts/application.js
insert app/assets/javascripts/application.js
insert app/assets/javascripts/application.js
create app/assets/javascripts/components.js

So below three line will be added to the application.js

//= require react
//= require react_ujs
//= require components

These line will be added to the bottom of your application.js file. I suggest to move the first two line toward top just below the jquery files and keep the last line i,e components towards end of the file. The reason is that, you want react js to be called before you do any DOM manipulation. Keeping components js at bottom ensure that you can call a function defined earlier by you in React component.

For Example : my app/assets/javascript/application.js look as below

//= require jquery
//= require jquery_ujs
//= require jquery-ui
//= require react
//= require react_ujs

... other js file Iam using ...

// keep react component js at bootom as
// you may need other defined function in it
//= require components
// put other js file above react components

Now configure React Add-ons in your application. The Add-ons have many helper which help you in writing React code.

If you look at component.js file it has below content :

#app/assets/javascript/component.js

//= require_tree ./components

So it basically call require_tree on components folder, thus any js file you create within this folder automatically get loaded.

Add below line to application.rb file

# config/application.rb

  config.react.addons = true

 

This enable Reacts addons which provide a number of useful helper method listed here .

With this you are ready to start with react. While working with React you need to write a lot of JS code, So I suggest to start using Coffee Script.

Even you can try it online here and use this editor to convert your existing js code to Coffee script .