Learn Flux from an extremely simple demo
This demo helps you learn Flux architecture. It is inspired by Andrew Ray's great article Flux For Stupid People.
Flux, invented by Facebook, is an architecture pattern for building client-side web applications.
It is similar to MVC architecture, but Flux's concept is much clearer than MVC's, and easier to learn.
Install the demo.
$ git clone [email protected]:ruanyf/extremely-simple-flux-demo.git $ cd extremely-simple-flux-demo && npm install $ npm start
Visit http://127.0.0.1:8080 with your browser.
You should see a button. Click it. That's all.
According to Flux, an application should be divided into four parts.
- Views: the UI layer
- Actions: messages sent from Views (e.g. mouseClick)
- Dispatcher: a place receiving actions, and calling callbacks
- Stores: a place managing the Application's state, and reminding Views to update
The key feature of the Flux architecture is "one way" (unidirectional) data flow.
- User interacts with Views
- Views propagate an Action triggered by user
- Dispatcher receives the Action and updates the Store
- Store emits a "change" event
- Views respond to the "change" event and update itself
Don't get it? Take it easy. I will give you the details soon.
Now let us follow the demo to learn Flux.
First of all, Flux is usually used with React. So your familiarity with React is assumed. If not, I prepared a React tutorial for you.
index.jsxhas only one component.
// index.jsx var React = require('react'); var ReactDOM = require('react-dom'); var MyButtonController = require('./components/MyButtonController');ReactDOM.render( , document.querySelector('#example') );
In the above code, you might notice our component's name isn't
MyButton, but
MyButtonController. Why?
Because I use React's controller view pattern here. A controller view component holds all states, then passes this data to its descendants.
MyButtonController's source code is simple.
// components/MyButtonController.jsx var React = require('react'); var ButtonActions = require('../actions/ButtonActions'); var MyButton = require('./MyButton');var MyButtonController = React.createClass({ createNewItem: function (event) { ButtonActions.addNewItem('new item'); },
render: function() { return ; } });
module.exports = MyButtonController;
In the above code,
MyButtonControllerputs its data into UI component
MyButton's properties.
MyButton's source code is even simpler.
// components/MyButton.jsx var React = require('react');var MyButton = function(props) { return
New Item; };module.exports = MyButton;
In the above code, you may find
MyButtonis a pure component (meaning stateless), which is really the biggest advantage of the controll view pattern.
Here, the logic of our application is when user clicks
MyButton, the
this.createNewItemmethod will be called. It sends an action to Dispatcher.
// components/MyButtonController.jsx// ... createNewItem: function (event) { ButtonActions.addNewItem('new item'); }
In the above code, calling the
createNewItemmethod will trigger an
addNewItemaction.
An action is an object which has some properties to carry data and an
actionTypeproperty to identify the action type.
ButtonActionsobject is the place we hold all actions.
// actions/ButtonActions.js var AppDispatcher = require('../dispatcher/AppDispatcher');var ButtonActions = { addNewItem: function (text) { AppDispatcher.dispatch({ actionType: 'ADD_NEW_ITEM', text: text }); }, };
In the above code, the
ButtonActions.addNewItemmethod will use
AppDispatcherto dispatch the
ADD_NEW_ITEMaction to the Stores.
The Dispatcher transfers the Actions to the Stores. It is essentially an event hub for your application's Views. There is only one global Dispatcher.
We use the Facebook official Dispatcher Library, and write a
AppDispatcher.jsas our application's dispatcher instance.
// dispatcher/AppDispatcher.js var Dispatcher = require('flux').Dispatcher; module.exports = new Dispatcher();
AppDispatcher.register()is used for registering a callback for actions.
// dispatcher/AppDispatcher.js var ListStore = require('../stores/ListStore');AppDispatcher.register(function (action) { switch(action.actionType) { case 'ADD_NEW_ITEM': ListStore.addNewItemHandler(action.text); ListStore.emitChange(); break; default: // no op } })
In the above code, when receiving the
ADD_NEW_ITEMaction, the callback will operate the
ListStore.
Please keep in mind, Dispatcher has no real intelligence on its own — it is a simple mechanism for distributing the actions to the stores.
The Stores contain the application state. Their role is somewhat similar to a model in a traditional MVC.
ListStoreto store data.
// stores/ListStore.js var ListStore = { items: [],getAll: function() { return this.items; },
addNewItemHandler: function (text) { this.items.push(text); },
emitChange: function () { this.emit('change'); } };
module.exports = ListStore;
In the above code,
ListStore.itemsis used for holding items,
ListStore.getAll()for getting all these items, and
ListStore.emitChange()for emitting an event to the Views.
The Stores should implement an event interface as well. Since after receiving an action from the Dispatcher, the Stores should emit a change event to tell the Views that a change to the data layer has occurred.
// stores/ListStore.js var EventEmitter = require('events').EventEmitter; var assign = require('object-assign');var ListStore = assign({}, EventEmitter.prototype, { items: [],
getAll: function () { return this.items; },
addNewItemHandler: function (text) { this.items.push(text); },
emitChange: function () { this.emit('change'); },
addChangeListener: function(callback) { this.on('change', callback); },
removeChangeListener: function(callback) { this.removeListener('change', callback); } });
In the above code,
ListStoreinheritances
EventEmitter.prototype, so you can use
ListStore.on()and
ListStore.emit().
After updated(
this.addNewItemHandler()), the Stores emit an event(
this.emitChange()) declaring that their state has changed, so the Views may query the new state and update themselves.
Now, we come back to the Views for implementing an callback for listening the Store's
changeevent.
// components/MyButtonController.jsx var React = require('react'); var ListStore = require('../stores/ListStore'); var ButtonActions = require('../actions/ButtonActions'); var MyButton = require('./MyButton');var MyButtonController = React.createClass({ getInitialState: function () { return { items: ListStore.getAll() }; },
componentDidMount: function() { ListStore.addChangeListener(this._onChange); },
componentWillUnmount: function() { ListStore.removeChangeListener(this._onChange); },
_onChange: function () { this.setState({ items: ListStore.getAll() }); },
createNewItem: function (event) { ButtonActions.addNewItem('new item'); },
render: function() { return ; } });
In the above code, you could see when
MyButtonControllerfinds out the Store's
changeevent occurred, it calls
this._onChangeto update the component's state, then trigger a re-render.
// components/MyButton.jsx var React = require('react');var MyButton = function(props) { var items = props.items; var itemHtml = items.map(function (listItem, i) { return
return
module.exports = MyButton;
MIT