Sử dụng ReactJS với Action cable Rails

Action Cable tích hợp với websocket dựa trên giao tiếp thời gian thực trong ứng dụng Ruby on Rails. Nó cho phép chúng ta xây dựng các dựng dụng thời gian thật như Chat, Cập nhật Status, ...

Action Cable + React

Action Cable cung cấp ứng dụng thời gian thật. ReactJS là một công cụ tốt để quản lý view phức tạp phía client. Bằng cách sử dụng chúng, chúng ta có thể dễ dang xây dựng ứng dụng web một cách linh hoạt mà yêu cầu quản lí phía client không làm việc quá nhiều.

Dữ liệu sẽ được cập nhật mới ngay lập tức bất cứ khi nào thay đổi thông qua Action Cable và hiển thị dữ liệu trên view mà người dùng không cần làm bất cứ điều gì trên ứng dụng bằng ReactJS.

Tích hợp React

Ví dụ đơn giản sử dụng Action Cable là ứng dụng chat. Chúng ta sẽ xây dựng một ứng dụng tương tự bằng ReactJS.

Đầu tiên chúng ta sẽ làm theo demo để lấy về ứng dụng chat bằng cách sử dụng Action Cable.

Bây giờ, ứng dụng chat đang làm việc kèm theo bổ sung thêm ReactJS vào ứng dụng.

Các bạn có thể xem thêm các videos về học ReactJS tại đây.

Bước 1 - khai báo gem React trong Gemfile

gem "react-rails"

# Bổ sung thêm sử dụng es6 thay cho coffeescript
gem "sprockets-es6"

Bước 2 - Bổ sung thêm file Javascript Làm theo react-rails installation và chạy lệnh

$ rails g react:install

Hệ thống sẽ tự tạo các file:

  • components.js
  • thư mục app/assets/javascripts/components

Sau đó bổ sung thêm các file sau vào file applcation.js

# app/assets/javascripts/application.js

[...]
//= require react
//= require react_ujs
//= require components
[...]

Đảm bảo rằng file application.js sẽ như sau:

# app/assets/javascripts/application.js

//= require jquery
//= require jquery_ujs
//= require turbolinks
//= require react
//= require react_ujs
//= require components
//= require cable

//= require channels
//= require_tree .

Bước 3 - Cài đặt Action Cable để lắng nghe các events

Chúng ta sẽ sử dụng es6 nên chúng ta sẽ đổi file app/assets/javascripts/channels/index.coffee thành file app/assets/javascripts/channels/index.es6 và thêm đoạn code sau:

var App = {};
App.cable = Cable.createConsumer('ws://localhost:28080');

Đồng thời cũng xóa đi file app/assets/javascripts/channels/comments.coffee, được sử dụng để cài đặt subscription. Chúng ta sẽ cài đặt từ React component.

Bước 4 - Tạo React component CommentList

Thêm đoạn code sau vào file app/assets/javascripts/components/comments_list.js.jsx

# app/assets/javascripts/components/comments_list.js.jsx

var CommentList = React.createClass({
  getInitialState(){
    let message = JSON.parse(this.props.message);
    return {message: message};
  },

  render() {
    let comments = this.state.message.comments.map((comment) => {
      return this.renderComment(comment);
    });

    return (
      <div>{comments}</div>
    );
  },

  renderComment(comment) {
    return (
      <article key={comment.id}>
        <h3>Comment by { comment.user.name }</h3>
        <p>{ comment.content }</p>
      </article>
    );
  }
});

Tới đây chúng ta đã khởi tạo xong một component đơn giản để hiện thị danh sách comments liên quan tới message. Message và các comment liên quan được thông qua như một props.

Bước 5 - Tạo message JSON builder

Tiếp theo chúng ta cần khởi tạo dữ liệu để thông qua component.

Thêm đoạn code dưới đây vào file app/views/messages/_message.json.jbuilder.

# app/views/messages/_message.json.jbuilder

json.(message, :created_at, :updated_at, :title, :content, :id)
json.comments(message.comments) do |comment|
  json.extract! comment, :id, :content
  json.user do
    json.extract! comment.user, :id, :name
  end
end

Đoạn code trên sẽ đẩy dữ liệu JSON vào CommentList component.

Bước 6 - Tạo view Rails để hiển thị component

Chúng ta cần phải cài đặt views cho Message và hiển thị Comments.

Cần phải khởi tạo form để tạo Comment trong messages. Nó đã tồn tại trong app/views/comments/_new.html.erb và chúng ta sẽ sử dụng nó:

<%= form_for [message, Comment.new], remote: true do |form| %>
  <%= form.text_area :content, size: "100x20" %><br>
  <%= form.submit "Post comment" %>
<% end %>

Sau khi tạo xong chúng ta cần phải thay thế form hiện tại bằng form mới.

Chúng ta cần phải xóa dòng sau đây khỏi file app/views/comments/create.js.erb

$('#comments').append('<%=j render @comment %>');

Chúng ta cần phải hiển thị chi tiết message và render component để hiển thị các comments. Bổ sung thêm đoạn code sau vào file app/views/messages/show.html.erb trước <%= render 'comments/comments', message: @message %>

<%= react_component 'CommentList', message: render(partial: 'messages/message.json', locals: {message: @message}) %>

Sau khi thêm đoạn code vào sẽ như sau:

# app/views/messages/show.html.erb

<h1><%= @message.title %></h1>
<p><%= @message.content %></p>
<%= react_component 'CommentList', message: render(partial: 'messages/message.json', locals: {message: @message}) %>
<%= render 'comments/new', message: @message %>

Lưu ý rằng, cách chúng ta render CommentList sẽ dựa vào nội dùng trả về từ json builder mà chúng ta đã tạo.

Bước 7 - Cài đặt Subscription để lắng nghe Action Cable từ React component

Để lắng nghe thay đổi từ comments, chúng ta cần cài đặt subscription từ Action Cable.

Thêm đoạn code sau vòa CommentList component.

setupSubscription(){

  App.comments = App.cable.subscriptions.create("CommentsChannel", {
    message_id: this.state.message.id,

    connected: function () {
      setTimeout(() => this.perform('follow',
                                    { message_id: this.message_id}), 1000 );
    },

    received: function (data) {
      this.updateCommentList(data.comment);
    },

    updateCommentList: this.updateCommentList

    });
}

Chúng ta cũng cần khai báo AC Channel liên quan trong Rails.

Khai báo đoạn code sau:

# app/channels/comments_channel.rb

class CommentsChannel < ApplicationCable::Channel
  def follow(data)
    stop_all_streams
    stream_from "messages:#{data['message_id'].to_i}:comments"
  end

  def unfollow
    stop_all_streams
  end
end

Trong React component của chúng ta, sẽ sử dụng App.cable.subscriptions.create để tạo một subscription mới cho việc update, và thông qua channel chúng ta muốn lắng nghe. Nó sẽ chấp thuận các method sau để callback hooks.

  • connected Subscription đã được kết nối thành công. Chúng ta sử dụng method perform để gọi tới hành động liên quan, và gửi dữ liệu tới method. perform('follow', {message_id: this.message_id}), 1000), gọi CommentsChannel#follow(data).
  • received Chúng ta sẽ nhận thông báo dữ liệu mới từ Rails. Tới đây sẽ sử dụng action để cập nhật Component. Cần phải pass qua updateCommentList: this.updateCommentList, là một method của Component được gọi tới với dữ liệu gửi từ Rails

Hoàn thiện React Component

Component của chúng ta sẽ như sau:

var CommentList = React.createClass({
  getInitialState(){
    let message = JSON.parse(this.props.message);
    return {message: message};
  },

  render() {
    let comments = this.state.message.comments.map((comment) => {
      return this.renderComment(comment);
    });

    return (
        <div>{comments}</div>
    );
  },

  renderComment(comment) {
    return (
        <article key={comment.id}>
          <h3>Comment by { comment.user.name } </h3>
          <p>{ comment.content }</p>
        </article>
    );
  },

  componentDidMount() {
    this.setupSubscription();
  },

  updateCommentList(comment) {
    let message = JSON.parse(comment);
    this.setState({message: message});
  },

  setupSubscription() {

    App.comments = App.cable.subscriptions.create("CommentsChannel", {
      message_id: this.state.message.id,

      connected: function () {
        // Timeout here is needed to make sure Subscription
        // is setup properly, before we do any actions.
        setTimeout(() => this.perform('follow',
                                      {message_id: this.message_id}),
                                      1000);
      },

      received: function(data) {
        this.updateCommentList(data.comment);
      },

      updateCommentList: this.updateCommentList
    });
  }
});

Bước 7 - Broadcast message khi comment mới được tạo

Phần cuối cùng là cập nhật mới broadcast tới mesage tới listeners, và sẽ phải subcribed tới channel

Thêm doạn code sau vào file app/jobs/message_relay_job.rb

class MessageRelayJob < ApplicationJob
  def perform(message)
    comment =  MessagesController.render(partial: 'messages/message',
                                         locals: {message: message})
    ActionCable.server.broadcast "messages:#{message.id}:comments",
                                 comment: comment
  end
end

sau đó sẽ gọi từ model Comment như sau:

# app/model/comment.rb

[...]
after_commit {MessageRelayJob.perform_later(self.message)}
[...]

Chúng ta cũng cần phải xóa refernece từ CommentRelayJob từ model Comment, từ after_commit sẽ gọi tới MessageRelayJob

Tổng kết

Hi vọng rằng những gì tôi viết ra đây về Action cable sẽ giúp các bạn làm việc tốt với ReactJS. Các bạn có thể tham khảo ví dụ về Action Cable + ReactJS tại đây

Bài viết được dịch từ bài viết Using ReactJS with Rails Action Cable