Tạo ứng dụng message box app sử dụng React và Rails

JavaScript là một ngôn ngữ lập trình được sử dụng rất phổ biến. Dạo này một số framework mới đang nổi lên hàng ngày làm cho các developer nghi vấn rằng nên dùng tool nào và xây dựng user interface đang có thay đổi rất lớn. Đối với Rails developer biết rằng với view .erb không phải là phổ biến nữa và sẽ được thay thế bằng framework khác dựa vào framework của Javascript-based. Trong đám đó, React của Facebook cùng cập hàng loạt phương pháp để xây dựng user interface. Thậm chí Rails có thể tiếp tục tích hợp với nó và rất dễ dàng để cài đặt một Rails API application và tạo lớp view với React. Bây giờ hãy xem app demo sau thực hiện chức năng đọc và tạo cho một model với React.

React.js là gì?

React.js là MVC framework chỉ sử dụng riêng cho view. Tương đương đó cũng có Backbone.js, AngularJS,.....

  • React.js quản lý một chỗ route state nếu state thay đổi sẽ tự động render lại toàn bộ các component.
  • Do state chỉ quan lý một chỗ thì không cần phải control kỹ về render do có tính dễ đọc và tính bảo trì cao. Ngoài ra React.js chỉ render phần thay đổi nên performance cũng không ảnh hưởng lắm.
  • Cơ bản chỉ cần quản lý một chỗ ở route state là tốt rồi nhưng component của route sẽ trở thành phức tạm nên khó để làm thành quy mô lớn. Có thể giải quyết bằng Flux.

1. Cài đặt react-rails

Thông số mô trường

  • Ruby 2.3.1p112
  • Rails 5.0.0.1
  • React-Rails 1.4.2
  • React 15.4.1

Tạo một project mới

rails new react_test
cd ./react_test

Thêm react-rails vào Gemfile

gem "react-rails"

Chạy bundle install

Tiếp theo chạy script install rails g react:install với script này sẽ tạo một manifest component.js và một thư mục app/assets/javascripts/components/ nơi tạo các components.

vào application.js để tích hợp React.js vào Rails

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

Thêm environment value của React.js

# config/environments/development.rb
Rails.application.configure do
...
+ # React development environment value
+ config.react.variant = :development
end

# config/environments/production.rb
Rails.application.configure do
...
  # React production environment value
  config.react.variant = :production
end

Trong trường hợp sử dụng addon thì phải thêm như sau

# config/environments/(development|test|production).rb
Rails.application.configure do
...
  # React.js Addon (default: false)
  config.react.addons = true
end

Tạo TopController#index

rails g controller top index

thêm route index cho controller Top

# config/routes.rb
  root 'top#index'

Chạy http://localhost:3000 rồi xem trong console có xảy ra lỗi Javascript hay không.

2. Các thành phần view của react

Hiển thị text Welcome react with rails

// app/assets/javascripts/main.js.jsx
$(function() {
    ReactDOM.render(
      <h1>Welcome react with rails</h1>,
      document.getElementById('content')
    );
 });

ReactDOM.render(,) để hiển thị Welcome react with rails trên trang web. React component được hiển thị và nhận biết như basic html tag div, span,...

<!-- app/views/top/index.html.erb -->
  <h1>Message Box</h1>
  <div id="content"></div>

Welcom react with rails

3. Tổng quan về Message box

React sử dụng Virtual DOM Tree, chỉ làm thay đổi HTML của phần thay đổi. Root node có state (có thể đổi), chuyển đến state (không thể đổi) tới các node con cần thiết. Node con nhận giá trị từ node cha, tiếp nhận props (ko thể đổi), dùng cái đó để render HTML. Mỗi node con nếu phát sinh event thì sẽ truyền đến node cha, đổi mới state của node cha bằng setState method. Như thế, từ node cha sẽ tái đổi mới toàn bộ node. Khi đó, vì có Virtual DOM tree nên chỉ đổi mới HTML phần thay đổi.

Message box sẽ có cấu trúc như sau

MessageBox
L MessageList
|  L MessageItem
L MessageForm

4. Tạo một danh sach message box

Đầu tiên sẽ tạo message box từ đây bằng tạo thư mục để chứa các component của React

mkdir app/assets/javascripts/components
  • Tạo component MessageBox
// app/assets/javascripts/components/message_box.js.jsx
  var MessageBox = React.createClass({
    render: function() {
      return (
        <div className="messageBox">
          This is message box.
        </div>
      );
    }
  });

React.createClass() để tạo component và trả cho phần view với return render của chức năng. Do class dùng riêng cho nên thuộc thính class HTML sẽ sử dụng className. Từ main.js.jsx các thành phần message box được gọi để sửa đổi

// app/assets/javascripts/main.js.jsx
 $(function() {
   ReactDOM.render(
     <MessageBox />,
     document.getElementById('content')
   );
 });

Sau đó bạn sẽ thấy trên màn hình như sau messagebox

ReactDOM.render sẽ render HTML component MessageBox như sau

<div id="content">
  <div class="messageBox" data-reactid=".0">
    This is message box.
  </div>
</div>
  • data-reactid được cập tự động mục đích để React quản lý mỗi DOM.

render của React dễ dàng chỉ phù hợp tại thời điểm thực hiện

render sẽ return HTML element trên cùng duy nhất. Nếu muốn return 2 div element sẽ gấy ra lỗi.

var MessageBox = React.createClass({
  render: function() {
    // Do return 2 div sẽ gấy lỗi
    return (
      <div className="messageBox">
        This is message box.
      </div>
      <div className="messageBox">
        This is message box.
      </div>
    );
  }
});
  • Các tag input/image phải có tag đóng.
var MessageBox = React.createClass({
  render: function() {
    return (
      <div className="messageBox">
        <image src="/path/to/file"/> {/* đúng */}
        <image src="/path/to/file">  {/* lỗi */}
      </div>
    );
  }
});
  • Tạo component MessageItem

MessageItem là component để hiển thị các message.

// app/assets/javascripts/components/message_item.js.jsx
var MessageItem = React.createClass({
  render: function() {
    return (
      <div className="message">
        <h2 className="messageUser">{this.props.message.user}</h2>
        <span>{this.props.message.text}</span>
      </div>
    );
  }
});

this.props là giá trị được gán từ component cha. Ngoài ra với cú pháp này {variable_name} để biết giá trị của biết. Ở đây message = { user: 'username', text: 'text' } được gán từ component cha sẽ được hiển thị nội dung từng mỗi username và message.

Quay lại MessageBox để thêm các MessageItem vào.

// message_box.js.jsx
 var MessageBox = React.createClass({
   render: function() {
   var messageItems = this.state.messages.map(function(message) {
      return (
        <MessageItem key={message.id} message={message}/>
      );
    });

     return (
       <div className="messageBox">
         {messageItems}
       </div>
     );
   }
 });

this.state giữ trạng thái của ứng dụng và giá trị có thể theo tên. Cơ bản nếu muốn quản lý state chỉ với root node(MessageBox) thì dễ để bảo trì JS code dễ hiểu vì state không trỏ đến các component ngẫu nhiên. Chúng ta dúng method this.setState để cập nhật các componenet của state và tất cả component sẽ được cập nhật bằng gọi render. Đối với React chỉ cập nhật khi có sự thay đổi nên render có thể thực hiện mà không cần quan tâm đến performance.

Cuối cùng chúng ta sẽ nhận data từ server, nhưng ở đấy tạm thời thì sẽ dùng tạm data chuẩn bị sẵn trước. getInitialState tất cả các component được gọi sau khi state được tạo và thực hiện gán giá trị ban đầu.

// message_box.js.jsx
 var MessageBox = React.createClass({
   getInitialState: function() {
     return {
       messages: [
         { id: 1, user: 'Tom',   text: 'Good morning' },
         { id: 2, user: 'John',  text: 'Good afternoon' },
         { id: 3, user: 'Emily', text: 'Good evening' }
       ]
    };
 },

   render: function() {
     ...

5. Tạo message form

MessageForm là thành phần tạo form message để nhập và post message.

Tạo MessageForm component

// app/assets/javascripts/components/message_form.js.jsx
  var MessageForm = React.createClass({
    render: function() {
      return (
        <form className="commentForm">
          <input type="text" placeholder="Yousr name" />
          <input type="text" placeholder="Message" />
          <input type="submit" value="Post" />
        </form>
      );
    }
  });

Cho MessageForm vào trong MessageBox

// message_box.js.jsx
var MessageBox = React.createClass({
  getInitialState: function() {
    ...
  },

  render: function() {
    var messageItems = this.state.messages.map(function(message) {
      return (
        <MessageItem key={message.id} message={message}/>
      );
    });

    return (
      <div className="messageBox">
        {messageItems}
        <MessageForm />
      </div>
    );
  }
});
  • Xử lý event submit Chỉnh sửa MessageItem để có thể post message khi ấn nút Post. Thêm handleSubmit để xử lý sự kiện submit trong MessageForm.
 var MessageForm = React.createClass({
   handleSubmit: function(event) {
     event.preventDefault();
     var user = this.refs.user.value.trim();
     var text = this.refs.text.value.trim();
     // どちらか入力されてなければ何もしない
     if (!user || !text) {
       return;
     }
     // 親コンポーネントのMessageBoxのイベントを呼ぶ
     this.props.onMessageSubmit({ user: user, text: text });
     // フォームの内容を削除
     this.refs.user.value = '';
     this.refs.text.value = '';
   },

    render: function() {
      return (
       <form className="commentForm" onSubmit={this.handleSubmit}>
         <input type="text" placeholder="Yousr name" ref="user" />
         <input type="text" placeholder="Message" ref="text"/>
          <input type="submit" value="Post" />
        </form>
      );
    }
  });

Khi sự kiện Submit xảy ra onSubmit={this.handleSubmit}, handleSubmit được gọi. Khi chúng ta ấn nút Post, nội dung trong input sẽ được thêm vào trong list.

Thuộc tính key

Post button sẽ thêm một tin nhắn Javascript xuất hiện trong console message như sau:

React.self-bf 407 d 87 .... js? Body = 1: 2166
Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of `MessageBox`. See https://fb.me/react-warning-keys for more information.

MesssageItem là một danh sách element thì chúng ta sẽ set một giá trị key attribute vì khi thêm một message mới từ form chưa có id vậy sẽ thêm vào handleMessageSubmit để tạo unique id

  • Phần sau sẽ nhận dữ liệu từ server nhưng bây giờ chỉ là một cách để tạo id dựa vào ngày tháng.
// message_box.js.jsx
  var MessageBox = React.createClass({
    getInitialState: function() {
      ...
    },

    handleMessageSubmit: function(message) {
+     message.id = new Date();
      var newMessages = this.state.messages.concat(message);
      this.setState({ messages: newMessages });
    },

    render: function() {
      var messageItems = this.state.messages.map(function(message) {
        return (
          <MessageItem key={message.id} message={message}/>
        );
      });

      return (
        <div className="messageBox">
          {messageItems}
          <MessageForm onMessageSubmit={this.handleMessageSubmit}/>
        </div>
      );
    }
  });

6.Kết hợp với phía server (Rails)

Tạo API trong Rails Chúng ta sẽ chuẩn bị 2 api

GET /messages.json - Lấy danh sách tất cả message.
POST /messages.json - Tạo một message

Đầu tiên tạo một controller và model rails g resource Message user:string text:string

rails g resource là tạo file model, migration, controller khi không muốn tạo view.

Thêm route cho API

 # config/routes.rb

  Rails.application.routes.draw do
*   resources :messages, only: [:index, :create], format: 'json'
    root 'top#index'
  end

Trong MessageController tạo phương thức indexcreate

# app/controllers/messages_controller.rb
 class MessagesController < ApplicationController
   def index
     messages = Message.all
     render json: messages
   end

   def create
     message = Message.new(create_params)
     if message.save
       render json: message, status: :created # 201
     else
       render json: message, status: :unprocessable_entity
     end
   end

   private
     def create_params
       params.permit(:user, :text)
     end
 end

Tạo dữ liệu ban đầu

# db/seeds.rb
Message.delete_all
Message.create!([
  { user: 'Tom',   text: 'Good morning' },
  { user: 'John',  text: 'Good afternoon' },
  { user: 'Emily', text: 'Good evening' }
])


# terminal
bundle exec rake db:migrate db:seed

Nhận danh sách message từ server với React

Chúng ta sẽ sử dụng Ajax để lấy message từ server với GET. Cho url API vào trong thuộc tính url của MessageBox

// app/assets/javascripts/main.js.jsx
 $(function() {
   ReactDOM.render(
*     <MessageBox url="/messages"/>,
     document.getElementById('content')
   );
 });

Tiếp đến getInitialState phục vụ khở tạo dữ liệu ban đầu với mảng rỗng, componentDidMount định nghĩa một dánh sách message nhận từ server, setState({ messages: messages }) để set message cho state. componentDidMount là một thuộc tính được gọi tự động bởi React.

/ app/assets/javascripts/components/message_box.js.jsx
 var MessageBox = React.createClass({
   getInitialState: function() {
*     return { messages: [] };
   },

  componentDidMount: function() {
    $.ajax({
      url:      this.props.url,
      dataType: 'json',
      cache:    false,
      success: function(messages) {
        this.setState({ messages: messages });
      }.bind(this),
      eror: function(_xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
   ...
 });

Lý do screen ban đầu trắng xóa chưa có dữ liệu một lúc vì 1- getInitialState set messages là một mảng rỗng. 2- React hiển thị MessageBox, do chưa có message trong list nên chưa hiển thị ra. 3- Khi componentDidMount gọi để lấy dữ liệu message từ server và set cho messages. 4- Trong setState messages được cập nhận và React sẽ render thành phần thay đổi vậy danh sách sẽ hiển thị ra.

Tải và hiển thị dữ liệu

Trên màn hình sẽ hiển thị "Loading" khi đang tải cho đến khi dữ liệu được lấy và thành công.

Vào trong MessageBox, thêm một biến isLoading, với trạng thái đang tải có giá trị là true khi tải thành công có giá trị là false.

var MessageBox = React.createClass({
  getInitialState: function() {
    // isLoading = true display the loading
    return { messages: [], isLoading: true };
  },

  componentDidMount: function() {
    $.ajax({
      url: this.props.url,
      dataType: "json",
      cache: false,
      success: function(messages) {
        // isLoading = false hide loading
        this.setState({messages: messages, isLoading: false });
      }.bind(this),
      error: function(_xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },

  handleMessageSubmit: function(message) {
    $.ajax({
      url: this.props.url,
      dataType: "json",
      type: "POST",
      data: message,
      success: function(message) {
        var newMessages = this.state.messages.concat(message);
        this.setState({messages: newMessages});
      }.bind(this),
      error: function(_xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },

  render: function() {
    var messageItems = this.state.messages.map(function(message) {
      return (
        <MessageItem key={message.id} message={message}/>
      );
    });
    // Change the content to render display by value of isLoading
    if(this.state.isLoading) {
      return (
        <div>Loading</div>
      );
    } else {
      return (
        <div>
          <div className="messageBox">
            {messageItems}
            <MessageForm onMessageSubmit={this.handleMessageSubmit}/>
          </div>
        </div>
      );
    }
  }
});

Post message tới server với React

Khi ấn nút Post chúng ta cũng sử dụng ajax để gửi thông tin trong form lên server để ghi vào DB.

// app/assets/javascripts/components/message_box.js.jsx
  var MessageBox = React.createClass({
    ...

   handleMessageSubmit: function(message) {
     $.ajax({
       url:      this.props.url,
       dataType: 'json',
       type:     'POST',
       data:     message,
       success: function(message) {
*         var newMessages = this.state.messages.concat(message);
          this.setState({ messages: newMessages });
       }.bind(this),
       error: function(_xhr, status, err) {
         console.error(this.props.url, status, err.toString());
       }.bind(this)
     });
   },

    ...
  }

Dưới đây là một message được tạo.

Kết luận

Đây là một bài viết ngắn để giúp hiểu nhanh hơn khi bắt đầu tìm hiểu về ReactJS.

Tham khảo

Tutorial: Intro To React Code

All Rights Reserved