├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── lib └── index.js ├── package.json ├── src └── index.js └── test └── spec.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": ["react"], 4 | "ecmaFeatures": { 5 | "jsx": true, 6 | "objectLiteralShorthandMethods": true 7 | }, 8 | "env": { 9 | "browser": true, 10 | "node": true, 11 | "es6": true 12 | }, 13 | "rules": { 14 | "quotes": [0], 15 | "new-parens": [1], 16 | "no-alert": [1], 17 | "handle-callback-err": [1], 18 | "strict": [1], 19 | "no-script-url": [0], 20 | "space-unary-ops": [0], 21 | "consistent-return": [0], 22 | "comma-dangle": 1, 23 | "no-mixed-requires": [1], 24 | "no-underscore-dangle": [0], 25 | "no-multi-spaces": [1], 26 | "no-unused-vars": [1], 27 | "key-spacing": [0], 28 | "no-empty": [1], 29 | "no-shadow": [0], 30 | "no-use-before-define": [0], 31 | "no-unused-expressions": [1], 32 | "no-new-func": [0], 33 | "new-cap": [0], 34 | "eqeqeq": [0], 35 | "curly": [0], 36 | "strict": [0], 37 | "no-new": [1], 38 | "eol-last": [1], 39 | "space-infix-ops": [1], 40 | "no-return-assign": [1], 41 | "comma-spacing": [1], 42 | "no-extra-boolean-cast": [1], 43 | "no-constant-condition": [1], 44 | 45 | "react/display-name": 0, 46 | "react/jsx-boolean-value": 1, 47 | "react/jsx-no-duplicate-props": 1, 48 | "react/jsx-no-undef": 1, 49 | "react/jsx-quotes": 1, 50 | "react/jsx-sort-prop-types": 0, 51 | "react/jsx-sort-props": 0, 52 | "react/jsx-uses-react": 1, 53 | "react/jsx-uses-vars": 1, 54 | "react/no-danger": 1, 55 | "react/no-did-mount-set-state": 0, 56 | "react/no-did-update-set-state": 1, 57 | "react/no-multi-comp": 1, 58 | "react/no-unknown-property": 1, 59 | "react/prop-types": 0, 60 | "react/react-in-jsx-scope": 0, 61 | "react/require-extension": 1, 62 | "react/self-closing-comp": 1, 63 | "react/sort-comp": 1, 64 | "react/wrap-multilines": 1 65 | }, 66 | "globals": { 67 | "describe": false, 68 | "chai": false, 69 | "beforeEach": false, 70 | "afterEach": false, 71 | "it": false 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | test/coverage 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5.0" 4 | script: "npm run test" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Sen Yang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Redux Sequence Action 2 | ========================== 3 | 4 | A [middleware](http://rackt.github.io/redux/docs/advanced/Middleware.html) enabling sequential action dispatch for Redux. 5 | 6 | [![Build Status](https://travis-ci.org/jasonslyvia/redux-sequence-action.svg)](https://travis-ci.org/jasonslyvia/redux-sequence-action) 7 | [![npm version](https://badge.fury.io/js/redux-sequence-action.svg)](http://badge.fury.io/js/redux-sequence-action) 8 | 9 | ``` 10 | $ npm install --save redux-sequence-action 11 | ``` 12 | 13 | ## Why 14 | 15 | Suppose you have a `AddressPicker` component which let user select the delivery address. It consists of 2 `select` with state list and city list. User can picks a state then a city. 16 | 17 | ![select](http://ww2.sinaimg.cn/bmiddle/831e9385gw1ex7w1vkbypj205900rjr7.jpg) 18 | 19 | So you will have the following action creators: 20 | 21 | - selectState (stateId) 22 | - selectCity (cityId) 23 | 24 | However, when user changes a state, the city list should be updated accordding to new state. For action creator `selectState`, it actually does the duty of `selectState` and `selectCity`. 25 | 26 | For example, suppose we must dispatch some actions in certain order: A => B & C => D => E. A is a sync action and others are async actions. So we do this: 27 | 28 | ```javascript 29 | dispatch((dispatch, getState) => { 30 | dispatch(A); 31 | Promise.all(dispatch(B), dispatch(C)).then(() => { 32 | return dispatch(D); 33 | }).then(() => { 34 | dispatch(E); 35 | }); 36 | }) 37 | // A => B & C => D => E 38 | // A ~ E are actions 39 | ``` 40 | 41 | It need apply a thunk middleware which dispatch function like action, and a fetch middleware which is responsible for getting API data and return a promise. 42 | 43 | ```javascript 44 | store => next => action => { 45 | //return a promise here 46 | return asyncAction(url, params).then( 47 | data => { 48 | return next({...action, payload: data, type: successType}) 49 | }, e => {} 50 | ) 51 | } 52 | 53 | ``` 54 | 55 | If you use redux-sequnce-action, you can merely write declarative code like this: 56 | 57 | ```javascript 58 | dispatch([ 59 | A, 60 | [B, C], 61 | D, 62 | E 63 | ]) 64 | ``` 65 | 66 | Yes, we only provide a syntax sugar. 67 | 68 | ## How 69 | 70 | To better reuse our code, we can dispatch a action that dispatchs more action in sequence, looks like this: 71 | 72 | ```javascript 73 | function selectState(stateId) { 74 | return [ 75 | { 76 | type: 'SELECT_STATE', 77 | payload: stateId 78 | }, 79 | (dispatch, getState) => { 80 | // `getState()` returns the state (or store) which is computed through 81 | // first action, so you can use this updated store to find out needed 82 | // portion and pass it to next action creator 83 | const {cityId} = getState().cityList[0]; 84 | dispatch(selectCity(cityId)) 85 | } 86 | ] 87 | } 88 | 89 | function selectCity(cityId) { 90 | return { 91 | type: 'SELECT_CITY', 92 | payload: cityId 93 | }; 94 | } 95 | ``` 96 | 97 | When we call `selectState(13)`, this action creator will first dispatch a `SELECT_STATE` action with payload `13`. Our reducer should update and return the new state (or store). 98 | 99 | Then it will dispatch another action defined as the second element in `steps` array. Inside this function, we can get updated store and find out wanted part of the store and pass it to next action. 100 | 101 | ## Usage 102 | 103 | ``` 104 | $ npm install --save redux-sequence-action 105 | ``` 106 | 107 | Then, to enable Redux Sequence Action, use applyMiddleware(): 108 | 109 | ```javascript 110 | import { createStore, applyMiddleware } from 'redux'; 111 | import sequenceAction from 'redux-sequence-action'; 112 | import rootReducer from './reducers/index'; 113 | 114 | // create a store that has redux-sequence-action middleware enabled 115 | const createStoreWithMiddleware = applyMiddleware( 116 | sequenceAction 117 | )(createStore); 118 | 119 | const store = createStoreWithMiddleware(rootReducer); 120 | ``` 121 | 122 | As your action creator, it should return an array of actions. 123 | 124 | ## Scripts 125 | 126 | ``` 127 | $ npm run test 128 | ``` 129 | 130 | ## License 131 | 132 | MIT 133 | 134 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | exports["default"] = function (_ref) { 8 | var dispatch = _ref.dispatch; 9 | var getState = _ref.getState; 10 | return function (next) { 11 | return function (action) { 12 | if (!Array.isArray(action)) { 13 | return next(action); 14 | } 15 | 16 | return action.reduce(function (result, currAction) { 17 | return result.then(function () { 18 | if (!currAction) { 19 | return Promise.resolve(); 20 | } 21 | 22 | return Array.isArray(currAction) ? Promise.all(currAction.map(function (item) { 23 | return dispatch(item); 24 | })) : dispatch(currAction); 25 | }); 26 | }, Promise.resolve()); 27 | }; 28 | }; 29 | }; 30 | 31 | module.exports = exports["default"]; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-sequence-action", 3 | "version": "0.2.1", 4 | "description": "redux sequnence action middleware", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "./node_modules/.bin/mocha test", 8 | "test-watch": "./node_modules/.bin/mocha test -w", 9 | "watch": "NODE_ENV=production ./node_modules/.bin/babel src/ --out-dir lib/ -w", 10 | "build": "NODE_ENV=production ./node_modules/.bin/babel src/ --out-dir lib/" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git@github.com:jasonslyvia/redux-sequence-action.git" 15 | }, 16 | "keywords": [ 17 | "redux", 18 | "redux-middleware", 19 | "middleware", 20 | "flux" 21 | ], 22 | "author": "jasonslyvia (http://undefinedblog.com/)", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/jasonslyvia/redux-sequence-action/issues" 26 | }, 27 | "homepage": "https://github.com/jasonslyvia/redux-sequence-action", 28 | "devDependencies": { 29 | "babel": "~5.8.23", 30 | "chai": "~3.3.0", 31 | "chai-spies": "~0.7.1", 32 | "mocha": "~2.3.3", 33 | "react": "~0.14.0", 34 | "react-redux": "~4.0.0", 35 | "redux": "~3.0.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export default ({dispatch, getState}) => next => action => { 2 | if(!Array.isArray(action)) { 3 | return next(action); 4 | } 5 | 6 | return action.reduce( (result, currAction) => { 7 | return result.then(() => { 8 | if (!currAction) { return Promise.resolve(); } 9 | 10 | return Array.isArray(currAction) ? 11 | Promise.all(currAction.map(item => dispatch(item))) : 12 | dispatch(currAction); 13 | }); 14 | }, Promise.resolve()); 15 | } 16 | -------------------------------------------------------------------------------- /test/spec.js: -------------------------------------------------------------------------------- 1 | require('babel/register'); 2 | var sequenceAction = require('../lib/'); 3 | var chai = require('chai'); 4 | var spies = require('chai-spies'); 5 | chai.use(spies); 6 | var expect = chai.expect; 7 | 8 | // mimic next middleware 9 | function next(action) { 10 | 11 | } 12 | 13 | describe('sequenceAction', function(){ 14 | it('should pass if action is not an array', function(){ 15 | var action = { 16 | type: 'ACTION', 17 | payload: 1 18 | }; 19 | var spy = chai.spy(next); 20 | sequenceAction({})(spy)(action); 21 | 22 | expect(spy).to.have.been.called.with(action); 23 | }); 24 | 25 | it('should let second action get the latest state', function(){ 26 | var mockState = { 27 | a: 1 28 | }; 29 | 30 | var mockDispatch = function(action) { 31 | //mock thunkMiddleware 32 | if(typeof action === 'function') { 33 | return action(); 34 | } 35 | 36 | mockState.a = action.payload; 37 | 38 | return action; 39 | }; 40 | 41 | var mockAction1 = { type: 'SOME_TYPE', payload: 2 }; 42 | 43 | var mockAction2 = function() { 44 | expect(mockState).to.equal({a: 2}); 45 | }; 46 | 47 | var stepAction = [ mockAction1, mockAction2 ]; 48 | 49 | sequenceAction({})(next)(stepAction); 50 | }); 51 | }); 52 | --------------------------------------------------------------------------------