├── .gitignore ├── lib ├── constants │ └── ActionConstants.js ├── components │ ├── App.jsx │ └── Category.jsx ├── services │ └── Api.js ├── core │ └── Dispatcher.js ├── actions │ └── ActionCreator.js └── stores │ └── Store.js ├── data └── categories.js ├── static └── index.html ├── app.js ├── .jshintrc ├── server.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | static/bundle.js 4 | -------------------------------------------------------------------------------- /lib/constants/ActionConstants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | RECEIVE_CATEGORIES: 'RECEIVE_CATEGORIES', 3 | RECEIVE_ERROR: 'RECEIVE_ERROR' 4 | }; 5 | -------------------------------------------------------------------------------- /data/categories.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Mock categories to be used with API call. 4 | var categories = [ 5 | { id: 1, name: 'Music' }, 6 | { id: 2, name: 'Movies' }, 7 | { id: 3, name: 'Books' } 8 | ]; 9 | 10 | module.exports = categories; 11 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Calling API in React/Flux 5 | 6 | 7 | 8 |
9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | 5 | // Export React so the dev tools can find it 6 | (window !== window.top ? window.top : window).React = React; 7 | 8 | React.render( 9 | React.createElement(require('./lib/components/App.jsx')), 10 | document.getElementById('app') 11 | ); 12 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "camelcase": true, 3 | "immed": true, 4 | "indent": 2, 5 | "latedef": true, 6 | "newcap": true, 7 | "quotmark": "single", 8 | 9 | "esnext": true, 10 | "globalstrict": true, 11 | 12 | "browser": true, 13 | "node": true, 14 | 15 | "globals": { 16 | "require": false, 17 | "__dirname": false, 18 | "__DEV__": false 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/components/App.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | var Category = require('./Category.jsx'); 5 | 6 | var App = React.createClass({ 7 | 8 | 9 | /* jshint ignore:start */ 10 | render: function() { 11 | return ( 12 |
13 |
14 |
15 | 16 | 17 | 18 |
19 | ); 20 | } 21 | /* jshint ignore:end */ 22 | 23 | }); 24 | 25 | module.exports = App; 26 | -------------------------------------------------------------------------------- /lib/services/Api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var request = require('superagent'); 4 | var Promise = require('es6-promise').Promise; // jshint ignore:line 5 | 6 | /** 7 | * Wrapper for calling a API 8 | */ 9 | var Api = { 10 | get: function (url) { 11 | return new Promise(function (resolve, reject) { 12 | request 13 | .get(url) 14 | .end(function (res) { 15 | if (res.status === 404) { 16 | reject(); 17 | } else { 18 | resolve(JSON.parse(res.text)); 19 | } 20 | }); 21 | }); 22 | } 23 | }; 24 | 25 | module.exports = Api; 26 | -------------------------------------------------------------------------------- /lib/core/Dispatcher.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Flux = require('flux'); 4 | var assign = require('object-assign'); 5 | 6 | /** 7 | * A singleton that operates as the central hub for application updates. 8 | * For more information visit https://facebook.github.io/flux/ 9 | */ 10 | var Dispatcher = assign(new Flux.Dispatcher(), { 11 | 12 | /** 13 | * @param {object} action The details of the action, including the action's 14 | * type and additional data coming from the view. 15 | */ 16 | handleViewAction: function (action) { 17 | var payload = { 18 | action: action 19 | }; 20 | this.dispatch(payload); 21 | } 22 | 23 | }); 24 | 25 | module.exports = Dispatcher; 26 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var http = require('http'); 4 | var router = require('routes')(); 5 | var ecstatic = require('ecstatic')(__dirname + '/static'); 6 | var categories = require('./data/categories'); 7 | 8 | router.addRoute('/api/categories', function (req, res, params) { 9 | res.setHeader('content-type', 'application/json'); 10 | res.write(JSON.stringify(categories)); 11 | res.end(); 12 | }); 13 | 14 | var server = http.createServer(function (req, res) { 15 | var m = router.match(req.url); 16 | if (m) m.fn(req, res, m.params); 17 | else ecstatic(req, res); 18 | }); 19 | 20 | server.listen(5000, function () { 21 | console.log('listening on :' + server.address().port); 22 | }); 23 | 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-flux-api-calls", 3 | "version": "0.0.1", 4 | "description": "React/Flux Where to make API calls", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "npm run build && node server.js", 8 | "start-dev": "npm run watch & npm start", 9 | "build": "browserify -t reactify app.js > static/bundle.js", 10 | "watch": "watchify -t reactify app.js lib/**/*.* -o static/bundle.js -dv" 11 | }, 12 | "author": "Brian Schemp", 13 | "dependencies": { 14 | "ecstatic": "3.2.0", 15 | "es6-promise": "^2.0.1", 16 | "flux": "^2.0.1", 17 | "object-assign": "^2.0.0", 18 | "react": "^0.12.2", 19 | "routes": "^2.0.0", 20 | "superagent": "^0.21.0" 21 | }, 22 | "devDependencies": { 23 | "browserify": "^8.1.1", 24 | "reactify": "^1.0.0", 25 | "watchify": "^2.3.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/actions/ActionCreator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Dispatcher = require('../core/Dispatcher'); 4 | var ActionConstants = require('../constants/ActionConstants'); 5 | var Store = require('../stores/Store'); 6 | var Promise = require('es6-promise').Promise; // jshint ignore:line 7 | var Api = require('../services/Api'); 8 | 9 | var ActionCreator = { 10 | 11 | /** 12 | * 13 | * 14 | */ 15 | getCategories: function () { 16 | Api 17 | .get('/api/categories') 18 | .then(function (categories) { 19 | Dispatcher.handleViewAction({ 20 | actionType: ActionConstants.RECEIVE_CATEGORIES, 21 | categories: categories 22 | }); 23 | }) 24 | .catch(function () { 25 | Dispatcher.handleViewAction({ 26 | actionType: ActionConstants.RECEIVE_ERROR, 27 | error: 'There was a problem getting the categories' 28 | }); 29 | }); 30 | } 31 | }; 32 | 33 | module.exports = ActionCreator; 34 | -------------------------------------------------------------------------------- /lib/components/Category.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | var Store = require('../stores/Store'); 5 | var ActionCreator = require('../actions/ActionCreator'); 6 | 7 | var Category = React.createClass({ 8 | 9 | handleChange: function (e) { 10 | //this.transitionTo('/products/' + e.target.value); 11 | }, 12 | 13 | getInitialState: function () { 14 | return { 15 | categories: [] 16 | }; 17 | }, 18 | 19 | componentWillMount: function () { 20 | Store.addChangeListener(this._onChange); 21 | }, 22 | 23 | componentDidMount: function () { 24 | ActionCreator.getCategories(); 25 | }, 26 | 27 | componentWillUnmount: function () { 28 | Store.removeChangeListener(this._onChange); 29 | }, 30 | 31 | _onChange: function () { 32 | this.setState({ 33 | categories: Store.getCategories() 34 | }); 35 | }, 36 | 37 | /* jshint ignore:start */ 38 | render: function () { 39 | var categories; 40 | 41 | if (this.state.categories) { 42 | categories = this.state.categories.map(function (category) { 43 | return ; 46 | }); 47 | } 48 | 49 | return ( 50 |
51 | 55 |
56 | ); 57 | 58 | } 59 | /* jshint ignore:end */ 60 | 61 | }); 62 | 63 | module.exports = Category; 64 | -------------------------------------------------------------------------------- /lib/stores/Store.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Dispatcher = require('../core/Dispatcher'); 4 | var ActionConstants = require('../constants/ActionConstants'); 5 | var EventEmitter = require('events').EventEmitter; 6 | var assign = require('object-assign'); 7 | 8 | var CHANGE_EVENT = 'change', 9 | _categories = []; 10 | 11 | /** 12 | * Set the values for categories that will be used 13 | * with components. 14 | */ 15 | function setCategories (categories) { 16 | _categories = categories; 17 | } 18 | 19 | var Store = assign({}, EventEmitter.prototype, { 20 | 21 | /** 22 | * Emits change event. 23 | */ 24 | emitChange: function () { 25 | this.emit(CHANGE_EVENT); 26 | }, 27 | 28 | /** 29 | * Adds a change listener. 30 | * 31 | * @param {function} callback Callback function. 32 | */ 33 | addChangeListener: function (callback) { 34 | this.on(CHANGE_EVENT, callback); 35 | }, 36 | 37 | /** 38 | * Removes a change listener. 39 | * 40 | * @param {function} callback Callback function. 41 | */ 42 | removeChangeListener: function (callback) { 43 | this.removeListener(CHANGE_EVENT, callback); 44 | }, 45 | 46 | /** 47 | * Return the value for categories. 48 | */ 49 | getCategories: function () { 50 | return _categories; 51 | } 52 | }); 53 | 54 | Store.dispatchToken = Dispatcher.register(function (payload) { 55 | var action = payload.action; 56 | 57 | switch (action.actionType) { 58 | case ActionConstants.RECEIVE_CATEGORIES: 59 | setCategories(action.categories); 60 | break; 61 | 62 | default: 63 | return true; 64 | } 65 | 66 | Store.emitChange(); 67 | 68 | return true; 69 | }); 70 | 71 | module.exports = Store; 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Working with React/Flux the last three months I couldn't decide where to make asynchronous calls. Should they be made in the component, store or action creators? I chose the action creators since dispatching of all actions come from them. A module could abstract the actual asynchronous call and return a promise. The promise would resolve with the result of the call or be rejected if there was an error. 2 | 3 | Here's an example of the module to make the asynchronous call. I used [superagent](https://github.com/visionmedia/superagent) to make the request. 4 | 5 | ```js 6 | var Api = { 7 | get: function (url) { 8 | return new Promise(function (resolve, reject) { 9 | request 10 | .get(url) 11 | .end(function (res) { 12 | if (res.status === 404) { 13 | reject(); 14 | } else { 15 | resolve(JSON.parse(res.text)); 16 | } 17 | }); 18 | }); 19 | } 20 | }; 21 | ``` 22 | 23 | The action creator would use this module. When the promise is returned dispatch an action containing the result. 24 | 25 | ```js 26 | var Api = require('./Api'); 27 | var Dispatcher = require('./Dispatcher'); 28 | var ActionConstants = require('./ActionConstants'); 29 | 30 | // Define the ActionCreator. 31 | var ActionCreator = { 32 | getCategories: function () { 33 | Api 34 | .get('/api/categories') 35 | .then(function (categories) { 36 | 37 | // Dispatch an action containing the categories. 38 | Dispatcher.handleViewAction({ 39 | actionType: ActionConstants.RECEIVE_CATEGORIES, 40 | categories: categories 41 | }); 42 | }); 43 | }; 44 | }; 45 | ``` 46 | 47 | The store would register with the dispatcher and provide a callback to handle the response from the action. It would also emit a change event so components would be notified that values have changed. 48 | 49 | ```js 50 | var Dispatcher = require('./Dispatcher'); 51 | var ActionConstants = require('./ActionConstants'); 52 | var EventEmitter = require('events').EventEmitter; 53 | var assign = require('object-assign'); 54 | 55 | var CHANGE_EVENT = 'change'; 56 | var _categories = []; 57 | 58 | function setCategories (categories) { 59 | _categories = categories; 60 | } 61 | 62 | // Define the Store. 63 | var Store = assign({}, EventEmitter.prototype, { 64 | 65 | emitChange: function () { 66 | this.emit(CHANGE_EVENT); 67 | }, 68 | 69 | addChangeListener: function (callback) { 70 | this.on(CHANGE_EVENT, callback); 71 | }, 72 | 73 | removeChangeListener: function (callback) { 74 | this.removeListener(CHANGE_EVENT, callback); 75 | }, 76 | 77 | getCategories: function () { 78 | return _categories; 79 | } 80 | }); 81 | 82 | // Store registers with dispatcher to handle actions. 83 | Store.dispatchToken = Dispatcher.register(function (payload) { 84 | var action = payload.action; 85 | 86 | switch (action.actionType) { 87 | case ActionConstants.RECEIVE_CATEGORIES: 88 | 89 | // Callback to handle the response from the action. 90 | setCategories(); 91 | break; 92 | 93 | default: 94 | return true; 95 | break; 96 | } 97 | 98 | Store.emitChange(); 99 | 100 | return true; 101 | }); 102 | ``` 103 | 104 | Finally our components would use the store to register change listeners and the action creator for getting our categories! 105 | 106 | ```js 107 | var Store = require('./Store'); 108 | var ActionCreator = require('./ActionCreator'); 109 | 110 | // Define the Category component. 111 | var Category = React.createClass({ 112 | 113 | getInitialState: function () { 114 | return { 115 | categories: [] 116 | }; 117 | }, 118 | 119 | componentWillMount: function () { 120 | Store.addChangeListener(this._onChange); 121 | }, 122 | 123 | // Use the ActionCreator to get the categories. 124 | componentDidMount: function () { 125 | ActionCreator.getCategories(); 126 | }, 127 | 128 | componentWillUnmount: function () { 129 | Store.removeChangeListener(this._onChange); 130 | }, 131 | 132 | /** 133 | * Update the state of categories for this component. 134 | * This will get called when our store handles the response 135 | * from the action. 136 | */ 137 | _onChange: function () { 138 | this.setState({ 139 | categories: Store.getCategories() 140 | }); 141 | }, 142 | 143 | // Display a drop-down containg the categories. 144 | render: function () { 145 | var categories; 146 | 147 | if (this.state.categories) { 148 | categories = this.state.categories.map(function (category) { 149 | return ; 152 | }); 153 | } 154 | 155 | return ( 156 |
157 | 161 |
162 | ); 163 | } 164 | }); 165 | ``` 166 | 167 | 168 | ## final thoughts 169 | The result from an asynchronous call should create an action. This keeps the data flowing through the application the flux way (Actions -> Dispatcher -> Stores -> Views). 170 | 171 | One issue having the category component make a Api call once it's mounted is it will be rendered twice. Having components get their data from props is better but sometimes the data has to be from an external service. 172 | 173 | ## running the app 174 | Install all the node modules 175 | 176 | ``` js 177 | $ npm install 178 | ``` 179 | 180 | Start the development server. This will use browserify to build the javascript 181 | and watchify to re-build the javascript on any changes. 182 | 183 | ``` js 184 | $ npm run start-dev 185 | ``` 186 | 187 | To start the server without any javascript watching 188 | 189 | ``` js 190 | $ npm start 191 | ``` 192 | 193 | Point your browser to http://localhost:5000 194 | 195 | Checkout the package.json file for the npm scripts. --------------------------------------------------------------------------------