Flux vs Reflux - The differences

Flux ?

Trước tiên, nếu chưa hiểu và nắm rõ về Flux, một Kiến Trúc mới được công bố của Facebook, bạn có thể tham khảo 2 bài viết dưới đây.

Nói một cách đơn giản thì:

Trong bài thuyết trình Hacker Way: Rethinking Web App Development at Facebook này, các kỹ sư của Facebook cũng đã lần đầu tiên giới thiệu một giải pháp cho vấn đề sử dụng React một cách hiệu quả. Họ đề xuất một Kiến Trúc mới, với tên gọi Flux.

Như vậy, nếu đã, đang hoặc sẽ có ý định viết một application sử dụng React thì Flux mà một thứ mà bạn đáng để bỏ thời gian và công sức ra để tìm hiểu đấy.

Còn nếu vẫn chưa nắm rõ React là gì, và sử dụng nó ra sao thì bạn hoàn toàn có thể tìm đọc các bài viết về chủ đề React.js, vốn có khá nhiều trên Viblo.

Reflux ?

Cũng trong bài viết Flux - Under the hood của mình cách đây vài ngày, mình đã đề cập đến vấn đề Flux vốn không phải là một Framework, mà là một Kiến Trúc. Facebook trình bày những tư tưởng của kiến trúc đó, và ta có thể tự tạo ra một Framework hay Library theo kiến trúc Flux của riêng mình, và cũng đã giới thiệu qua về một số Libraries được tạo ra nhằm giúp các lập trình viên có thể tạo ra một application theo kiến trúc Flux một cách dễ dàng.

Và Reflux là một trong số đó.

Nhưng vấn đề là tại sao mình lại chọn giới thiệu Reflux, mà không phải các libraries khác, chẳng hạn như fluxible của Yahoo ?

Reflux được viết bởi Mikael Brassman, với một số điểm khác biệt so với Flux truyền thống. Hiện nay Reflux là thư viện implement Kiến Trúc Flux phổ biến thứ 2 trên Github, chỉ sau Flux của Facebook, với khoảng 25.000 lượt download hàng tháng, và hơn 2700 stars, vượt xa các libraries khác.

Các bạn có thể xem chi tiết hơn về việc so sánh giữa các Libraries Flux tại Which Flux implementation should I use?.

Còn trong bài viết này, ta sẽ cùng tập trung vào tìm hiểu về Reflux, nó giống và khác với Flux ở điểm nào, nó đem lại những tính năng mới gì, và giúp chúng ta viết code một cách ngắn gọn ra sao.

Flux vs Reflux

Trước tiên, phải khẳng định rằng Reflux được sinh ra từ Flux, và mục đích của nó cũng là để sử dụng React một cách hiệu quả. Chính vì thế, Reflux mang trong mình những tư tưởng cốt lỗi của Flux, cụ thể như sau.

  • Unidirectional Data Flow: Nếu đã từng đọc bài viết Flux - Under the hood thì chắc bạn vẫn còn nhớ mình đã từng nói rất rõ về tính chất cốt lõi này của Flux, giúp nó tạo nên sự khác biệt với kiến trúc MVC. Và Reflux cũng tuân thủ theo Unidirectional Data Flow.
  • Có một số thành phần giống với Flux, gồm:
    • Views, chính là các React Component.
    • Actions, chức các function được gọi đến khi có tương tác từ view.
    • Stores, nơi quản lý và thực hiện các thao tác thay đổi dữ liệu.

Tuy nhiên, Reflux cũng mang nhiều điểm khác biệt so với Flux, những thứ góp phần tạo nên nét riêng biệt và thành công của nó.

  • Không còn thành phần Dispatcher trong Reflux nữa. Nếu như Dispatcher được mô tả như là thành phần quan trọng trong Flux giúp đảm bảo Unidirectional Data Flow, thì ở Reflux nó thậm chí còn bị loại bỏ.
  • Reflux đi theo hướng phát triển giúp Stores lắng nghe trực tiếp từ Actions. Chúng ta không còn phải mất công viết những đoạn code dài dòng switch với case trong file Stores nữa, Reflux sẽ lo hết các công việc nhàm chán và dài dòng đó cho chúng ta. Ngoài ra, Stores không chỉ có thể lắng nghe Actions mà nó còn có thể lắng nghe trực tiếp từ Stores khác.
  • Support rất nhiều công cụ để ta có thể tạo Actions, viết các hàm Listerner hay sử dụng Callback một cách dễ dàng và ngắn gọn.

Như vậy, mặc dù cùng là Unidirectional Data Flow, nhưng Flux và Reflux cũng có chút khác biệt, ta có thể thấy thông qua so sánh dưới đây:

Flux
╔═════════╗       ╔════════════╗       ╔════════╗       ╔═════════════════╗
║ Actions ║──────>║ Dispatcher ║──────>║ Stores ║──────>║ View Components ║
╚═════════╝       ╚════════════╝       ╚════════╝       ╚═════════════════╝
     ^                                                          │
     └──────────────────────────────────────────────────────────┘

Reflux
╔═════════╗       ╔════════╗       ╔═════════════════╗
║ Actions ║──────>║ Stores ║──────>║ View Components ║
╚═════════╝       ╚════════╝       ╚═════════════════╝
     ^                                      │
     └──────────────────────────────────────┘

Có một điểm thú vị của Reflux là cả Actions lẫn Stores trong Reflux đều là listenable (có thể lắng nghe). Stores thì lắng nghe Actions cũng như Stores khác, và Views thì lắng nghe Stores.

Dưới đây là datagram về Data Flow cũng như cách thức listen của các thành phần trong Reflux

reflux.jpeg

Khác với thư viện Flux của Facebook dường như chỉ cung cấp mỗi một module Dispatcher, và sử dụng dependency EventEmitter để trigger event thì Reflux to lớn hơn rất nhiều. Hãy cùng xem và cảm nhận sự khác biệt giữa Flux và Reflux thông qua ví dụ dưới đây nhé.

Application sử dụng Reflux

Trong bài viết Flux - Under the hood, để giải thích về cách tổ chức cũng như Data Flow của một Appication viết theo Kiến Trúc Flux, mình đã viết một ví dụ đơn giản là một trang với chức năng Vote Up và Vote Down. Lần này, mình sẽ viết lại trang đó bằng cách sử dụng thư viện Reflux, và sẽ phân tích xem lượng code đã được cải thiện như thế nào.

Đầu tiên là về cách tổ chức code:

Flux

flux.png

Reflux

reflux.png

Như ta có thể thấy thì ở app dùng Reflux ta không cần dùng đến Dispatcher nữa. Ngoài ra, do cũng không cần check các actionType ở trong Actions hay Stores nữa nên ta cũng không cần phải khai báo một đống Constants lằng nhằng nữa.

Giờ ta sẽ đi trực tiếp vào code, đầu tiên là Actions


// Flux
var AppDispatcher = require('../dispatcher/AppDispatcher.jsx');
var Constants = require('../constants/Constants.jsx');

var VoteActions = {
    voteUp: function() {
        AppDispatcher.dispatch({
            actionType: Constants.ACTION_VOTE_UP
        });
    },
    voteDown: function() {
        AppDispatcher.dispatch({
            actionType: Constants.ACTION_VOTE_DOWN
        });
    },
    voteReset: function() {
        AppDispatcher.dispatch({
            actionType: Constants.ACTION_VOTE_RESET
        });
    }
};

module.exports = VoteActions;

// Reflux
var Reflux = require('reflux');

var VoteActions = Reflux.createActions([
    "voteUp",
    "voteDown",
    "voteReset",
]);

module.exports = VoteActions;

Như ta thấy thì trong app dùng Library Flux, ta sử dụng Dispatcher để broadcast các Actions, và công việc này tỏ ra không được DRY cho lắm. Reflux cung cấp cho ta hàm createActions để ta tạo ra các action một cách cực kỳ dễ dàng và ngắn gọn. Ta cũng không cần phải truyền theo actionType làm gì nữa.

Bên phía Store, các action sẽ được lắng nghe như sau

// Flux
var AppDispatcher = require('../dispatcher/AppDispatcher.jsx');
var EventEmitter = require('events').EventEmitter;
var Constants = require('../constants/Constants.jsx');
var assign = require('object-assign');

var CHANGE_EVENT = 'change';

var _votes = {
    up: 0,
    down: 0
};

function _voteUp() {
    _votes.up++;
}

function _voteDown() {
    _votes.down++;
}

function _voteReset() {
    _votes = {
        up: 0,
        down: 0
    };
}

var VoteStore = assign({}, EventEmitter.prototype, {
    getVotes: function() {
        var up_rate = (_votes.up || _votes.down) ? _votes.up / (_votes.up + _votes.down) * 100 : 50;
        var down_rate = 100 - up_rate;
        _votes.up_rate = up_rate.toFixed(2);
        _votes.down_rate = down_rate.toFixed(2);
        return _votes;
    },

    emitChange: function() {
        this.emit(CHANGE_EVENT);
    },

    addChangeListener: function(callback) {
        this.on(CHANGE_EVENT, callback);
    }
});

AppDispatcher.register(function(action) {
    switch(action.actionType) {
        case Constants.ACTION_VOTE_UP:
            _voteUp();
            VoteStore.emitChange();
            break;
        case Constants.ACTION_VOTE_DOWN:
            _voteDown();
            VoteStore.emitChange();
            break;
        case Constants.ACTION_VOTE_RESET:
            _voteReset();
            VoteStore.emitChange();
            break;
        default:
        // no op
    }
});

module.exports = VoteStore;

// Reflux
var Reflux = require('reflux');
var VoteActions = require('../actions/VoteActions.jsx');

var _votes = {
    up: 0,
    down: 0
};

var VoteStore = Reflux.createStore({
    listenables: [VoteActions],
    onVoteUp: function() {
        _votes.up++;
        this.updateVotes();
    },
    onVoteDown: function() {
        _votes.down++;
        this.updateVotes();
    },
    onVoteReset: function() {
        _votes = {
            up: 0,
            down: 0
        };
        this.updateVotes();
    },
    updateVotes: function() {
        this.trigger(this.getVotes());
    },
    getInitialState: function() {
        return this.getVotes();
    },
    getVotes: function() {
        var up_rate = (_votes.up || _votes.down) ? _votes.up / (_votes.up + _votes.down) * 100 : 50;
        var down_rate = 100 - up_rate;
        _votes.up_rate = up_rate.toFixed(2);
        _votes.down_rate = down_rate.toFixed(2);
        return _votes;
    }
});

Ta có thể thấy VoteStore của app Reflux cũng ngắn gọn hơn rất nhiều. Ta không cần phải viết một đoạn code switch ... case dài dòng nữa, thay vào đó, Reflux đã lo hết cho chúng ta. Chẳng hạn như onVoteUp() sẽ được tự động lắng nghe VoteActions.voteUp().

Ngoài ra chúng ta cũng không cần phải register callback cho View ở đây nữa. Ở Reflux thì Store sẽ báo sự thay đổi cho View, hay Store khác đang lắng nghe nó thông qua hàm this.trigger().

Cuối cùng, ta đi đến View, tức React Component. Thực tế cũng không có nhiều thay đổi lắm, ngoài việc cách thông báo lắng nghe Store.

// Flux
getInitialState: function() {
	return VoteStore.getVotes();
},
componentDidMount: function() {
	VoteStore.addChangeListener(this._onChange);
},
_onChange: function() {
	this.setState(VoteStore.getVotes());
},
_onVoteUpClick: function() {
	VoteActions.voteUp();
},
_onVoteDownClick: function() {
	VoteActions.voteDown();
},
_onVoteResetClick: function() {
	VoteActions.voteReset();
},

// Reflux
mixins: [Reflux.connect(VoteStore, 'votes')],
_onVoteUpClick: function() {
	VoteActions.voteUp();
},
_onVoteDownClick: function() {
	VoteActions.voteDown();
},
_onVoteResetClick: function() {
	VoteActions.voteReset();
},

Reflux cung cấp cho ta các Mixins hữu dụng để ta có thể thực hiện việc thông báo View Component lắng nghe Store một cách dễ dàng. Chẳng hạn như trong ví dụ này mình sử dụng mixin connect. Nó sẽ thực hiện nhiệm vụ thiết lập rằng mỗi khi ở Store hàm trigger được gọi thì State của View Compenent sẽ được tự động thay đổi, và lưu vào property mang tên votes.

Ngoài ra thì cách gọi đến Action ở cả 2 bên là giống hệt nhau.

Đến đây, ta hãy cùng xem lại một lượt Data Flow của Reflux. Data Flow của app Flux có thể xem ở phần cuối của bài viết Flux - Under the hood.

// Đầu tiên, ở View, ta sử dụng mixin connect, thông báo rằng View sẽ lắng nghe VoteStore, và lưu trữ dữ liệu vào trong state.votes
mixins: [Reflux.connect(VoteStore, 'votes')]

// Khi View được mount xong, nó sẽ lấy dữ liệu initialState từ VoteStore, chứ bản thân trong View không cần hàm getInitialState.
getInitialState: function() {
	return this.getVotes();
},

// Khi vote up button được click, hàm _onVoteUpClick sẽ được gọi. Nó sẽ gọi đến hàm voteUp() trong VoteActions
_onVoteUpClick: function() {
    VoteActions.voteUp();
},

// Như đã biết, ở VoteActions, ta có register hàm voteUp để có thể gọi từ View, cũng như để Store lắng nghe
var VoteActions = Reflux.createActions([
    "voteUp",
    "voteDown",
    "voteReset",
]);

// Trong Store, ta thiết lập để VoteStore lắng nghe tất cả các action trong VoteAction thông qua thuộc tính listenables. Đương nhiên là Reflux còn cung cấp cho chúng ta nhiều cách để thực hiện công việc register khác nữa.
var VoteActions = require('../actions/VoteActions.jsx');

var VoteStore = Reflux.createStore({
    listenables: [VoteActions],
    // ...
});

// Với việc đăng ký qua listenables như vậy, khi VoteActions.voteUp() được gọi thì onVoteUp trong Store sẽ được chạy.
onVoteUp: function() {
	_votes.up++;
	this.updateVotes();
},

// Sau khi thay đổi giá trị _votes.up ta gọi đến hàm updateVotes() trong VoteStore. Hàm updateVotes() sẽ thực hiện nhiệm vụ báo cho View Component đăng lắng nghe VoteStore rằng nó đã thay đổi, và gửi giá trị đến với Component đấy.
updateVotes: function() {
	this.trigger(this.getVotes());
},

// Khi đó state của View Component sẽ bị đặt lại giá trị, và view sẽ thay đổi. Thứ giúp ta thực hiện công việc này chính là React.

// Vậy là ta đã kết thúc một chu trình đơn giản, đi từ action của người dùng, đến lúc update view.

Vậy là ta đã tìm hiểu qua về cấu trúc của một application đơn giản viết bằng Reflux. Reflux thật sự là một Library mạnh mẽ và thú vị, giúp bạn viết code sử dụng React một cách hiệu quả và có tổ chức.

Reflux cung cấp khá nhiều API cũng như mixin hữu ích, mà có lẽ bạn sẽ mất chút ít thời gian để làm quen và sử dụng chúng. Bạn có thể tìm hiểu thêm tại đây

References