├── .gitignore ├── README.md ├── gulpfile.js ├── package.json └── src ├── assets └── product.png ├── index.html └── js ├── actions └── app-actions.js ├── components ├── app-template.js ├── app.js ├── cart │ ├── app-cart.js │ ├── app-decreaseitem.js │ ├── app-increaseitem.js │ └── app-removefromcart.js ├── catalog │ ├── app-addtocart.js │ ├── app-catalog.js │ └── app-catalogitem.js ├── header │ ├── app-cartsummary.js │ └── app-header.js └── product │ └── app-catalogdetail.js ├── constants └── app-constants.js ├── dispatchers └── app-dispatcher.js ├── main.js ├── mixins └── StoreWatchMixin.js └── stores └── app-store.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .idea/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![egghead react flux architecture](https://d2eip9sf3oo6c2.cloudfront.net/series/covers/000/000/005/full/react_flux_series_banner_2.png?1404147531) 2 | 3 | ## Egghead React Flux Example App 4 | 5 | This app requires node.js! 6 | 7 | With node installed, you will also need to install **gulp** globally: 8 | 9 | `npm i -g gulp` 10 | 11 | Now, from the project directory of the master branch install the local dependencies: 12 | 13 | `npm install` 14 | 15 | Now run `gulp` in the project folder. This builds the project in the `dist` folder and watches for any changes. You can serve the `dist` folder. httpster is a great option for this (`npm i -g httpster`). 16 | 17 | Find the [React Flux Architecture video lesson series on egghead.io](https://egghead.io/series/react-flux-architecture). 18 | 19 | 20 | You can switch branches to switch to a particular lesson: 21 | 22 | * [Lesson 1: Development Environment Setup](https://egghead.io/lessons/react-development-environment-setup) 23 | * `git checkout 01-dev-enviroment-setup` 24 | * [Lesson 2: Overview and Dispatchers](https://egghead.io/lessons/react-flux-overview-and-dispatchers) 25 | * `git checkout 02-dispatchers` 26 | * [Lesson 3: Actions](https://egghead.io/lessons/react-actions) 27 | * `git checkout 03-actions` 28 | * [Lesson 4: Stores](https://egghead.io/lessons/react-flux-stores) 29 | * `git checkout 04-stores` 30 | * [Lesson 5: Component/Views](https://egghead.io/lessons/react-flux-components-views) 31 | * `git checkout 05-components-views` 32 | * [Lesson 6: Project Organization](https://egghead.io/lessons/react-react-flux-project-organization) 33 | * `git checkout 06-project-organization` 34 | * [Lesson 7: Routing with react-router-component](https://egghead.io/lessons/react-react-flux-routing-with-react-router-component) 35 | * `git checkout 07-routing` 36 | * [Lesson 8: Remove Duplicate Code with Mixins](https://egghead.io/lessons/react-react-flux-remove-duplicate-code-with-mixins) 37 | * `git checkout 08-mixins` 38 | * [Lesson 9: Wrapping Up](https://egghead.io/lessons/react-react-flux-wrapping-up) 39 | * `git checkout 09-wrap-up` 40 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var browserify = require('browserify'); 3 | var reactify = require('reactify'); 4 | var source = require('vinyl-source-stream'); 5 | 6 | gulp.task('browserify', function() { 7 | browserify('./src/js/main.js') 8 | .transform('reactify') 9 | .bundle() 10 | .pipe(source('main.js')) 11 | .pipe(gulp.dest('dist/js')); 12 | }); 13 | 14 | gulp.task('copy',function() { 15 | gulp.src('src/index.html') 16 | .pipe(gulp.dest('dist')); 17 | gulp.src('src/assets/**/*.*') 18 | .pipe(gulp.dest('dist/assets')); 19 | }); 20 | 21 | gulp.task('default',['browserify', 'copy'], function() { 22 | return gulp.watch('src/**/*.*', ['browserify', 'copy']) 23 | }); 24 | 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "egghead-flux-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "gulpfile.js", 6 | "dependencies": { 7 | "browserify": "^10.2.6", 8 | "envify": "^3.4.0", 9 | "flux": "^2.1.1", 10 | "gulp": "^3.9.0", 11 | "react": "^0.14.0", 12 | "react-async": "^2.1.0", 13 | "react-dom": "^0.14.0", 14 | "react-router-component": "^0.27.0", 15 | "reactify": "^1.1.1", 16 | "vinyl-source-stream": "^1.1.0" 17 | }, 18 | "devDependencies": {}, 19 | "scripts": { 20 | "test": "echo \"Error: no test specified\" && exit 1" 21 | }, 22 | "author": "", 23 | "license": "ISC" 24 | } -------------------------------------------------------------------------------- /src/assets/product.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eggheadio/egghead-react-flux-example/2ed06a5d289dbbce486de476c2225b16872dc406/src/assets/product.png -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Document 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/js/actions/app-actions.js: -------------------------------------------------------------------------------- 1 | var AppConstants = require('../constants/app-constants'); 2 | var AppDispatcher = require('../dispatchers/app-dispatcher'); 3 | 4 | var AppActions = { 5 | addItem: function(item){ 6 | AppDispatcher.handleViewAction({ 7 | actionType: AppConstants.ADD_ITEM, 8 | item: item 9 | }) 10 | }, 11 | removeItem: function(index){ 12 | AppDispatcher.handleViewAction({ 13 | actionType: AppConstants.REMOVE_ITEM, 14 | index: index 15 | }) 16 | }, 17 | increaseItem: function(index){ 18 | AppDispatcher.handleViewAction({ 19 | actionType: AppConstants.INCREASE_ITEM, 20 | index: index 21 | }) 22 | }, 23 | decreaseItem: function(index){ 24 | AppDispatcher.handleViewAction({ 25 | actionType: AppConstants.DECREASE_ITEM, 26 | index: index 27 | }) 28 | } 29 | } 30 | 31 | module.exports = AppActions; 32 | -------------------------------------------------------------------------------- /src/js/components/app-template.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var Header = require('./header/app-header.js'); 3 | 4 | var Template = React.createClass({ 5 | render:function(){ 6 | return ( 7 |
8 |
9 | {this.props.children} 10 |
11 | ); 12 | } 13 | }); 14 | 15 | module.exports = Template; 16 | -------------------------------------------------------------------------------- /src/js/components/app.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var Catalog = require('./catalog/app-catalog'); 3 | var Cart = require('./cart/app-cart'); 4 | var Router = require('react-router-component'); 5 | var CatalogDetail = require('./product/app-catalogdetail'); 6 | var Template = require('./app-template.js'); 7 | var Locations = Router.Locations; 8 | var Location = Router.Location; 9 | 10 | var App = React.createClass({ 11 | render:function(){ 12 | return ( 13 | 20 | ); 21 | } 22 | }); 23 | 24 | module.exports = App; 25 | -------------------------------------------------------------------------------- /src/js/components/cart/app-cart.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var AppStore = require('../../stores/app-store.js'); 3 | var RemoveFromCart = require('./app-removefromcart.js'); 4 | var Increase = require('./app-decreaseitem') 5 | var Decrease = require('./app-increaseitem') 6 | var StoreWatchMixin = require('../../mixins/StoreWatchMixin'); 7 | var Link = require('react-router-component').Link 8 | 9 | function cartItems(){ 10 | return {items: AppStore.getCart()} 11 | } 12 | 13 | var Cart = React.createClass({ 14 | mixins:[StoreWatchMixin(cartItems)], 15 | render:function(){ 16 | var total = 0; 17 | var items = this.state.items.map(function(item, i){ 18 | var subtotal = item.cost * item.qty; 19 | total += subtotal; 20 | return ( 21 | 22 | 23 | {item.title} 24 | {item.qty} 25 | 26 | 27 | 28 | 29 | ${subtotal} 30 | 31 | ); 32 | }) 33 | return ( 34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | {items} 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
ItemQtySubtotal
Total${total}
55 | Continue Shopping 56 |
57 | ) 58 | } 59 | }); 60 | 61 | module.exports = Cart 62 | -------------------------------------------------------------------------------- /src/js/components/cart/app-decreaseitem.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var AppActions = require('../../actions/app-actions'); 3 | 4 | var DecreaseItem = React.createClass({ 5 | handler: function(){ 6 | AppActions.decreaseItem(this.props.index) 7 | }, 8 | render:function(){ 9 | return 10 | } 11 | }); 12 | 13 | module.exports = DecreaseItem; 14 | -------------------------------------------------------------------------------- /src/js/components/cart/app-increaseitem.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var AppActions = require('../../actions/app-actions'); 3 | 4 | var IncreaseItem = React.createClass({ 5 | handler: function(){ 6 | AppActions.increaseItem(this.props.index) 7 | }, 8 | render:function(){ 9 | return 10 | } 11 | }); 12 | 13 | module.exports = IncreaseItem; 14 | -------------------------------------------------------------------------------- /src/js/components/cart/app-removefromcart.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var AppActions = require('../../actions/app-actions'); 3 | 4 | var RemoveFromCart = React.createClass({ 5 | handler: function(){ 6 | AppActions.removeItem(this.props.index) 7 | }, 8 | render:function(){ 9 | return 10 | } 11 | }); 12 | 13 | module.exports = RemoveFromCart; 14 | -------------------------------------------------------------------------------- /src/js/components/catalog/app-addtocart.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var AppActions = require('../../actions/app-actions'); 3 | 4 | var AddToCart = React.createClass({ 5 | handler: function(){ 6 | AppActions.addItem(this.props.item) 7 | }, 8 | render:function(){ 9 | return 10 | } 11 | }); 12 | 13 | module.exports = AddToCart; 14 | -------------------------------------------------------------------------------- /src/js/components/catalog/app-catalog.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var AppStore = require('../../stores/app-store.js'); 3 | var AddToCart = require('./app-addtocart.js') 4 | var StoreWatchMixin = require('../../mixins/StoreWatchMixin'); 5 | var CatalogItem = require('../catalog/app-catalogitem'); 6 | 7 | 8 | function getCatalog(){ 9 | return {items: AppStore.getCatalog()} 10 | } 11 | 12 | var Catalog = React.createClass({ 13 | mixins:[StoreWatchMixin(getCatalog)], 14 | render:function(){ 15 | var items = this.state.items.map(function(item){ 16 | return 17 | 18 | }) 19 | return ( 20 |
21 | {items} 22 |
23 | ) 24 | } 25 | }); 26 | 27 | module.exports = Catalog 28 | -------------------------------------------------------------------------------- /src/js/components/catalog/app-catalogitem.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var Link = require('react-router-component').Link; 3 | var AddToCart = require('./app-addtocart'); 4 | 5 | var CatalogItem = React.createClass({ 6 | render:function(){ 7 | var itemStyle = { 8 | borderBottom:'1px solid #ccc', 9 | paddingBottom:15 10 | }; 11 | return ( 12 |
13 |

{this.props.item.title}

14 | 15 |

{this.props.item.summary}

16 |

${this.props.item.cost} {this.props.item.inCart && '(' + this.props.item.qty + ' in cart)'}

17 |
18 | Learn More 19 | 20 |
21 |
22 | 23 | ) 24 | } 25 | }); 26 | 27 | module.exports = CatalogItem; 28 | -------------------------------------------------------------------------------- /src/js/components/header/app-cartsummary.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var Link = require('react-router-component').Link; 3 | var AppStore = require('../../stores/app-store.js'); 4 | var StoreWatchMixin = require('../../mixins/StoreWatchMixin'); 5 | 6 | function cartTotals(){ 7 | return AppStore.getCartTotals(); 8 | } 9 | 10 | var CartSummary = React.createClass({ 11 | mixins: [StoreWatchMixin(cartTotals)], 12 | render:function(){ 13 | return ( 14 |
15 | 16 | Cart Items: {this.state.qty} / ${this.state.total} 17 | 18 |
19 | ); 20 | } 21 | }); 22 | 23 | module.exports = CartSummary; 24 | -------------------------------------------------------------------------------- /src/js/components/header/app-header.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var CartSummary = require('./app-cartsummary.js'); 3 | 4 | var Header = React.createClass({ 5 | render:function(){ 6 | return ( 7 |
8 |

Lets Shop

9 |
10 |
11 | 12 |
13 |
14 | ); 15 | } 16 | }); 17 | 18 | module.exports = Header; 19 | -------------------------------------------------------------------------------- /src/js/components/product/app-catalogdetail.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var AppStore = require('../../stores/app-store.js'); 3 | var AddToCart = require('../catalog/app-addtocart.js') 4 | var StoreWatchMixin = require('../../mixins/StoreWatchMixin'); 5 | var Link = require('react-router-component').Link; 6 | 7 | function getCatalogItem(component){ 8 | var thisItem; 9 | AppStore.getCatalog().forEach(function(item){ 10 | if(item.id.toString() === component.props.item){ 11 | thisItem = item 12 | } 13 | }); 14 | return {item: thisItem} 15 | } 16 | 17 | var CatalogDetail = React.createClass({ 18 | mixins:[StoreWatchMixin(getCatalogItem)], 19 | render:function(){ 20 | return ( 21 |
22 |

{this.state.item.title}

23 | 24 |

{this.state.item.description}

25 |

${this.state.item.cost} {this.state.item.inCart && '(' + this.state.item.qty + ' in cart)'}

26 |
27 | 28 | Continue Shopping 29 |
30 |
31 | ); 32 | } 33 | }); 34 | 35 | module.exports = CatalogDetail; 36 | -------------------------------------------------------------------------------- /src/js/constants/app-constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ADD_ITEM: 'ADD_ITEM', 3 | REMOVE_ITEM: 'REMOVE_ITEM', 4 | INCREASE_ITEM: 'INCREASE_ITEM', 5 | DECREASE_ITEM: 'DECREASE_ITEM' 6 | }; 7 | -------------------------------------------------------------------------------- /src/js/dispatchers/app-dispatcher.js: -------------------------------------------------------------------------------- 1 | var Dispatcher = require('flux').Dispatcher; 2 | var assign = require('react/lib/Object.assign'); 3 | 4 | var AppDispatcher = assign(new Dispatcher(), { 5 | handleViewAction: function(action){ 6 | console.log('action', action); 7 | this.dispatch({ 8 | source: 'VIEW_ACTION', 9 | action: action 10 | }) 11 | } 12 | }); 13 | 14 | module.exports = AppDispatcher; 15 | -------------------------------------------------------------------------------- /src/js/main.js: -------------------------------------------------------------------------------- 1 | var App = require('./components/app'); 2 | var React = require('react'); 3 | var ReactDOM = require('react-dom'); 4 | 5 | ReactDOM.render(, document.getElementById('main')); -------------------------------------------------------------------------------- /src/js/mixins/StoreWatchMixin.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var AppStore = require('../stores/app-store'); 3 | 4 | var StoreWatchMixin = function(cb){ 5 | return { 6 | getInitialState:function(){ 7 | return cb(this) 8 | }, 9 | componentWillMount:function(){ 10 | AppStore.addChangeListener(this._onChange) 11 | }, 12 | componentWillUnmount:function(){ 13 | AppStore.removeChangeListener(this._onChange) 14 | }, 15 | _onChange: function(){ 16 | this.setState(cb(this)) 17 | } 18 | } 19 | } 20 | 21 | module.exports = StoreWatchMixin; 22 | -------------------------------------------------------------------------------- /src/js/stores/app-store.js: -------------------------------------------------------------------------------- 1 | var AppDispatcher = require('../dispatchers/app-dispatcher'); 2 | var AppConstants = require('../constants/app-constants'); 3 | var assign = require('react/lib/Object.assign'); 4 | var EventEmitter = require('events').EventEmitter; 5 | 6 | var CHANGE_EVENT = 'change'; 7 | 8 | var _catalog = []; 9 | 10 | for(var i=1; i<9; i++){ 11 | _catalog.push({ 12 | 'id': 'Widget' + i, 13 | 'title':'Widget #' + i, 14 | 'summary': 'This is an awesome widget!', 15 | 'description': 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Ducimus, commodi.', 16 | 'cost': i, 17 | 'img': '/assets/product.png' 18 | }); 19 | } 20 | 21 | var _cartItems = []; 22 | 23 | function _removeItem(index){ 24 | _cartItems[index].inCart = false; 25 | _cartItems.splice(index, 1); 26 | } 27 | 28 | function _increaseItem(index){ 29 | _cartItems[index].qty++; 30 | } 31 | 32 | function _decreaseItem(index){ 33 | if(_cartItems[index].qty>1){ 34 | _cartItems[index].qty--; 35 | } 36 | else { 37 | _removeItem(index); 38 | } 39 | } 40 | 41 | function _addItem(item){ 42 | if(!item.inCart){ 43 | item['qty'] = 1; 44 | item['inCart'] = true; 45 | _cartItems.push(item); 46 | } 47 | else { 48 | _cartItems.forEach(function(cartItem, i){ 49 | if(cartItem.id===item.id){ 50 | _increaseItem(i); 51 | } 52 | }); 53 | } 54 | } 55 | 56 | function _cartTotals(){ 57 | var qty =0, total = 0; 58 | _cartItems.forEach(function(cartItem){ 59 | qty+=cartItem.qty; 60 | total+=cartItem.qty*cartItem.cost; 61 | }); 62 | return {'qty': qty, 'total': total}; 63 | } 64 | 65 | var AppStore = assign(EventEmitter.prototype, { 66 | emitChange: function(){ 67 | this.emit(CHANGE_EVENT) 68 | }, 69 | 70 | addChangeListener: function(callback){ 71 | this.on(CHANGE_EVENT, callback) 72 | }, 73 | 74 | removeChangeListener: function(callback){ 75 | this.removeListener(CHANGE_EVENT, callback) 76 | }, 77 | 78 | getCart: function(){ 79 | return _cartItems 80 | }, 81 | 82 | getCatalog: function(){ 83 | return _catalog 84 | }, 85 | 86 | getCartTotals: function(){ 87 | return _cartTotals() 88 | }, 89 | 90 | dispatcherIndex: AppDispatcher.register(function(payload){ 91 | var action = payload.action; // this is our action from handleViewAction 92 | switch(action.actionType){ 93 | case AppConstants.ADD_ITEM: 94 | _addItem(payload.action.item); 95 | break; 96 | 97 | case AppConstants.REMOVE_ITEM: 98 | _removeItem(payload.action.index); 99 | break; 100 | 101 | case AppConstants.INCREASE_ITEM: 102 | _increaseItem(payload.action.index); 103 | break; 104 | 105 | case AppConstants.DECREASE_ITEM: 106 | _decreaseItem(payload.action.index); 107 | break; 108 | } 109 | 110 | AppStore.emitChange(); 111 | 112 | return true; 113 | }) 114 | 115 | }) 116 | 117 | module.exports = AppStore; 118 | --------------------------------------------------------------------------------