Ruby on Rails - Comment real-time loading

Ruby on Rails - Comment real-time loading

The goal of this post was to create a real-time loading of comments on ruby on rails. Like in this demonstration: screen-capture.gif


Take note:

This is a continuation of my previous blog post here: Create Post and Comment in Ruby on Rails. I highly suggest that you look at it since it will be used as a base for this one. If you wanna get the base first, you could check this.

Let's start, shall we?

  1. Install react-rails gem by adding this to the Gemfile:

    gem 'react-rails'
    

    then run bundle install.

  2. After installing, run these commands to your console:

    $ bundle install
    $ rails webpacker:install         # OR (on rails version < 5.0) rake webpacker:install
    $ rails webpacker:install:react   # OR (on rails version < 5.0) rake webpacker:install:react
    $ rails generate react:install
    

    We would be using jquery for our API. Install jquery using:

    $ yarn add jquery
    

    Add this code to your environment.js.

    const webpack = require('webpack')
    environment.plugins.prepend('Provide',
     new webpack.ProvidePlugin({
       $: 'jquery/src/jquery',
       jQuery: 'jquery/src/jquery'
     })
    )
    
    module.exports = environment
    

    Edit the newly generated application.js under 'app/javascript/packs/'.

    // This file is automatically compiled by Webpack, along with any other files
    // present in this directory. You're encouraged to place your actual application logic in
    // a relevant structure within app/javascript and only use these pack files to reference
    // that code so it'll be compiled.
    
    // Support component names relative to this directory:
    var componentRequireContext = require.context("components", true);
    var ReactRailsUJS = require("react_ujs");
    ReactRailsUJS.useContext(componentRequireContext);
    
    require("@rails/ujs").start()
    require("jquery")
    

    Add the application.js to the head layout at the 'app/views/layouts/'.

    <%= javascript_pack_tag 'application' %>
    
  3. Create the React Component.

    $ rails g react:component CommentSection commentsPath:string
    

    This would generate the react component we will be using for the real-time loading of comments. The 'commentsPath:string' is the props that will pass the API URL to the component.

  4. Install the active model serializer gem after adding this to your Gemfile.

    gem 'active_model_serializers'
    

    Create the comment serializer by typing this to your console.

    $ rails g serializer Comment
    

    Then add the text field to the comment serializer.

    class CommentSerializer < ActiveModel::Serializer
     attributes :id, :text
    end
    
  5. Now we will create the controller we would be using for the API.

    Create the API folder first. Go to the controller folder in the rails app, then do this:

    $  mkdir api
    

    Then go to the newly created folder and make the controller we would be using.

    $ touch posts_controller.rb
    

    Edit posts_controller.rb with this code.

    class Api::PostsController < ApplicationController
     before_action :set_post, only: [:comments]
    
     def comments
       render json: @post.comments, each_serializer: CommentSerializer
     end
    
     private
    
     def set_post
       @post = Post.find(params[:id])
     end
    end
    

    The posts#show should return an array of comments.

  6. Add the API path to config/routes.rb.

    Rails.application.routes.draw do
     # other routes
    
     namespace :api do
       resource :posts, only: [], defaults: {format: "json"} do
         member do
           get "/:id/comments" => "posts#comments", as: "comments"
         end
       end
     end
    end
    

    Get the pathname of the newly added route by checking 'rails routes' to your console terminal. In my case, its 'comments_api_posts_path'.

  7. Add react component to post#show view. Pass the new pathname we just created in the react component.

    <!--app/views/posts/show.html.erb-->
    <p id="notice"><%= notice %></p>
    
    <%= @post.title %>
    <br>
    <%= @post.text %>
    <br>
    
    <b>Comments</b>
    <br>
    
    <%= react_component("CommentSection", { commentsPath: comments_api_posts_path(id: @post.id)}) %>
    
    <%= render "comments/form", comment: @comment, post_id: @post.id%>
    
    <%= link_to 'Edit', edit_post_path(@post) %> |
    <%= link_to 'Back', posts_path %>
    

    The commentsPath will be passed down the path as props in the react component.

  8. Update the React component CommentSection.js.

    import React from "react"
    import PropTypes from "prop-types"
    
    class CommentSection extends React.Component {
    constructor(props){
      super(props);
    
      this.state = {
        comments: []
      }
    }
    
    componentDidMount(){
      //Run fetchComments() for the first time
      this.fetchComments();
    
      //Set Interval for running fetchComments()
      this.interval = setInterval(() =>{
       this.fetchComments();
      }, 1000);
    }
    
    componentWillUnmount(){
      // Clear the interval right before component unmount
      clearInterval(this.interval);
    }
    
    // Fetches Comments
    fetchComments(){
    
      $.ajax({
        url: this.props.commentsPath,
        dataType: 'json',
        success: function (result){
          //Set state based on result
          this.setState({comments: result})
        }.bind(this)
      });
    }
    
    render () {
      return (
        <React.Fragment>
          <ul>
            {
              this.state.comments.map(function(comment, index){
    
                return <li key={index}>{comment.text}</li>
              })
            }
          </ul>
        </React.Fragment>
      );
    }
    }
    
    export default CommentSection
    

    A little bit of explanation. The fetchComments() function, fetches the comments of the post based on the value of commentsPath props(with the value of the API path of the current post). The result of the fetch will return an array of comments and that will be set as a state, which will be rendered by the component.

  9. Change the form at the 'app/views/comments/_form.html.erb' and comments_controller.rb

    <!-- app/views/comments/_form.html.erb -->
    <!--  add 'local:false' in the form_with-->
    
    <%= form_with(model: comment, local: false) do |form| %>
    
    # app/controllers/comments_controller.rb
    # edit create action
    
    def create
      @comment = Comment.new(comment_params)
    
      if @comment.save
       respond_to do |format|
        format.js{ render :js => "document.getElementById('comment_text').value = '';" }
       end
      end
    end
    

    The javascript would remove the text after you submit the comment form.

And that's the last step! Try restarting your server, and check your localhost.

If you want to check the code, go to the Github Repository.