The goal of this post was to create a real-time loading of comments on ruby on rails. Like in this demonstration:
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?
Install react-rails gem by adding this to the Gemfile:
gem 'react-rails'
then run bundle install.
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' %>
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.
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
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.
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'.
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.
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.
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.