A little bit of ReactJS

Nội dung bài viết được hiểu theo ý hiểu của tác giả nên ko thể tránh khỏi những sai lầm hoặc chưa thấu đáo. Nếu có, xin hãy comment để chúng ta cùng thảo luận

Intro về ReactJS

Dạo gần đây Facebook có tiến hành open source ReactJS, một framework được dùng để xây dựng UI cho các ứng dụng web của công ty, ví dụ như https://instagram.com/. Mình cũng thử tìm hiểu về ReactJS và làm thử một demo nhỏ cho cuộc thi CTF sắp tới. Có thể nói ReactJS có 3 đặc điểm chính sau:

  • Đơn giản là một framework cho việc xây dựng UI, tức là phần V trong mô hình MVC
  • Virtual DOM, ReactJS xây dựng riêng 1 DOM ảo, chứa mô tả về DOM thật, nhằm mục đích nhanh chóng tìm ra vị trí hoặc phần tử thay đổi để thực hiện việc thay đổi tương ứng trên DOM thật
  • Data Flow: ReactJS theo mô hình dữ liệu 1 chiều, 1 nguồn dữ liệu duy nhất, ta sẽ thấy nó qua các concept sau của ReactJS

Các concept cơ bản của ReactJS

Để thấy rõ hơn các điều này, ta cùng nhau thực hiện một demo nho nhỏ. Trên https://facebook.github.io/react/docs/tutorial.html đã có sẵn một demo khá chi tiết và cơ bản về các concept của ReactJS qua ví dụ về tạo một trang comment, ở đây ta sẽ là 1 demo tương tự.

  • Làm một bảng các task: các task sẽ có thông tin về task, thể loại, số điểm, trạng thái
  • Khi click vào 1 một task thì ở khung task view sẽ hiển thị thêm các thông tin của task đó

Đây là toàn bộ code của phần demo:

<html>
  <head>
    <title>Hello React</title>
    <script src="https://fb.me/react-0.13.0.js"></script>
    <script src="https://fb.me/JSXTransformer-0.13.0.js"></script>
    <script src="https://code.jquery.com/jquery-1.10.0.min.js"></script>
    <style type="text/css" media="screen">
      .TreeNode {
        width: 500px;
        height: 100px;
      }
      .locked {
        background-color: red;
      }
      .unlocked {
        background-color: green;
      }
    </style>
  </head>
  <body>
    <div id="content"></div>

    <script type="text/jsx">
      var tasks = [
        {id: 12, 'category': 'code', 'status': 'locked'},
        {id: 13, 'category': 'ctf', 'status': 'unlocked', 'content': 'sample content'}
      ]

      var TreeNode = React.createClass({
        getDefaultProps: function() {
          return {
            status: 'locked',
            category: 'unknown',
            content: '',
          };
        },
        handleClick: function() {
          console.log('Clicked ' + this.props.reactKey);
          this.props.gotClicked(this.props.reactKey);
        },
        render: function() {
          var classString = 'TreeNode ' + this.props.status;
          return (
            <div onClick={this.handleClick} className={classString} >
              A Tree node - {this.props.status} - {this.props.category} - {this.props.content}
            </div>
          );
        }
      });

      var TaskView = React.createClass({
        getDefaultProps: function() {
          return {
            task: {}
          };
        },
        render: function() {
          return (
            <div className="TaskView">
              A Task View - {this.props.task.status} - {this.props.task.category} - {this.props.task.content}
            </div>
          );
        }
      });

      var SkillTree = React.createClass({
        getInitialState: function() {
            return {
              tasks: [],
              current_task: {}
            };
        },
        loadAllTasksFromServer: function() {
          this.setState({tasks: tasks});
        },
        componentDidMount: function() {
          console.log('loadAllTasksFromServer');
          this.loadAllTasksFromServer();
        },
        handleNodeClick: function(task_id) {
          console.log('Got node clicked ' + task_id);
          for (var i = 0; i < tasks.length; i++) {
            if (task_id == tasks[i].id) {
              this.setState({current_task: tasks[i]});
            }
            console.log(tasks[i]);
          };
        },
        render: function() {
          var nodes = this.state.tasks.map(function(node) {
            return (
              <TreeNode key={node.id} gotClicked={this.handleNodeClick} reactKey={node.id} category={node.category} status={node.status} className="TreeNode" />
            );
          }.bind(this));
          return (
            <div className="SkillTree">
              <h1>A Skill Tree</h1>
              {nodes}
              <TaskView task={this.state.current_task} />
            </div>
          );
        }
      });
      // replace with value from server
      React.render(
        <SkillTree />,
        document.getElementById('content')
      );
    </script>
  </body>
</html>

Component

Concept đầu tiên chính là Component: Các thành phần trong React sẽ được chia thàn các component, trong đó thì lại có cả cha và con, nhờ có cấu trúc như vậy sẽ có rất nhiều lợi ích. Component yêu cầu cần có hàm render, là nơi sẽ thực hiện hiển thị ra màn hình. Ta sẽ tổ chức như sau:

  • Cha: Skill Tree: là một wrapper bao ngoài cho toàn bộ phần hiển thị,
  • Con gồm có
    • Tree Node: là các task của bảng, sẽ có phần tương tác click.
    • Task View: phần hiển thị chi tiết của task
  • Skill Tree sẽ có nhiều Tree Node và chỉ có Task View duy nhất
                                                
              Skill Tree
+--------------------------------------+
|                                      |
| +----------------+  +--------------+ |
| |   Tree Node    |  |   Tree Node  | |
| +----------------+  +--------------+ |
|                                      |
|         +------------------+         |
|         |    Tree Node     |         |
|         +------------------+         |
|                                      |
|  +--------------------------------+  |
|  |                                |  |
|  |        Task View               |  |
|  |                                |  |
|  |                                |  |
|  +--------------------------------+  |
|                                      |
+--------------------------------------+

Render

Ở tại

React.render(
    <SkillTree />,
    document.getElementById('content')
);

thì tag SkillTree chính là một component được render vào mount point là div có id là content

Code của component hiện đang được viết bằng JSX, một kiểu biểu diễn Component bằng XML, cụ thể hơn, bạn hãy đọc thêm tại https://facebook.github.io/react/docs/jsx-in-depth.html Và ở dưới đây:

render: function() {
  return (
    <div className="TaskView">
      A Task View - {this.props.task.status} - {this.props.task.category} - {this.props.task.content}
    </div>
  );
}

tại method render của task View, ta có thể thấy Task View sẽ được render ra như là một div cùng với các thuộc tính...

Props

And, here we go, concept tiếp theo chính là Props thuộc tính của component. Những thuộc tính này là unmutable - ko thay đổi và được truyền xuống từ cha như là 1 attribute, như của Task View sẽ là từ method render của cha nó, Skill Tree

<TaskView task={this.state.current_task} />

Tạm thời bạn chưa cần quan tâm this.state.current_task là gì vội. Task view nhận thuộc tính task từ cha, là 1 object có dạng:

{id: 13, 'category': 'ctf', 'status': 'unlocked', 'content': 'sample content'}

Với thuộc tính nhận được này thì, Task View có thể truy cập theo this.props.task để hiện thị ra. 1 câu hỏi, trong trường hợp cha ko truyền thuộc tính cho con thì sao ?. Đã có method:

getDefaultProps: function() {
  return {
    task: {}
  };
},

và tác dụng của nó thì chắc bạn cũng đã đoán được rồi. Vậy Task View chỉ đơn giản render ra những thứ mà nó nhận được, hoặc là default value

State

Nếu chỉ có như vậy thì web của chúng ta hiện tại vẫn là tĩnh, chưa có gì thay đổi cả vì props là unmutable. Quay lại với this.state.current_task, đây chính là concept tiếp theo: State, trạng thái của Component, phần động. Trạng thái của Componet sẽ thay đổi, ta có thể thay đổi bằng cách gọi method setState, và lúc đó sẽ trigger render để hiển thị lại Component.

Vậy Component nào cần có state, Component nào ko cần ? Theo mình hiểu, hãy đưa state vào nơi cần dùng, và hãy đưa chuyển lên cho cha. Như ví dụ của chúng ta:

  • Task View: ko cần có state vì nhiệm vụ của nó chỉ là render những thứ được truyền vào
  • Tree Node: ngoài event click thì cũng tương tự, ko cần có state vì nhiệm vụ của nó chỉ là render những thứ được truyền vào
  • Skill Tree: sẽ nhận các event tương ứng, thay đổi state của nó, truyền state đã đổi đó như là props cho các con để chúng render lại

That's it

Quay lại với yêu cầu của demo, lúc đầu ta sẽ hoàn toàn ko có dữ liệu, bởi ReactJS là V, hiển thị, ta cần load dữ liệu từ 1 nguồn nào đó về trước khi Component được load. Ta thực hiện nó tại life cycle method: chi tiết hơn tại https://facebook.github.io/react/docs/component-specs.html

componentDidMount: function() {
  console.log('loadAllTasksFromServer');
  this.loadAllTasksFromServer();
},

và tại loadAllTasksFromServer, ta sẽ ajax lấy dữ liệu về và set state tasks cho Skill Tree (ở đây ta khởi tạo sẵn biến global tasks)

loadAllTasksFromServer: function() {
  this.setState({tasks: tasks});
},
var tasks = [
  {id: 12, 'category': 'code', 'status': 'locked'},
  {id: 13, 'category': 'ctf', 'status': 'unlocked', 'content': 'sample content'}
]

Có dữ liệu ban đầu rồi, ta có thể truyền các thông tin này như là props cho các Tree Node, mỗi node ta truyền thêm reactKey như là định danh (id) cho từng component con

var nodes = this.state.tasks.map(function(node) {
  return (
    <TreeNode key={node.id} gotClicked={this.handleNodeClick} reactKey={node.id} category={node.category} status={node.status} className="TreeNode" />
  );
}.bind(this));

Passing Event

Như ở trên ta đã truyền callback handleNodeClick cho các Tree Node dưới dạng thuộc tính gotClicked. Ta cần dùng bind để các node có thể hiểu tương ứng this là gì. Hãy cùng xem qua phần code của Tree Node:

handleClick: function() {
  console.log('Clicked ' + this.props.reactKey);
  this.props.gotClicked(this.props.reactKey);
},
render: function() {
  var classString = 'TreeNode ' + this.props.status;
  return (
    <div onClick={this.handleClick} className={classString} >
      A Tree node - {this.props.status} - {this.props.category} - {this.props.content}
    </div>
  );
}

Ở phần render của Tree Node, ta có sự kiện onClick ở trên div, được xử lý bởi handleClick. Tại đây, ta sử dụng thuộc tính gotClicked được cha truyền cho để truyền lại id thông báo là node con đã được click, khi đó, tại method callback handleNodeClickcủa cha:

handleNodeClick: function(task_id) {
  console.log('Got node clicked ' + task_id);
  for (var i = 0; i < tasks.length; i++) {
    if (task_id == tasks[i].id) {
      this.setState({current_task: tasks[i]});
    }
    console.log(tasks[i]);
  };
},

ta set state current_task -> trigger render của Task View. Túm lại sơ đồ sẽ là

Tree Node                              Skill Tree
onClick -> handleClick
           this.props.gotClicked ----> handleNodeClick

                                       update state,...

Time for a dump demo

d2952ab8089f67e262338e4d2ca531ba.png

Final

Cùng tổng hợp lại:

  • Component
  • State
  • Props

Đọc thêm các vấn đề nâng cao hơn tại: