Hiển thị các bài đăng có nhãn Reactjs. Hiển thị tất cả bài đăng
Hiển thị các bài đăng có nhãn Reactjs. Hiển thị tất cả bài đăng

Reactjs - Dùng React-Router và Material UI để tạo Web App có nhiều page

Giới thiệu

Trong bài viết trước về react-router, tôi có giới thiệu sơ qua về react-router, tuy nhiên nội đó đã khá cũ, và react-router cũng đã có nhiều thay đổi. Sau bài viết giới thiệu việc dùng Material UI Component để tạo Web App, tôi muốn thêm phần routing vào Web App đó để nó được hoàn chỉnh hơn và nhân đó tôi muốn thêm về react-router mới và kết hợp nó với MUI để tạo một Web App.

Server side and Client side Routing

Trước khi đi vào vấn đề chính tôi cũng muốn giới thiệu sơ qua về cách routing giữa server side và client side.

Server Side Routting

Mô hình server side routing: khi có request từ Client thì phía server sẽ có bộ Router để nhận request đó, và tương ứng với route hay path từ client mà Server sẽ render nội dung Page tương ứng.
rr.serverSideRender

Client Side Routting

Mô hình client side routing: bộ Router và Render sẽ không nằm ở phía server mà sẽ do phía Client đảm nhận. Trong URL chuyển trang, sẽ có ký hiệu hashtag # để browser không chuyển request này xuống server mà chỉ xử lý ở phía Client thôi.
rr.clientSideRender

React-router

React-Router là một thư việc của React, giúp các React App có thể routing ở phía client.
Bên dưới là lời giới thiệu từ HP của react-router:
“React Router is a complete routing library for React.
React Router keeps your UI in sync with the URL. It has a simple API with powerful features like lazy code loading, dynamic route matching, and location transition handling built right in. Make the URL your first thought, not an after-thought.”
Trong phạm vi bài viết này tôi chỉ giới thiệu cách dùng cơ bản nhất của react-router như render 1 route, nest các route mà không giới thiệu các nội dung khác. Các nội dung khác của react-router, bạn có thể tham khảo thêm tại:
https://github.com/reactjs/react-router-tutorial/tree/master/lessons

3 Components chính của React Router:

  1. Router
  2. Route
  3. Link
rr.overview
Cơ bản, component Link dùng để dẫn hướng trang (component) cần trỏ đến, Route là component kết nối giữa path và component tương ứng với path đó, còn Router thì wrap tất cả các Route con.

Sample

Chúng ta thử tạo 2 sample, một là chỉ đơn thuần là render 1 route tương ứng với 1 URL, hai là lồng các route con trong 1 route cha (nested route)

Simple Route

Ở sample này ta chỉ quan tâm đến vấn đề React App sẽ render Route Component tương ứng với URL (path) nó Route đó đã config.
Cụ thể
  • khi URL là #/page-1 thì nó render component Page1
  • khi URL là #/page-2 thì nó render component Page2
  • khi URL là #/page-3 thì nó render component Page3
rr.sample.simple

Nested Route

Ở sample này, chúng ta sẽ đi sâu hơn 1 chút. Cụ thể sẽ lồng các route con là #/page-1, #/page-2, #/page-3 vào trong route cha là “/”.
Có các vấn đề về nested route như sau:
  1. React App được xây dựng từ việc lồng các Component con trong các Component cha.
  2. Khi chuyển trang thì có những component như Header, Navigation sẽ không thay đổi. Nên thực ra mình chỉ render lại Component Node nào cần thay đổi thôi.
  3. Khi nest các Route thì nó cũng sẽ tự động nest các Component tương ứng.
  4. Trình tự tạo React App sẽ là xác định route cha (App Route), sau đó đặt các Route con (Page Route) bên trong nó. Sau đó, render các Component con (Page) tương ứng bên trong Component cha (App).
rr.sample.nest

Coding

Init Project

Tham khảo Bài viết về reactjs và material-ui

Cấu trúc Project

.
|-- dist                           // build 
|-- node_modules                   // node_modules folder
|-- package.json                   // project packages
|-- readme.md                      // readme
|-- simple.html                    // HTML for simple route
|-- simpleRouteApp.js              // simple route js 
|-- nested.html                    // HTML for nested route
|-- nestedRouteApp.js              // nested route js
|-- webpack-nested-route.config.js // webpack config for nested route
`-- webpack-simple-route.config.js // webpack config for simple route 

Giải thích nội dung config file

Package.json


{
  "name": "react-mui-router-sample",
  "version": "1.0.0",
  "description": "reactjs-mui-router-sample",
  "main": "app.js",
  "scripts": {
    "test": "test",
    "build-simple": "webpack --config webpack-simple-route.config.js --progress --colors",
    "build-nest": "webpack --config webpack-nested-route.config.js --progress --colors"
  },
  "repository": {
    "type": "git"
  },
  "keywords": [
    "reactjs",
    "material-ui",
    "react-router"
  ],
  "author": "phungnc",
  "license": "ISC",
  "homepage": "/reactjs-mui-router-sample#readme",
  "devDependencies": {
    "babel-core": "^6.9.1",
    "babel-loader": "^6.2.4",
    "babel-preset-es2015": "^6.9.0",
    "babel-preset-react": "^6.5.0",
    "material-ui": "^0.15.1",
    "react": "^15.1.0",
    "react-dom": "^15.1.0",
    "react-router": "^2.4.1",
    "react-tap-event-plugin": "^1.0.0",
    "webpack": "^1.13.1"
  }
}
Có 2 dòng scripts khác với lúc trước đó là
    "build-simple": "webpack --config webpack-simple-route.config.js --progress --colors",
    "build-nest": "webpack --config webpack-nested-route.config.js --progress --colors"
build-simple có nghĩa là sẽ call file webpack-simple-route.config.js và build-nest sẽ call file webpack-nested-route.config.js.
Lệnh call tương ứng sẽ là:
npm run build-simple
npm run build-nest

webpack config

module.exports = {
  entry: './nestedRouteApp.js',
  output: {
    path: __dirname + '/dist',
    filename: 'nestedRouteApp-bundle.js'
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        loaders: ['babel-loader'],
        exclude:  /(node_modules)/
      }
    ]
  }
}
module.exports = {
  entry: './nestedRouteApp.js',
  output: {
    path: __dirname + '/dist',
    filename: 'nestedRouteApp-bundle.js'
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        loaders: ['babel-loader'],
        exclude:  /(node_modules)/
      }
    ]
  }
}
Source code mẫu cho 2 sample này, tôi có để trên Github:
https://github.com/phungnc/reactjs-mui-router-sample.git
Sau khi fork về, bạn hãy mở các file js tương ứng và tham khảo thêm hình giải thích bên dưới:

Simple Route Sample

rr.coding.simple

Nested Route Sample

rr.coding.nest

Run:

Install node modules:

npm install

Build

// build simple sample
npm run build-simple

// build nested route sample
npm run build-nest

Run

open simple sample
open simple.html
Thử vào các link
../simple.html#/page-1
../simple.html#/page-2
../simple.html#/page-3
open nested route sample
open nested.html
Thử click vào các link của Left Nav

Tham khảo

  1. Bài viết về webpack
  2. Bài viết cơ bản về Reactjs và Material-UI
  3. Loạt bài viết về chủ để Reactjs

Reactjs - Dùng Material UI Component thử tạo một trang web vô cùng đơn giản

Giới thiệu

Material UI Component được tạo ra dựa trên sự kết hợp giữa lý thuyết design của Google và thư viện tạo UI Reactjs của Facebook.
material-ui-component
Xin mời bạn vào trang: http://www.material-ui.com, vào mục components để xem tất cả các MUI Components mà nó đang cung cấp.

Sample

Bây giờ chúng ta thử build 1 layout như sau.
material-ui-component
Phần bên ngoài bọc tất cả, chúng ta gọi là App, trong App có phần header, bên dưới header là container bọc lấy các block.
Bây giờ ta tìm trong MUI Component, thì chúng ta có thể hình dung các MUI Component tương ứng sẽ như sau:
material-ui-component

Howto

Technology

Để thực hiện sample trên, chúng ta sử dụng các tool như là babel, để compile es2015webpack để load các modules, component cần thiết.
material-ui-component

Chuẩn bị

  1. Tạo thư mục: mkdir react-mui-sample
  2. Init project: npm init, khi chạy lệnh này trên màn hình terminal sẽ yêu cầu bạn nhập các thông tin cần thiết.
  3. Install React, ReactDOM, MaterialUI và plugin, tool
npm install react react-dom react-tap-event-plugin material-ui --save-dev
npm install webpack babel-cli babel-preset-es2015 babel-preset-react babel-loader --save-dev
Sau đó, vào thư mục project react-mui-sample, tạo file config cho babel .babelrc, nhập nội dung bên dưới vào file đó để setting preset.
{
  "presets": ["es2015", "react"]
}
Tiếp theo là thêm dòng "build": "webpack --config webpack.config.js --progress --colors" vào file package.json mà bạn đã init ở trên:
{
  "name": "react-mui-sample",
  "version": "1.0.0",
  "description": "this is sample that use material ui component for demo",
  "main": "index.js",
  "scripts": {
    "test": "test",
    "build": "webpack --config webpack.config.js --progress --colors"
  },
  "keywords": [
    "reactjs",
    "material-ui"
  ],
  "author": "phungnc",
  "license": "ISC",
  "devDependencies": {
    "babel-cli": "^6.10.1",
    "babel-core": "^6.9.1",
    "babel-loader": "^6.2.4",
    "babel-preset-es2015": "^6.9.0",
    "babel-preset-react": "^6.5.0",
    "material-ui": "^0.15.0",
    "react": "^15.1.0",
    "react-dom": "^15.1.0",
    "react-tap-event-plugin": "^1.0.0",
    "webpack": "^1.13.1"
  }
}
Cuối cùng thì config cho webpack:
module.exports = {
  entry: {
    app: './app.js'
  },
  output: {
    path: __dirname + '/dist',
    filename: 'bundle.js'
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        loaders: ['babel-loader'],
        exclude:  /(node_modules)/
      }
    ]
  }
}
Rồi. Bây giờ hãy ngó chút xíu qua phần usage của Material UI, trước khi chúng ta bước vào phần coding thực sự.
http://www.material-ui.com/#/get-started/usage

Coding, cấu trúc app:

Trước khi bắt đầu coding thì chúng ta hãy hình dung sơ qua về cấu trúc cũng như luồng build của nó như thế nào:
material-ui-component
Cấu trúc source code của project sẽ như sau:
react-mui-sample
 |_ .babelrc             // file config của babel
 |_ webpack.config.json  // file config của webpack
 |_ package.json         // file package của project
 |_ node_module
 |_ app.js               // file chính của App
 |_ index.html           // file giao diện chính của App
 |_ dist                 // thư mục chứa file build 
    |_ bundle.js         // file build 
Source code project:
https://github.com/phungnc/reactjs-mui-simple-sample.git
Sau khi lấy về, hãy mở file app.js, và tham khảo hình tôi giải thích như bên dưới:
material-ui-component

Build and Run

Hãy trỏ đến thư mục react-mui-sample, thực hiện lệnh build:
npm run build
Lệnh này thực ra sẽ gọi script webpack --config webpack.config.js --progress --colors trong file package.json.
Và bây giờ bạn có thể mở file index.html lên và xem thành quả. Bạn có thể edit lại nội dung theo ý mình bằng cách thay đổi file app.js
Hy vọng bài viết này có thể giúp bạn bắt tay vào việc thử xây dựng cho riêng mình 1 giao diện web app bằng Material UI Component. Trong bài viết sau, tôi sẽ đưa ra 1 sample và hướng dẫn cách tạo 1 App phức tạp hơn.

Reactjs - Bài 18 - Kiến trúc Flux

Home page:
http://facebook.github.io/flux/
Overview:
“Flux is the application architecture that Facebook uses for building client-side web applications. It complements React’s composable view components by utilizing a unidirectional data flow. It’s more of a pattern rather than a formal framework, and you can start using Flux immediately without a lot of new code.”
Có 3 ý chính:
- Flux là architecture hơn là một formal framework.
- Sử dụng cho việc xây dựng client-side web applications.
- Nó bổ sung khả năng kết hợp các view components của React bằng việc sử dụng một unidirectional data flow
Kiến trúc Flux trên Homepage Flux:
Flux Architech
Như hình minh họa trên, tôi nêu sơ lược các phần trong Flux, còn cụ thể như thế nào thì bạn phải bắt tay vào code thì mới hiểu rõ nhất.
  • View : là các React Component.
  • Store: là nơi chứa, xử lý dữ liệu cho App.
  • Action: là nơi khởi tạo action.
  • Dispatcher: là nơi phát action.
Bây giờ, chúng ta sẽ bắt đầu tìm hiểu về Flux thông qua ví dụ mà chúng ta đã sử dụng. Tôi sẽ đi theo hướng là thay đổi lần lần cách thức hoạt động cũ thành Flux, cho các bạn dễ dàng nhận ra sự khác biệt.
Bạn có thể clone source code trên Github theo link bên dưới, chuyển sang nhánh flux, và chọn từng node commit để theo dõi theo trình tự mà tôi sẽ trình bày sau.
https://github.com/phungnc/reactjs-tut-demo/tree/feature/flux/flux-client

Store - (Static) View

Khoan đi vào Actions, Dispatcher, trước mắt chúng ta thử xem mối quan hệ giữa View và Store.
Chia phần ứng dụng thành các file:
- Member.react.js đóng vai trò như là 1 Item View
- MemberApp.react.js đóng vai trò như là 1 List Item View
- app.js đóng vai trò như là root app, sẽ chứa MemberApp
- index.html vẫn như cũ
- MemberStore.js đóng vai trò như model data của Member.
js
|_index.html
|_app.js
|_stores
| |_ MemberStore.js
|
|_views
| |_ Member.react.js
| |_ MemberApp.react.js
MemberStore.js
// MemberMemberStore.jsjs
var EventEmitter = require('events').EventEmitter;

var _members = {
      members: [
        {id:1, name: "Avatar 1", like: false, src: "http://canime.files.wordpress.com/2010/05/mask-dtb.jpg"},
        {id:2, name: "Avatar 2", like: true, src: "http://z4.ifrm.com/30544/116/0/a3359905/avatar-3359905.jpg"},
        {id:3, name: "Avatar 3", like: false, src: "http://www.dodaj.rs/f/O/IM/OxPONIh/134.jpg"}
      ]
};

class MemberStore extends EventEmitter {
  constructor() {
    super();

  }
  getMembers() {
    return _members;
  }
}
module.exports = new MemberStore();
Ở đây MemberStore chỉ đơn thuần là một Object có phương thức getMembers() trả về dữ liệu members
Ở file MemberApp.react.js, ta chỉ cần thay đổi nội dung trả về từ members bằng việc trả về kết quả từ phương thức getMembers() từ MemberStore. Tất nhiên để làm được chuyện này ta phải require MemberStoreMemberApp.react.js
Với source này tôi có đưa syntax ES6 vào cho nên chúng ta cần babelify kết hợp với browserify.
Rồi dùng command như sau để bundle up file bundle.js
browserify app.js -t babelify -o bundle.js
Sau đó bạn thử open file index.html, thử thao tác các button, ¯_(ツ)_/¯ mọi thứ vẫn như xưa. Như vậy chúng ta đã xong việc lấy dữ liệu từ Store để hiển thị lên View.

View - Actions

Phần này ta sẽ đi vào mối quan hệ giữa View và Action.
Trong kiến trúc Flux, Action đóng vai trò tiếp nhận action từ View vào tạo action cho App.

Thử với delete action - xây dựng mối quan hệ

Thêm file MemberActions.js trong thư mục action. Đầu tiên ta chỉ cần có một destroy action:
module.exports = {
  destroy: id => {
    alert ("Destroy " + id);
  }
};
Trong file, Member.react.js, import file MemberActions.js
var MemberActionCreators = require('../actions/MemberActions');
Nhờ vậy Member.react.js có thể gọi method destroy từ MemberActions.js.
 _onDelete() {
   MemberActions.destroy(this.props.id);
 },

Actions and Action Creators

Ở trên, ta đã xây dựng được mối quan hệ giữa View và Actions: hễ User click vào button delete ở View thì Actions có nhiệm vụ alert dòng chữ “Destroy {id}” với id là dữ liệu được truyền từ View thông qua method MemberActions.destroy();. Chúng ta thử phân tích về thông tin mà nó alert lên:
  • Destroy: là một loại action hay Action Type
  • id: là data mà View truyền tới.
2 yếu tố trên tạo thành 1 action. Chúng ta có thể xem 1 Action = {Action Type, Data}.
Thực ra nếu bạn đã quen thuộc với khái niệm Event thì chắc hẳn sẽ thấy Action rất giống Event.
Action Creators đóng vai trò như một handling giữa Actions và Dispatcher. Tập hợp các Actions Creator sẽ tạo thành một module chứa các API mà ta sẽ dễ dàng sử dụng sau này.
  destroy: id => {
    AppDispatcher.dispatch({
      type: MemberConstants.MEMBER_DESTROY, 
      id: id
    });
  }
{
  type: MemberConstants.MEMBER_DESTROY, 
  id: id
}
là một Action, việc gắn Action với Dispatcher ta gọi là Action Creator.

Dispatcher

Dispatcher đóng vai trò như một bộ phát tín hiệu, phát đi Actions mà nó đã được ‘gắn’ vào.

View - Actions - Dispatcher - Store (- View)

Dispatcher - Store

Dispatcher sẽ phát ‘tín hiệu Actions’ tới Store.
(View thông qua Action Creators để tạo Action và nhờ Dispatcher phát Action này đến Store). Store lúc này sẽ làm nhiệm vụ phân loại Action để đưa ra xử lý, phần xử lý này sẽ nằm trong method constructor():
  constructor() {
    super();
    _dispatchToken = AppDispatcher.register(action => {
      switch(action.type) {

        case MemberConstants.MEMBER_DESTROY:
          alert(action.type + ' ' +action.id);
          break;
      }
    });
  }
Để Dispatcher có thể phát Action Type, ta cần Contants để chứa Action Type đó.
MemberConstants.js
module.exports = {
  MEMBER_DESTROY : 'MEMBER_DESTROY'
};
Hãy thử chạy souce code, bây giờ MemberStore đã nhận được tín hiệu Action bao gồm Action Type và Action Data từ Dispatcher.

Store - View

OK, chúng ta đã thấy “dòng” Action từ View đến Store. Như đã giới thiệu ở trên, Store đóng vai trò quản lý dữ liệu của app, mọi thay đổi dữ liệu của App đều phải được xử lý ở Store. Và dĩ nhiên sự thay đổi dữ liệu này sẽ không có ý nghĩa với User nếu nó không phản ánh lên UI. Do đó chúng ta phải phản ánh Action này lên View: khi user delete Member Item thì Item này sẽ bị remove. Việc này bao gồm 3 bước:
1. Remove dữ liệu ở Store.
2. Phát sự kiện thay đổi từ Store.
3. Bắt sự kiện và thực hiện việc remove Item ở View

1. Remove dữ liệu ở Store:

Dữ liệu ở Store là Collection _members.
Method xóa một Item:
function destroy(id) {
  let memberIndex = _members.findIndex(member => member.id == id);
  delete _members[memberIndex];
};
Ở đây tôi dùng ES6 polyfill array.prototype.findindex để tìm Item cần remove ra khỏi Array _member.
Và tại nơi xử lý loại action, ta sẽ gọi method detroy() này
switch(action.type) {  
  case MemberConstants.MEMBER_DESTROY:
    destroy(action.id);
    break;
}

2. Phát sự kiện thay đổi từ Store.

Sau khi đã thực hiện xong việc remove Item ra khỏi Array, ta sẽ phát sự kiện thay đổi này:
switch(action.type) {  
  case MemberConstants.MEMBER_DESTROY:
    destroy(action.id);
    this.emit('change');
    break;
}

3. Bắt sự kiện và thực hiện việc remove Item ở View

  • Bắt sự kiện:
Việc thiết lập bắt sự kiện này sẽ được thực hiện sau khi component đã được render.
componentDidMount() {
  MemberStore.on('change',this._onChange);
}
Khi có sự kiện ‘change’ thì nó sẽ gọi handler _onChange (sẽ nói sau) để thực hiện việc update thay đổi đó.
  • Remove Item:
_onChange() {
  this.setState({members: MemberStore.getMembers()});
}
Khi có sự thay đổi ở Store, ta chỉ cần update lại state của MemberApp, khi đó MemberApp.react sẽ tự động render lại View (chắc bạn đã làm quen với điều này ở các bài trước).
  • Remove event
Vì sau khi remove Item, Component sẽ render lại, cho nên chúng ta cần remove Event để tránh bị double, và việc removeListener sẽ được thực hiện trước khi render().
componentWillUnmount() {
  MemberStore.removeListener('change',this._onChange);
}
Sau khi Component được render, chúng ta lại thiết lập lại việc bắt sự kiện như ở trên.
MemberApp.react.js
var React   = require('react/addons'),
    request = require('superagent'),
    Router  = require('react-router'),
    mui     = require('material-ui'),
    Member  = require('./Member.react')
    ;

var Route         = Router.Route;
var RouteHandler  = Router.RouteHandler;
var DefaultRoute  = Router.DefaultRoute;
var Link          = Router.Link;

var ThemeManager = new mui.Styles.ThemeManager();

var List     = mui.List;
var ListDivider  = mui.ListDivider;

var MemberStore = require('../stores/MemberStore');


var MemberApp = React.createClass({
  childContextTypes: {
    muiTheme: React.PropTypes.object
  },
  getChildContext() {
    return {
      muiTheme: ThemeManager.getCurrentTheme()
    };
  },
  getInitialState() {
    return {
      members: MemberStore.getMembers()
    }
  },
//  deleteItem(id){
//    this.setState({
//      members: this.state.members.filter(function(member){
//        return member.id !== id;
//      })
//    });
//  },
  componentDidMount() {
    MemberStore.on('change',this._onChange);
  },
  componentWillUnmount() {
    MemberStore.removeListener('change',this._onChange);
  },
  // Event hanlder for 'change' event comming from the MemberStore
  _onChange() {
    this.setState({members: MemberStore.getMembers()});
  },
  render() {
    var members = this.state.members.map(function(member){
      return (
        <div>
          <Member id={member.id} onDelete={this.deleteItem} name={member.name} initialLike={member.like} src={member.src}/>
          <ListDivider inset={true}/>
        </div>
        );
    }, this);
    return (<List>{members}</List>);
  }
})

module.exports = MemberApp;
Lưu ý: chúng ta không còn cần method deleteItem(id) nữa.
MemberStore.js
// MemberStore.js
var AppDispatcher = require('../AppDispatcher');
var MemberConstants = require('../MemberConstants');

var EventEmitter = require('events').EventEmitter;

var _dispatchToken;
// Member Collections
var _members = [
  {id:1, name: "Avatar 1", like: false, src: "http://canime.files.wordpress.com/2010/05/mask-dtb.jpg"},
  {id:2, name: "Avatar 2", like: true, src: "http://z4.ifrm.com/30544/116/0/a3359905/avatar-3359905.jpg"},
  {id:3, name: "Avatar 3", like: false, src: "http://www.dodaj.rs/f/O/IM/OxPONIh/134.jpg"}
];
require('array.prototype.findindex');

function destroy(id) {
  let memberIndex = _members.findIndex(member => member.id == id);
  delete _members[memberIndex];
};

class MemberStore extends EventEmitter {
  constructor() {
    super();
    _dispatchToken = AppDispatcher.register(action => {
      switch(action.type) {
        case MemberConstants.MEMBER_DESTROY:
          destroy(action.id);
          this.emit('change');
          break;
      }
    });
  }

  getDispatchToken() {
    return _dispatchToken;
  }

  getMembers() {
    return _members;
  }
}

module.exports = new MemberStore();
Cấu trúc 1 Store về cơ bản sẽ như sau (theo hướng TOP-DOWN)
TOP:
- require(import) module cần dùng.
- private variables 
- private method: thực hiện việc xử lý dữ liệu, như `function destroy(id)`
MIDDLE:
- switch … case: phân loại Action Type, gọi method xử lý và phát sự kiện tương ứng.
BOTTOM:
- public method (getter) get data từ Store, như method `getMembers()`
Done!

Reactjs - Bài 17 - Tìm hiểu Jest Framework là gì

Goal

Giới thiệu Reactjs Testing Framework: Jest

Testing Framework

Phần TestUtils đã cho bạn khái niệm về các phương thức test React Component, bây giờ chúng ta sẽ thử ứng dụng Testing Framework.

Mặc dù khi tìm hiểu các bài viết về Reactjs Testing Framework, tôi có thấy nhiều ý kiến cho rằng Jest không bằng các Javascript Testing Framework khác như Mocha hay Karma, nhưng vì theo tôi đang viết theo series về Reactjs nên ở đây tôi sẽ tìm hiểu và giới thiệu Jest.

Jest

From Offical Page:

Jest provides you with multiple layers on top of Jasmine:

1.Automatically finds tests to execute in your repo
2.Automatically mocks dependencies for you when running your tests
3.Allows you to test asynchronous code synchronously
4.Runs your tests with a fake DOM implementation (via jsdom) so that your tests can run on the command line
5.Runs tests in parallel processes so that they finish sooner

Mock trong Jest

Tôi nghĩ tôi cần giải thích sơ qua Mock trong Jest.
Mock có nghĩa là làm giả, làm nhái. Để làm quen từ ngữ tôi sẽ dùng từ mock với nghĩa trên.Jest có thể mock các modules, functions, component thành một “mocked version” rồi sử dụng các Mock này mà không phải đụng đến “real version” của nó.

Mock API Reference

jest.mock(moduleName)

-> should always return a mocked version of the specified module from require() (e.g. that it should never return the real module).

jest.dontMock(moduleName)

-> should never return a mocked version of the specified module from require() (e.g. that it should always return the real module)

Getting Started:

Tôi sẽ thử test cái source code trong bài trước là Reusable Component.

Setup

Trước mắt, cần setup source code như sau:

  1. Copy toàn bộ thư mục “material-ui-component” thành thư mục “testing”.
  2. Tách source code trong file main.jsx thành các Component như member.jsx.
  3. Tạo thư mục test để chứa các file test
  4. Tạo npm test command:

    Trong thư mục “testing”:

    npm init

    để tạo file package.json (File này sẽ có vai trò chứa những config cho Jest sau này), khi init nhớ nhập ‘jest’ cho line test command: jest

  5. JSX transformer: vì source code “material-ui-component” viết theo syntax JSX nên khi test ta cũng cần chuyển đổi JSX thành JS, trong file package.json ta thêm đoạn code sau:

    "jest": {
    "scriptPreprocessor": "preprocessor.js"
    }

Và tạo file preprocessor.js

var ReactTools = require('react-tools');
module.exports = {
  process: function(src) {
    return ReactTools.transform(src, {harmony: true});
  }
};

Bạn cũng cần install react-tools.
6. Remove Mock

Trong file package.json, ta thêm như sau để chỉ định việc không mock tất cả các node module


"jest": {
"scriptPreprocessor": "preprocessor.js",
"unmockedModulePathPatterns": ["node_modules/react"]
},

Tạo Unit Test cho Component member

__test__/member-test.js

jest.dontMock('../member.jsx');

var React = require('react/addons');
var Member= require('../member.jsx');
var TestUtils = React.addons.TestUtils;

describe('Member component', function() {

  it('set name for member', function() {

    // Render a member with name is Foo
    var memberFoo = TestUtils.renderIntoDocument(
      <Member name="Foo" />
    );
    // Verify that it's name is Foo
    var memberName = TestUtils.findRenderedComponentWithType(memberFoo, Member);
    expect(memberName.getDOMNode().textContent).toEqual('Foo');

  });

});

Problem:

Với version nodejs tôi đang dùng là v0.11.14 thì khi chạy sample mẫu từ Jest thì OK, tuy nhiên khi thử test với Component member.jsx thì bị lỗi là: “Error: Worker process exited before responding! exit code: null, exit signal: SIGSEGV stderr:”. Và dường như đây là 1 known issue

https://github.com/facebook/jest/issues/243

Và tôi cũng đã thử với version mới nhất của nodejs tại thời điểm này là là v0.12.5 nhưng cũng không khá hơn.

Note: tôi đang dùng nvm để quản lý version của nodejs. Nhờ nó tôi dễ dàng upgrade cũng như downgrade version của nodejs.

Cũng theo một comment trên https://github.com/facebook/jest/issues/243 , tôi thử dùng nodejs 0.10 và thử retest thì OK:

#Chuyển sang node v0.10
nvm use 0.10

#Rebuild all dependencies
npm rebuild

#Run test
npm test

Bạn có thể thử thay đổi ‘Foo’ trong .toEqual('Foo') thành ‘Fool’ và thử chạy lại câu lệnh npm test, nó sẽ báo kết quả Fail.

Source code demo cho ví dụ trên:

https://github.com/phungnc/reactjs-tut-demo/tree/feature/jest/jest

Tôi cũng thử viết thêm một vài test cho member component vào file __test__/member-test.js:

https://github.com/phungnc/reactjs-tut-demo/blob/feature/jest_testing_more_complex/jest/member.jsx

Reactjs - Bài 16 - Testing - sử dụng TestUtils

Goal

Hôm nay tôi sẽ viết về Reactjs Addons TestUtils trong Reactjs.

TestUtils

http://facebook.github.io/react/docs/test-utils.html

TestUtils là một addon của Reactjs, cung cấp các method để test các React components.

Các mục như Simulate, renderIntoDocument đã viết khá rõ trong link trên. Ở đây tôi chỉ cố gắng demo 2 dạng testing là Assert và Component Testing.

Tôi có dùng lại Source Code bài Addon ReactLink để demo, lý do là nó đã có include sẵn React Addons :).

Bạn hãy chạy đoạn code dưới và xem kết quả nhé.

<!DOCTYPE html>
<html>
<head>
  <meta http-equiv='Content-type' content='text/html; charset=utf-8'>
  <title>Basic React TestUtils Testing</title>
</head>
<body>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.js"></script>  
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/JSXTransformer.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react-with-addons.js"></script>
  <script type="text/jsx">
    var Hello = React.createClass({
      render: function() {
        return (
          <div>{this.props.name}</div>
        );
      }
    });
    var OnlyMe = React.createClass({
      render: function() {
        return (
          <div>{this.props.name}</div>
        );
      }
    });
    var Link = React.createClass({
      mixins: [React.addons.LinkedStateMixin],
      getInitialState: function() {
        return {message: 'Hello!'};
      },
      handleChange: function(newValue) {
        this.setState({message: newValue});
      },
      render: function() {
       var valueLink = {
          value: this.state.message,
          requestChange: this.handleChange
        };
        var valueLinkk =this.linkState('message');
        return (
          <div>
            <Hello name="FOO "/>
            <Hello name="BAR"/>
            <OnlyMe name="OnlyMe"/>
            <p className="title">{valueLink.value}</p>
            <span className="title">World!</span>
            <span className="sub-title">My Name is C.P</span>
            <input type="text" valueLink={valueLink} />
          </div>
        );
      }
    });

    var LinkComponent = React.render(<Link />, document.body);

    // TestUtils
    var TestUtils = React.addons.TestUtils;
    // Assert
    document.write("=== Assert Testing Type ===");
    //
    document.write("<br/>");
    document.write("<br/>");
    document.write("<b>1.isElement: check if Link is an Element</b>");
    document.write("<br/>");
    document.write(">>>Result is: ",TestUtils.isElement(<Link />));
    //
    document.write("<br/>");
    document.write("<br/>");
    document.write("<b>2.isElementOfType : check if Link is is a ReactElement whose type is of a React componentClass</b>");
    document.write("<br/>");
    document.write(">>>Result is: ",TestUtils.isElementOfType(<Link />, Link));
    //
    document.write("<br/>");
    document.write("<br/>");
    document.write("<b>3.isDOMComponent: check if Link instance is a DOM component (such as a <div> or <span>)</b>");
    document.write("<br/>");
    document.write(">>>Result is: ",TestUtils.isDOMComponent(LinkComponent));
    //
    document.write("<br/>");
    document.write("<br/>");
    document.write("<b>4.isCompositeComponent : check if Link instance is a composite component (created with React.createClass())</b>");
    document.write("<br/>");
    document.write(">>>Result is: ",TestUtils.isCompositeComponent(LinkComponent));
    //
    document.write("<br/>");
    document.write("<br/>");
    document.write("<b>5.isCompositeComponentWithType : check if Link instance is a composite component (created with React.createClass()) whose type is of a React componentClass.</b>");
    document.write("<br/>");
    document.write(">>>Result is: ",TestUtils.isCompositeComponentWithType(LinkComponent, Link));
    //
    document.write("<br/>");
    document.write("<br/>");
    document.write("=== Component Testing Type ===");
    document.write("<br/>");
    document.write("<b>1.array findAllInRenderedTree(ReactComponent tree, function test)</b>");
    document.write("<br/>");
    document.write("Traverse all components in tree and accumulate all components where test(component) is true. This is not that useful on its own, but it's used as a primitive for other test utils.");
    document.write("<br/>");
    document.write(">>> Result is: ",
      TestUtils.findAllInRenderedTree(LinkComponent, 
        function(component) { return component.tagName === "P" }
      ).map(function(component){ return component.getDOMNode().textContent })
    );
    //
    document.write("<br/>");
    document.write("<br/>");
    document.write("<b>2.array scryRenderedDOMComponentsWithClass(ReactComponent tree, string className)</b>");
    document.write("<br/>");
    document.write("Finds all instances of components in the rendered tree that are DOM components with the class name matching className");
    document.write("<br/>");
    document.write(">>> Result is: ",TestUtils.scryRenderedDOMComponentsWithClass(LinkComponent, 
      'title'
      ).map(function(component){ return component.getDOMNode().textContent }));
    //
    document.write("<br/>");
    document.write("<br/>");
    document.write("<b>3.ReactComponent findRenderedDOMComponentWithClass(ReactComponent tree, string className)</b>");
    document.write("<br/>");
    document.write("Like scryRenderedDOMComponentsWithClass() but expects there to be one result, and returns that one result, or throws exception if there is any other number of matches besides one.");
    document.write("<br/>");
    document.write(">>> Result is: ",TestUtils.findRenderedDOMComponentWithClass(LinkComponent, 
      'sub-title'
      ).getDOMNode().textContent);
    //
    document.write("<br/>");
    document.write("<br/>");
    document.write("<b>4.array scryRenderedDOMComponentsWithTag(ReactComponent tree, string tagName)</b>");
    document.write("<br/>");
    document.write("Finds all instances of components in the rendered tree that are DOM components with the tag name matching tagName");
    document.write("<br/>");
    document.write(">>> Result is: ",TestUtils.scryRenderedDOMComponentsWithTag(LinkComponent, 
      'span'
      ).map(function(component){ return component.getDOMNode().textContent }));
    //
    document.write("<br/>");
    document.write("<br/>");
    document.write("<b>5.ReactComponent findRenderedDOMComponentWithTag(ReactComponent tree, string tagName)</b>");
    document.write("<br/>");
    document.write("Like scryRenderedDOMComponentsWithTag() but expects there to be one result, and returns that one result, or throws exception if there is any other number of matches besides one.");
    document.write("<br/>");
    document.write(">>> Result is: ",TestUtils.findRenderedDOMComponentWithTag(LinkComponent, 
      'p'
      ).getDOMNode().textContent);
    //
    document.write("<br/>");
    document.write("<br/>");
    document.write("<b>6.array scryRenderedComponentsWithType(ReactComponent tree, function componentClass)</b>");
    document.write("<br/>");
    document.write("Finds all instances of components with type equal to componentClass");
    document.write("<br/>");
    document.write(">>> Result is: ",TestUtils.scryRenderedComponentsWithType(LinkComponent, 
      Hello
      ).map(function(component){ return component.getDOMNode().textContent }));
    //
    document.write("<br/>");
    document.write("<br/>");
    document.write("<b>7.ReactComponent findRenderedComponentWithType(ReactComponent tree, function componentClass)</b>");
    document.write("<br/>");
    document.write("Same as scryRenderedComponentsWithType() but expects there to be one result and returns that one result, or throws exception if there is any other number of matches besides one.");
    document.write("<br/>");
    document.write(">>> Result is: ",TestUtils.findRenderedComponentWithType(LinkComponent, 
      OnlyMe
      ).getDOMNode().textContent);
    //
  </script>
</body>
</html>

Reactjs - Bài 15 - Tái sử dụng Component Material UI trong Reactjs

Goal

Giới thiệu việc sử dụng Component có sẵn cho Reactjs.

Reusalbe Components

“When designing interfaces, break down the common design elements (buttons, form fields, layout components, etc.) into reusable components with well-defined interfaces. That way, the next time you need to build some UI, you can write much less code. This means faster development time, fewer bugs, and fewer bytes down the wire.”
facebook
Lược dịch:
Khi design giao diện, hãy break down những design elements như button, form fields, layout components, v.v… thành các components có thể tái sử dụng và well-defined interfaces (I/F của components). Với cách này, bạn không cần tốn nhiều thời gian cho lần tạo giao diện tiếp theo. Nghĩa là thời gian phát triển nhanh hơn, ít bug hơn, và giảm bớt wire.

Tìm React Component ở đâu?

Bạn có thể tìm kiếm các React Component ở site bên dưới này.

ReactComponent

Đây là site tập hợp các ReactComponent.
http://react-components.com/

React Rocks

http://react.rocks/

Github wiki

https://github.com/facebook/react/wiki/Complementary-Tools#ui-components

Thử sử dụng một ReactComponent

Hiện tại trên trang http://react-components.com/, tôi sẽ thử sử dụng một Component phổ biến nhất là material-ui để tạo giao diện cho demo Avatar.
Source code:
https://github.com/phungnc/reactjs-tut-demo/tree/feature/material-ui-component/material-ui-component
Trong source code mới này:
1. Vì material-ui đã có Componet Avatar, nên tôi sẽ đổi trên các Component trong demo trước từ Avatar thành Member.
2. Các file delete.jsx, toggle-star.jsx là các Icon Component tôi download từ https://github.com/callemall/material-ui/tree/master/src/svg-icons
3. Tôi vẫn dùng Browserify cho nên bạn vẫn phải dùng lệnh browserify -t reactify avatar.jsx > bundle.js để bundle up file bundle.js.
4. Khi thực hiện browserify nếu xảy ra lỗi thiếu module nào thì hãy install module đó.

Reactjs - Bài 14 - React router

Reactjs - router

[Update ngày 21/06/2016]
Nội dung bài viết bên dưới cách đây 1 năm, hiện tại react-router đã update rất nhiều, nên tôi khuyến khích các bạn hãy cập nhật nội dung về  react-router tại
https://github.com/reactjs/react-router-tutorial/tree/master/lessons

Tuy nhiên nội dung bên dưới vẫn có giá trị tham khảo, đặc biệt là khi có và không có react-router.

Goal

Giới thiệu react-router.

Giới thiệu

Nếu các bạn đã trải qua các bài viết từ bài đầu tiên đến giờ có lẽ bạn cũng đã nhận ra rằng React.js chỉ là thư viện để tạo các Component, nó không có Router.
react-router được dùng để giúp việc dẫn hướng UI đồng bộ với URL.
Để giúp các bạn hiểu rõ hơn react-router, chúng ta sẽ thử implement 2 tình huống như sau
- Without react-router
- With react-router
- Server rendering react-router
Cách này tôi dựa trên ý tưởng theo tài liệu của React Router nhưng được trình bày lại theo luồng source code demo từ những bài trước, cho nên nếu việc tham khảo trực tiếp thông qua tài liệu của React Router khiến bạn dễ hiểu hơn thì bạn nên tham khảo nó.
Ý tưởng demo như sau:
Chúng ta sẽ sử dụng source code Avatar list, và thêm trang detail cho từng Avatar. Có nghĩa là từ list Avatar, click vào link của từng Avatar thì nó chuyển sang trang Avatar detail của Avatar đó.

Without react-router

Thêm đoạn code xử lý load nội dung Avatar list hay Avatar Item theo route:
var App = React.createClass({
  render(){
    var Child;
    switch (this.props.route) {
      case 'avatars': Child = Avatars; break;
      case 'avatar': Child = Avatar; break;
      default: Child = Avatars;
    }
    return(
      <div>
        <Child />
      </div>
    )
  }
});

function render(){
  var route = window.location.hash.substr(1);
  React.render(<App route = {route}/>, document.body);
}
window.addEventListener('hashchange', render);
render();
App sẽ render các nội dung khác nhau tùy theo giá trị this.props.route. Nếu chỉ 2 trường hợp như trên thì còn khá là đơn giản nhưng trong trường hợp nhiều trang hơn, nhiều cấp cho route hơn thì vấn đề sẽ nhanh chóng trở nên phức tạp.
Source code minh họa:
https://github.com/phungnc/reactjs-tut-demo/tree/feature/route_without_react_router/route-without-react-router
Lưu ý: Vì tôi đã giới thiệu việc dùng browserify trong bài trước, nên từ bây giờ tôi sẽ dùng browserify cho việc require các module cần thiết. Cho bạn cần dùng câu lệnh sau để build file bundle.js
browserify -t reactify avatar.jsx > bundle.js

With react-router

Phần xử lý route sẽ trở thành:
var routes  = (
  <Route handler={App}>
    <DefaultRoute handler={Avatars} />
    <Route name="avatars" handler={Avatars} />
    <Route name="avatar" handler={Avatar} />
  </Route>
);

var App = React.createClass({
  render(){
    return(
      <div>
        <RouteHandler/>
      </div>
    )
  }
});
//Finally we need to listen to the url and render the application.
Router.run(routes, Router.HashLocation, function(Root) {
  React.render(<Root/>, document.body);
});
Nhớ là bạn cần ‘require’ react-router Router = require('react-router'); ở đầu file.
Source code:
https://github.com/phungnc/reactjs-tut-demo/tree/feature/route_with_react_router/route-with-react-router
Trong phần này, bạn cần đọc thêm các nội dung ở link này:
https://github.com/reactjs/react-router-tutorial/tree/master/lessons

Server-side rendering and react-router

React-router chẳng những có thể dùng ở phía client mà còn dùng được ngay cả khi render React App ở phía Server.
Nếu bạn hiểu sơ qua nguyên lý hoạt động React-router ở trên và đã đọc qua bài viết Server-side rendering thì việc modify lại code dùng React-router cho phía server sẽ không có vấn đề gì. Chỉ cần chú ý một chút là ở Sever chúng ta bundle up file browser.js
browserify -t reactify browser.js > bundle.js
Source code:
https://github.com/phungnc/reactjs-tut-demo/tree/feature/server_side_react_router/server-rendering-react-router

Reactjs - Bài 13 - server-side renderring

Goal

Trong các bài trước chúng ta chỉ sử dụng Reactjs ở phía Client, chúng ta có thể render HTML hoàn toàn ở phía Client. Trong bài này tôi sẽ giới thiệu cách ứng dụng Reactjs trong việc render HTML ở phía Server.

Client-side rendering

Trong các ví dụ trước, chúng ta render View hoàn toàn ở phía client nhờ vào method React.render(). Hãy xem hình minh họa bên dưới

Client-side-rendering

Để giúp các bạn hiểu rõ quá trình chuyển việc render từ phía client sang phía server cho nên tôi sẽ liệt kê các bộ phận cũng như từng bước của việc render ở phía Client như sau:

  1. File index.html: layout của view, chứa DOM để gắn ReactElement vào (trong các bài trước, tôi gắn React Element vào thẻ ).
  2. Include các file react.js, JSXTransformer.js,(ngoài ra còn có các file khác như superagent.js hay React-addon…)
  3. React.createClass() định nghĩa Virtual DOM
  4. React.createElement() tạo Virtual DOM
  5. React.render() render Virtual DOM vào thẻ
  6. Có một được một trang index.html hoàn chỉnh.

Source code mẫu cho Client-side rendering:

<!DOCTYPE html>
<html>
<head>
  <meta http-equiv='Content-type' content='text/html; charset=utf-8'>
  <title>Basic Example Props</title>
</head>
<body>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.js"></script>  
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/JSXTransformer.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/superagent/0.15.7/superagent.min.js"></script>
  <script type="text/jsx">

    var Avatar = React.createClass({
      propTypes: {
        id: React.PropTypes.string.isRequired,
        name: React.PropTypes.string.isRequired,
        width: React.PropTypes.number.isRequired,
        height: React.PropTypes.number.isRequired,
        initialLike: React.PropTypes.bool.isRequired,
        // Thêm Interface onDelete()
        onDelete: React.PropTypes.func.isRequired,
      },
      getInitialState() {
        return {
          liked: this.props.initialLike
        };
      },
      onClick() {
        this.setState({liked: !this.state.liked});
      },
      // Ủy quyền cho Component cha xử lý.
      _onDelete() {
        this.props.onDelete(this.props.id);
      },
      render() {
        var textLike = this.state.liked ? 'like' : 'haven\'t liked';
        return (
          <li key={this.props.id}>
            <span>{this.props.id}</span>
            <img  src={this.props.src} width={this.props.width} height={this.props.height} alt="alt" />
            <span>{this.props.name}</span>
            <button onClick={this.onClick}>{textLike}</button>
            <button onClick={this._onDelete}>Delete</button>
          </li>
        );
    }
    });
    var request = window.superagent;
    var Avatars = React.createClass({

      getInitialState() {
        return {
          avatars: [
            {id: 1, name: "Avatar 1", height: 100, width: 100, like: false, src: "http://canime.files.wordpress.com/2010/05/mask-dtb.jpg"},
            {id: 2, name: "Avatar 2", height: 100, width: 100, like: true, src: "http://z4.ifrm.com/30544/116/0/a3359905/avatar-3359905.jpg"},
            {id: 3, name: "Avatar 3", height: 100, width: 100, like: false, src: "http://www.dodaj.rs/f/O/IM/OxPONIh/134.jpg"}
          ]
        }
      },
      // Thêm method deleteItem() set lại State (chứa các Component con) cho Component cha này
      deleteItem(id) {
        this.setState({
          avatars: this.state.avatars.filter(function(avatar){
            return avatar.id !== id;
          })
        });
      },
      componentDidMount: function() {
        var self = this;
        request.get('http://localhost:3000/api/employees', function(res) {
        console.log(res);
          self.setState({avatars: res.body});
        });
      },

      render() {  
        var avatars = this.state.avatars.map(function(avatar){
        // use below solution
        // map(function(){},this) 
        // or map(function(){}.bind(this)) 
        // or var that = this; onDelete = {that.deleteUser}
        // to pass this value to map function.
        // bind onDelete (event) to deleteUser.
          return <Avatar onDelete={this.deleteItem} id={avatar.id} name={avatar.name} width={avatar.width} height={avatar.height} src={avatar.src} initialLike={avatar.like} />;
        }, this);
        return (
          <ul>
            {avatars}
          </ul>
        );
      }
    });

    var AvatarsComponent = React.render(<Avatars />, document.body);
  </script>
</body>
</html>

Server-side rendering

Server-side rendering thực chất là việc tạo ra và trả về browser 1 trang index.html hoàn chỉnh.

Server-side-rendering

Để thực hiện điều đó chúng ta cũng cần các bước trên, nhưng sẽ có một số biến đổi tương đượng như sau:

  1. File index.html
    Chúng ta sẽ dùng ngôn ngữ template để gắn ReactElement vào vị trí DOM cần thiết (vì ở server thì chưa có Real DOM để chúng ta get và gắn vào). Ở đây tôi dùng ngôn ngữ template là jade, cho nên chúng ta cũng sẽ có file index.jade. File này cũng chứa layout của View

  2. Include các file react.js, JSXTransformer.js
    Tôi dùng nodejs, nên tôi sẽ dùng method require() để include các package tương ứng.

  3. React.createClass() không thay đổi

  4. React.createElement() không thay đổi
  5. React.render() ở phía client có 2 ý nghĩa là render Virtual DOM thành HTML (hay real DOM) và gắn vào thẻ . Điều đó, phía server sẽ thực hiện một cách tương tự như sau:

    • Dùng React.renderToString. (Vui lòng tham khảo method này để hiểu thêm) để trả về một HTML string (chứa nội dung HTML mà ta muốn render).
    • Truyền HTML string đó vào thông qua biến !{markup}. Jade engine sẽ render ra index.html hoàn chỉnh
  6. Trả về cho Browser trang index.html vừa tạo ra ở trên.

Server-side-rendering-static

Source code:

https://github.com/phungnc/reactjs-tut-demo/tree/feature/server_side_rendering_static/server-rendering-static

Lưu ý: bạn hãy tự cài những module còn thiếu nhé, vì có những module tôi dùng như là global module nên khi viết source này tôi không có cài lại.

Sau khi bạn clone về, bạn start cái server bằng lệnh:

node server

và vào link:

http://localhost:5000

Boom! bạn đã có được một View giống như khi render ở phía client.
Tuy nhiên khi bạn thử click và button như like hay là delete thì chẳng có điều gì xảy ra. Tại sao vậy?

Tại vì thực ra server chỉ trả về cho Browser 1 file HTML tĩnh có “gắn” phần HTML vừa được React tạo ra vào thẻ <body> thôi chứ không có phần source code JS đi kèm theo. Điều này khác với việc render ở Client là bản thân souce code JS đã include sẵn. Cho nên bây giờ ta tiếp tục “trả” về cho client source code JS đó.

Để thực hiện điều này, tôi dùng browserify để “write code that uses require in the same way that you would use it in Node”. Cho nên chúng ta sẽ tận dụng cách viết ở phía Node Server cho phía Client. Browserify sẽ bundle up (tập hợp) tất cả các module cần thiết định nghĩa trong file mà chúng ta tạm gọi là browser.js thành file bundle.js và gửi cho Client.

Hình mình họa:

server-side rendering dynamic

Bây giờ, chúng ta sẽ tạo một file là browser.js để thông qua nó Browserify tạo file bundle.js:

'use strict';
var React   = require('react'),
    Avatar = require('./avatar');
React.render(<Avatar />, document.body);

Vì ở đây chúng ta dùng JSX syntax cho nên chúng ta cũng cần reactify để giúp Browserify transform JSX.

Bạn có thể thử command như bên dưới để thử bundle up file bundle.js từ file browser.js ở trên:

browserify -t reactify browser.js > bundle.js

Khi bạn mở file bundle.js, bạn sẽ thấy file bundle.js này đã gộp Reactjs và file avatar.js lại thành một.

OK, chúng ta đã có bundle.js giờ chỉ cần gửi file này về cho browser bằng cách thêm đoạn code sau vào bên dưới body trong file index.jade

    |     
    script(src='./bundle.js')

Và thêm dòng code sau app.use(Express.static(__dirname + '/')); vào bên dưới dòng app.set('view engine', 'Jade'); ở file server.js để báo cho express.static biết là nơi chứa các file static.

Bây giờ bạn reload lại page, thử click các button thử xem, It’s work!

Source code:

https://github.com/phungnc/reactjs-tut-demo/tree/feature/server_side_rendering_dynamic/server-rendering

Lưu ý: bạn hãy tự cài những module còn thiếu nhé, vì có những module tôi dùng như là global module nên khi viết source này tôi không có install vào.

Việc sử dụng Javascript ở cả server và client người ta gọi là Isomorphic JavaScript (Javascript đồng hình)

!= Nodejs

  1. react-php-v8js
    https://github.com/reactjs/react-php-v8js

  2. react-rails
    https://github.com/reactjs/react-rails

  3. React.NET
    https://github.com/reactjs/React.NET

  4. react-python
    https://github.com/reactjs/react-python

Reactjs - Bài 12 - Addon

Goal

Giới thiệu addon trong Reactjs

1. Addon

React.addons là nơi ta đóng gói những utils dành cho việc xây dựng React apps.

Cách sử dụng:

Thêm

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react-with-addons.js"></script>

vào phần head của page.

Animation Addon.

React cung cấp addon component ReactTransitionGroup như là low-level API for animation, và một ReactCSSTransitionGroup để dễ dành implement những CSS animation và transition đơn giản.

Ví dụ

Lấy ví dụ trong bài ‘props and state’, ở ví dụ đó, ta có 1 list các Avatar. Khi click vào nút delete nào thì Avatar đó sẽ bị remove.

Bây giờ ta thử dùng Reactjs Animation Addon để tạo hiệu ứng khi ta remove một Avatar.

<!DOCTYPE html>
<html>
<head>
  <meta http-equiv='Content-type' content='text/html; charset=utf-8'>
  <title>Basic Example Props</title>
  <style>
    .example-leave {
      opacity: 1;
      transition: opacity .5s ease-in;
    }

    .example-leave.example-leave-active {
      opacity: 0.01;
    }
  </style>
</head>
<body>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.js"></script>  
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/JSXTransformer.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react-with-addons.js"></script>
  <script type="text/jsx">

    var Avatar = React.createClass({
      propTypes: {
        id: React.PropTypes.number.isRequired,
        name: React.PropTypes.string.isRequired,
        width: React.PropTypes.number.isRequired,
        height: React.PropTypes.number.isRequired,
        initialLike: React.PropTypes.bool.isRequired,
        // Thêm Interface onDelete()
        onDelete: React.PropTypes.func.isRequired,
      },
      getInitialState() {
        return {
          liked: this.props.initialLike
        };
      },
      onClick() {
        this.setState({liked: !this.state.liked});
      },
      // Ủy quyền cho Component cha xử lý.
      _onDelete() {
        this.props.onDelete(this.props.id);
      },
      render() {
        var textLike = this.state.liked ? 'like' : 'haven\'t liked';
        return (
          <li key={this.props.id}>
            <span>{this.props.id}</span>
            <img  src={this.props.src} width={this.props.width} height={this.props.height} alt="alt" />
            <span>{this.props.name}</span>
            <button onClick={this.onClick}>{textLike}</button>
            <button onClick={this._onDelete}>Delete</button>
          </li>
        );
    }
    });
    var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
    var Avatars = React.createClass({

      getInitialState() {
        return {
          avatars: [
            {id: 1, name: "Avatar 1", height: 100, width: 100, initialLike: false, src: "http://canime.files.wordpress.com/2010/05/mask-dtb.jpg"},
            {id: 2, name: "Avatar 2", height: 100, width: 100, initialLike: true, src: "http://z4.ifrm.com/30544/116/0/a3359905/avatar-3359905.jpg"},
            {id: 3, name: "Avatar 3", height: 100, width: 100, initialLike: false, src: "http://www.dodaj.rs/f/O/IM/OxPONIh/134.jpg"}
          ]
        }
      },
      // Thêm method deleteItem() set lại State (chứa các Component con) cho Component cha này
      deleteItem(id) {
        this.setState({
          avatars: this.state.avatars.filter(function(avatar){
            return avatar.id !== id;
          })
        });
      },

      render() {  
        var avatars = this.state.avatars.map(function(avatar){
        // use below solution
        // map(function(){},this) 
        // or map(function(){}.bind(this)) 
        // or var that = this; onDelete = {that.deleteUser}
        // to pass this value to map function.
        // bind onDelete (event) to deleteUser.
          return <Avatar onDelete={this.deleteItem} id={avatar.id} name={avatar.name} width={avatar.width} height={avatar.height} src={avatar.src} initialLike={avatar.initialLike} />;
        }, this);
        return (
          <ul>
            <ReactCSSTransitionGroup transitionName="example">
              {avatars}
            </ReactCSSTransitionGroup>
          </ul>
        );
      }
    });

    var AvatarsComponent = React.render(<Avatars />, document.body);
  </script>
</body>
</html>

Các điểm updated:
1. Thêm react-with-addons.js
2. Thêm <style></style>
3. Wrap {avatars} bởi ReactCSSTransitionGroup

Trong component này, khi remove 1 item ra khỏi ReactCSSTransitionGroup nó sẽ lấy example-leave CSS class và example-leave-active CSS class được add vào trong ‘tick’ tiếp theo. Cái convention này dựa vào transitionName prop (Trong ví dụ trên ta có chỉ định transitionName=”example”).

2. Two-way binding

Phần này chủ yếu là tôi lược dịch lại từ trang gốc của Reactjs. Do đó nếu bạn cảm thấy nội dung trên trang gốc dễ hiểu hơn thì bạn nên bỏ qua bài này.

Chú ý: nếu bạn là Reactjs newbie thì bạn cũng không cần phải tìm hiểu về ReactLinkReactLink chưa cần thiết cho hầu hết các ứng dụng.

Trong Reactjs, data đi theo một luồng, theo mô hình input - output, ta gọi là one way binding.

Tuy nhiên, nhiều ứng dụng thường yêu cầu khi có lấy dữ liệu từ output và truyền ngược vào trong chương trình. Ví dụ như khi bạn muốn update React state từ input của user.

Trong React, để làm điều này, trước hết bạn phải listen sự kiện “change” ( onChange() ), rồi đọc giá trị của DOM và gọi setState() để thay đổi state của component. khi state của component thay đổi thì nó sẽ re-render lại UI của component.

Reactjs mang đến cho bạn addon ReactLink, sẽ luôn ràng buộc giữa giá trị của DOM và state của component. Hãy tưởng tượng, với ReactLink bạn đã có một sợi dây “kết nối” giữa data trên DOM và React state.

Chú ý: ReactLink thực chất cũng chỉ là một wrapper gộp onChange/setState(). Do đó về nguyên tắc nó không có thay đổi bản chất data flows trong React Application.

Phần ví dụ ReactLink: Before and After minh họa khá rõ về điều này, cho có lẽ tôi không cần trình bày thêm, chỉ lưu ý là ReactLink dùng mixins để thêm method linkState() từ LinkedStateMixin vào React component.

Trong bài trên, phần Under the Hood chủ yếu giải thích sâu hơn về LinkedStateMixin, nhưng tôi nghĩ trong giới hạn một newbie thì ta chỉ cần biết là React Addon có cung cấp LinkedStateMixin dành cho việt two-way binding. Còn nếu bạn muốn hiểu sâu hơn thì tôi nghĩ đọc source code react-with-addons.js phần LinkedStateMixin sẽ dễ hiểu hơn nhiều.

Ngoài 2 addon giới thiệu ở trên, Reactjs còn cung cấp các addon khác, bạn có thể tham khảo tại link bên dưới:

https://facebook.github.io/react/docs/addons.html