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:

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
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
MemberStore
và
MemberApp.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()
});
}
Để 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
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 đó.
_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).
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
var AppDispatcher = require('../AppDispatcher');
var MemberConstants = require('../MemberConstants');
var EventEmitter = require('events').EventEmitter;
var _dispatchToken;
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!