├── .gitignore ├── README.md ├── actions └── ButtonActions.js ├── components ├── MyButton.jsx └── MyButtonController.jsx ├── dispatcher └── AppDispatcher.js ├── img ├── banner.png ├── dataflow.png ├── screenshot.png └── screenshot1.png ├── index.html ├── index.jsx ├── package.json ├── stores └── ListStore.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This demo helps you learn [Flux architecture](https://facebook.github.io/flux/). It is inspired by Andrew Ray's great article [Flux For Stupid People](http://blog.andrewray.me/flux-for-stupid-people/). 2 | 3 | ## What is Flux? 4 | 5 | Flux, invented by Facebook, is an architecture pattern for building client-side web applications. 6 | 7 | It is similar to MVC architecture, but Flux's concept is [much clearer](http://www.infoq.com/news/2014/05/facebook-mvc-flux) than MVC's, and easier to learn. 8 | 9 | ![](img/banner.png) 10 | 11 | ## How to Play? 12 | 13 | Install the demo. 14 | 15 | ```bash 16 | $ git clone git@github.com:ruanyf/extremely-simple-flux-demo.git 17 | $ cd extremely-simple-flux-demo && npm install 18 | $ npm start 19 | ``` 20 | 21 | Visit http://127.0.0.1:8080 with your browser. 22 | 23 | ![](img/screenshot1.png) 24 | 25 | You should see a button. Click it. That's all. 26 | 27 | ## Core Concepts 28 | 29 | According to Flux, an application should be divided into four parts. 30 | 31 | > - **Views**: the UI layer 32 | > - **Actions**: messages sent from Views (e.g. mouseClick) 33 | > - **Dispatcher**: a place receiving actions, and calling callbacks 34 | > - **Stores**: a place managing the Application's state, and reminding Views to update 35 | 36 | ![](img/dataflow.png) 37 | 38 | The key feature of the Flux architecture is "one way" (unidirectional) data flow. 39 | 40 | > 1. User interacts with Views 41 | > 1. Views propagate an Action triggered by user 42 | > 1. Dispatcher receives the Action and updates the Store 43 | > 1. Store emits a "change" event 44 | > 1. Views respond to the "change" event and update itself 45 | 46 | Don't get it? Take it easy. I will give you the details soon. 47 | 48 | ## Demo Details 49 | 50 | Now let us follow the demo to learn Flux. 51 | 52 | First of all, Flux is usually used with React. So your familiarity with React is assumed. If not, I prepared a [React tutorial](https://github.com/ruanyf/react-demos) for you. 53 | 54 | ### Views 55 | 56 | Our demo application's [`index.jsx`](https://github.com/ruanyf/extremely-simple-flux-demo/blob/master/index.jsx) has only one component. 57 | 58 | ```javascript 59 | // index.jsx 60 | var React = require('react'); 61 | var ReactDOM = require('react-dom'); 62 | var MyButtonController = require('./components/MyButtonController'); 63 | 64 | ReactDOM.render( 65 | , 66 | document.querySelector('#example') 67 | ); 68 | ``` 69 | 70 | In the above code, you might notice our component's name isn't `MyButton`, but `MyButtonController`. Why? 71 | 72 | Because I use React's [controller view pattern](http://blog.andrewray.me/the-reactjs-controller-view-pattern/) here. A controller view component holds all states, then passes this data to its descendants. `MyButtonController`'s [source code](https://github.com/ruanyf/extremely-simple-flux-demo/blob/master/components/MyButtonController.jsx) is simple. 73 | 74 | ```javascript 75 | // components/MyButtonController.jsx 76 | var React = require('react'); 77 | var ButtonActions = require('../actions/ButtonActions'); 78 | var MyButton = require('./MyButton'); 79 | 80 | var MyButtonController = React.createClass({ 81 | createNewItem: function (event) { 82 | ButtonActions.addNewItem('new item'); 83 | }, 84 | 85 | render: function() { 86 | return ; 89 | } 90 | }); 91 | 92 | module.exports = MyButtonController; 93 | ``` 94 | 95 | In the above code, `MyButtonController` puts its data into UI component `MyButton`'s properties. `MyButton`'s [source code](https://github.com/ruanyf/extremely-simple-flux-demo/blob/master/components/MyButton.jsx) is even simpler. 96 | 97 | ```javascript 98 | // components/MyButton.jsx 99 | var React = require('react'); 100 | 101 | var MyButton = function(props) { 102 | return
103 | 104 |
; 105 | }; 106 | 107 | module.exports = MyButton; 108 | ``` 109 | 110 | In the above code, you may find [`MyButton`](https://github.com/ruanyf/extremely-simple-flux-demo/blob/master/components/MyButton.jsx) is a pure component (meaning stateless), which is really the biggest advantage of the controll view pattern. 111 | 112 | Here, the logic of our application is when user clicks `MyButton`, the [`this.createNewItem`](https://github.com/ruanyf/extremely-simple-flux-demo/blob/master/components/MyButtonController.jsx#L27) method will be called. It sends an action to Dispatcher. 113 | 114 | ```javascript 115 | // components/MyButtonController.jsx 116 | 117 | // ... 118 | createNewItem: function (event) { 119 | ButtonActions.addNewItem('new item'); 120 | } 121 | ``` 122 | 123 | In the above code, calling the `createNewItem` method will trigger an `addNewItem` action. 124 | 125 | ### What is an Action? 126 | 127 | An action is an object which has some properties to carry data and an `actionType` property to identify the action type. 128 | 129 | In our demo, the [`ButtonActions`](https://github.com/ruanyf/extremely-simple-flux-demo/blob/master/actions/ButtonActions.js) object is the place we hold all actions. 130 | 131 | ```javascript 132 | // actions/ButtonActions.js 133 | var AppDispatcher = require('../dispatcher/AppDispatcher'); 134 | 135 | var ButtonActions = { 136 | addNewItem: function (text) { 137 | AppDispatcher.dispatch({ 138 | actionType: 'ADD_NEW_ITEM', 139 | text: text 140 | }); 141 | }, 142 | }; 143 | ``` 144 | 145 | In the above code, the `ButtonActions.addNewItem` method will use `AppDispatcher` to dispatch the `ADD_NEW_ITEM` action to the Stores. 146 | 147 | ### Dispatcher 148 | 149 | 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. 150 | 151 | We use the [Facebook official Dispatcher Library](https://github.com/facebook/flux), and write a [`AppDispatcher.js`](https://github.com/ruanyf/extremely-simple-flux-demo/blob/master/dispatcher/AppDispatcher.js) as our application's dispatcher instance. 152 | 153 | ```javascript 154 | // dispatcher/AppDispatcher.js 155 | var Dispatcher = require('flux').Dispatcher; 156 | module.exports = new Dispatcher(); 157 | ``` 158 | 159 | `AppDispatcher.register()` is used for registering a callback for actions. 160 | 161 | ```javascript 162 | // dispatcher/AppDispatcher.js 163 | var ListStore = require('../stores/ListStore'); 164 | 165 | AppDispatcher.register(function (action) { 166 | switch(action.actionType) { 167 | case 'ADD_NEW_ITEM': 168 | ListStore.addNewItemHandler(action.text); 169 | ListStore.emitChange(); 170 | break; 171 | default: 172 | // no op 173 | } 174 | }) 175 | ``` 176 | 177 | In the above code, when receiving the `ADD_NEW_ITEM` action, the callback will operate the `ListStore`. 178 | 179 | Please keep in mind, Dispatcher has no real intelligence on its own — it is a simple mechanism for distributing the actions to the stores. 180 | 181 | ### Stores 182 | 183 | The Stores contain the application state. Their role is somewhat similar to a model in a traditional MVC. 184 | 185 | In this demo, we have a [`ListStore`](https://github.com/ruanyf/extremely-simple-flux-demo/blob/master/stores/ListStore.js) to store data. 186 | 187 | ```javascript 188 | // stores/ListStore.js 189 | var ListStore = { 190 | items: [], 191 | 192 | getAll: function() { 193 | return this.items; 194 | }, 195 | 196 | addNewItemHandler: function (text) { 197 | this.items.push(text); 198 | }, 199 | 200 | emitChange: function () { 201 | this.emit('change'); 202 | } 203 | }; 204 | 205 | module.exports = ListStore; 206 | ``` 207 | 208 | In the above code, `ListStore.items` is used for holding items, `ListStore.getAll()` for getting all these items, and `ListStore.emitChange()` for emitting an event to the Views. 209 | 210 | 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. 211 | 212 | ```javascript 213 | // stores/ListStore.js 214 | var EventEmitter = require('events').EventEmitter; 215 | var assign = require('object-assign'); 216 | 217 | var ListStore = assign({}, EventEmitter.prototype, { 218 | items: [], 219 | 220 | getAll: function () { 221 | return this.items; 222 | }, 223 | 224 | addNewItemHandler: function (text) { 225 | this.items.push(text); 226 | }, 227 | 228 | emitChange: function () { 229 | this.emit('change'); 230 | }, 231 | 232 | addChangeListener: function(callback) { 233 | this.on('change', callback); 234 | }, 235 | 236 | removeChangeListener: function(callback) { 237 | this.removeListener('change', callback); 238 | } 239 | }); 240 | ``` 241 | 242 | In the above code, `ListStore` inheritances `EventEmitter.prototype`, so you can use `ListStore.on()` and `ListStore.emit()`. 243 | 244 | 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. 245 | 246 | ### Views, again 247 | 248 | Now, we come back to [the Views](https://github.com/ruanyf/extremely-simple-flux-demo/blob/master/components/MyButtonController.jsx) for implementing an callback for listening the Store's `change` event. 249 | 250 | ```javascript 251 | // components/MyButtonController.jsx 252 | var React = require('react'); 253 | var ListStore = require('../stores/ListStore'); 254 | var ButtonActions = require('../actions/ButtonActions'); 255 | var MyButton = require('./MyButton'); 256 | 257 | var MyButtonController = React.createClass({ 258 | getInitialState: function () { 259 | return { 260 | items: ListStore.getAll() 261 | }; 262 | }, 263 | 264 | componentDidMount: function() { 265 | ListStore.addChangeListener(this._onChange); 266 | }, 267 | 268 | componentWillUnmount: function() { 269 | ListStore.removeChangeListener(this._onChange); 270 | }, 271 | 272 | _onChange: function () { 273 | this.setState({ 274 | items: ListStore.getAll() 275 | }); 276 | }, 277 | 278 | createNewItem: function (event) { 279 | ButtonActions.addNewItem('new item'); 280 | }, 281 | 282 | render: function() { 283 | return ; 287 | } 288 | }); 289 | ``` 290 | 291 | In the above code, you could see when `MyButtonController` finds out the Store's `change` event occurred, it calls `this._onChange` to update the component's state, then trigger a re-render. 292 | 293 | ```javascript 294 | // components/MyButton.jsx 295 | var React = require('react'); 296 | 297 | var MyButton = function(props) { 298 | var items = props.items; 299 | var itemHtml = items.map(function (listItem, i) { 300 | return
  • {listItem}
  • ; 301 | }); 302 | 303 | return
    304 |
      {itemHtml}
    305 | 306 |
    ; 307 | }; 308 | 309 | module.exports = MyButton; 310 | ``` 311 | 312 | ## License 313 | 314 | MIT 315 | -------------------------------------------------------------------------------- /actions/ButtonActions.js: -------------------------------------------------------------------------------- 1 | var AppDispatcher = require('../dispatcher/AppDispatcher'); 2 | 3 | var ButtonActions = { 4 | 5 | addNewItem: function (text) { 6 | AppDispatcher.dispatch({ 7 | actionType: 'ADD_NEW_ITEM', 8 | text: text 9 | }); 10 | }, 11 | 12 | }; 13 | 14 | module.exports = ButtonActions; 15 | -------------------------------------------------------------------------------- /components/MyButton.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var MyButton = function(props) { 4 | var items = props.items; 5 | var itemHtml = items.map(function (listItem, i) { 6 | return
  • {listItem}
  • ; 7 | }); 8 | 9 | return
    10 |
      {itemHtml}
    11 | 12 |
    ; 13 | }; 14 | 15 | module.exports = MyButton; 16 | -------------------------------------------------------------------------------- /components/MyButtonController.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var ListStore = require('../stores/ListStore'); 3 | var ButtonActions = require('../actions/ButtonActions'); 4 | var MyButton = require('./MyButton'); 5 | 6 | var MyButtonController = React.createClass({ 7 | getInitialState: function () { 8 | return { 9 | items: ListStore.getAll() 10 | }; 11 | }, 12 | 13 | componentDidMount: function() { 14 | ListStore.addChangeListener(this._onChange); 15 | }, 16 | 17 | componentWillUnmount: function() { 18 | ListStore.removeChangeListener(this._onChange); 19 | }, 20 | 21 | _onChange: function () { 22 | this.setState({ 23 | items: ListStore.getAll() 24 | }); 25 | }, 26 | 27 | createNewItem: function (event) { 28 | ButtonActions.addNewItem('new item'); 29 | }, 30 | 31 | render: function() { 32 | return ; 36 | } 37 | 38 | }); 39 | 40 | module.exports = MyButtonController; 41 | -------------------------------------------------------------------------------- /dispatcher/AppDispatcher.js: -------------------------------------------------------------------------------- 1 | var Dispatcher = require('flux').Dispatcher; 2 | var AppDispatcher = new Dispatcher(); 3 | var ListStore = require('../stores/ListStore'); 4 | 5 | AppDispatcher.register(function (action) { 6 | switch(action.actionType) { 7 | case 'ADD_NEW_ITEM': 8 | ListStore.addNewItemHandler(action.text); 9 | ListStore.emitChange(); 10 | break; 11 | default: 12 | // no op 13 | } 14 | }) 15 | 16 | module.exports = AppDispatcher; 17 | -------------------------------------------------------------------------------- /img/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanyf/extremely-simple-flux-demo/2aa579ae5f9ebacfcce80b6977733b7f2c08f564/img/banner.png -------------------------------------------------------------------------------- /img/dataflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanyf/extremely-simple-flux-demo/2aa579ae5f9ebacfcce80b6977733b7f2c08f564/img/dataflow.png -------------------------------------------------------------------------------- /img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanyf/extremely-simple-flux-demo/2aa579ae5f9ebacfcce80b6977733b7f2c08f564/img/screenshot.png -------------------------------------------------------------------------------- /img/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanyf/extremely-simple-flux-demo/2aa579ae5f9ebacfcce80b6977733b7f2c08f564/img/screenshot1.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /index.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var ReactDOM = require('react-dom'); 3 | var MyButtonController = require('./components/MyButtonController'); 4 | 5 | ReactDOM.render( 6 | , 7 | document.querySelector('#example') 8 | ); 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extremely-simple-flux-demo", 3 | "version": "1.0.0", 4 | "description": "an extremely simple demo for the Flux architecture", 5 | "main": "index.jsx", 6 | "scripts": { 7 | "start": "webpack-dev-server --progress", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [ 11 | "Demo", 12 | "Flux", 13 | "React" 14 | ], 15 | "author": "Ruan Yifeng", 16 | "license": "MIT", 17 | "dependencies": { 18 | "babel-loader": "~5.3.2", 19 | "flux": "~2.0.3", 20 | "jsx-loader": "^0.13.2", 21 | "object-assign": "^4.0.1", 22 | "react": "^0.14.0", 23 | "react-dom": "^0.14.0", 24 | "webpack": "^1.11.0" 25 | }, 26 | "devDependencies": { 27 | "open-browser-webpack-plugin": "0.0.2", 28 | "webpack-dev-server": "~1.10.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /stores/ListStore.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | var assign = require('object-assign'); 3 | 4 | var ListStore = assign({}, EventEmitter.prototype, { 5 | items: [], 6 | 7 | getAll: function () { 8 | return this.items; 9 | }, 10 | 11 | addNewItemHandler: function (text) { 12 | this.items.push(text); 13 | }, 14 | 15 | emitChange: function () { 16 | this.emit('change'); 17 | }, 18 | 19 | addChangeListener: function(callback) { 20 | this.on('change', callback); 21 | }, 22 | 23 | removeChangeListener: function(callback) { 24 | this.removeListener('change', callback); 25 | } 26 | }); 27 | 28 | module.exports = ListStore; 29 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin"); 3 | var OpenBrowserPlugin = require('open-browser-webpack-plugin'); 4 | 5 | module.exports = { 6 | entry: './index.jsx', 7 | output: { 8 | filename: 'bundle.js' 9 | }, 10 | resolve: { 11 | extensions: ['', '.js', '.jsx'], 12 | }, 13 | module: { 14 | loaders:[ 15 | { test: /\.jsx$/, exclude: /node_modules/, loader: 'jsx-loader' }, 16 | { test: /\.js$/, exclude:/node_modules/, loader: 'babel-loader'}, 17 | ] 18 | }, 19 | plugins: [ 20 | new CommonsChunkPlugin('init.js'), 21 | new OpenBrowserPlugin({ url: 'http://localhost:8080' }) 22 | ] 23 | }; 24 | --------------------------------------------------------------------------------