├── .babelrc ├── .eslintrc ├── .gitignore ├── .mocharc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── package.json ├── scripts ├── build.sh ├── bump-package-version.js └── release.sh └── src ├── __tests__ ├── asyncDispatch.js ├── index.js ├── setup.js └── splitAsyncKeys.js ├── asyncDispatch.js ├── index.js ├── splitAsyncKeys.js └── version.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": ["transform-object-rest-spread"] 4 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "algolia", 3 | "globals": { 4 | "describe": true, 5 | "context": true, 6 | "it": true, 7 | "beforeEach": true, 8 | "afterEach": true, 9 | "expect": true, 10 | "sinon": true, 11 | "after": true, 12 | "before": true 13 | } 14 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | npm-debug.log 4 | docs/.sass-cache/ 5 | docs/.webpack/ 6 | .DS_Store 7 | .coverage/ -------------------------------------------------------------------------------- /.mocharc: -------------------------------------------------------------------------------- 1 | --require source-map-support/register 2 | --require babel-register 3 | --require ./src/__tests__/setup.js 4 | --reporter dot 5 | --colors -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "5" -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # 0.1.0 (2016-07-11) 3 | 4 | 5 | 6 | 7 | ## [0.1.1](https://github.com/algolia/redux-updeep/compare/v0.1.0...v0.1.1) (2016-07-11) 8 | 9 | 10 | 11 | 12 | # 0.1.0 (2016-07-11) 13 | 14 | 15 | 16 | 17 | ## [0.1.2](https://github.com/algolia/eventual-values/compare/v0.1.1...v0.1.2) (2016-07-07) 18 | 19 | 20 | 21 | 22 | ## [0.1.1](https://github.com/algolia/eventual-values/compare/v0.1.0...v0.1.1) (2016-07-07) 23 | 24 | 25 | 26 | 27 | # 0.1.0 (2016-07-07) 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Algolia 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redux-magic-async-middleware 2 | 3 | `redux-magic-async-middleware` is a middleware which makes it easy to handle asynchronous data with redux. Combine it with [redux-updeep] for increased awesomeness and reduced boilerplate ! 4 | 5 | [![Version][version-svg]][package-url] [![Build Status][travis-svg]][travis-url] [![License][license-image]][license-url] 6 | 7 | 8 | ## Installation 9 | 10 | `redux-magic-async-middleware` is available on npm. You'll need [redux-updeep] to take full advantage of it. 11 | 12 | ```bash 13 | npm install -S redux-magic-async-middleware redux-updeep 14 | ``` 15 | 16 | ## Getting started 17 | 18 | ### 1. Add as a middleware 19 | 20 | Just add it as you would add any middleware 21 | 22 | ```js 23 | import {createStore, applyMiddleware, combineReducers} from 'redux'; 24 | import magicAsyncMiddleware from 'redux-magic-async-middleware'; 25 | 26 | let combinedReducers = combineReducers(...); 27 | let otherMiddlewares = [...]; 28 | 29 | let store = createStore( 30 | combinedReducers, 31 | applyMiddleware( 32 | magicAsyncMiddleware, 33 | ...otherMiddlewares 34 | ) 35 | ); 36 | ``` 37 | 38 | ### 2. Dispatch promises in your action payload as if they were synchronous values 39 | 40 | Remember how [redux-updeep] lets you easily dispatch actions and merges the payload automatically in your state ? Well, using `redux-magic-async-middleware` you can do the same with promises. While they are loading, they are represented by [eventual values](https://github.com/algolia/eventual-values), so you can check their status easily. Once they are resolved the data will automatically be updated. 41 | 42 | ```js 43 | const initialState = { 44 | user: undefined, 45 | projectList: undefined 46 | }; 47 | 48 | export default createReducer('USER', initialState); 49 | 50 | export function loadUserData() { 51 | return { 52 | type: 'USER/LOAD_USER_DATA', 53 | payload: { 54 | user: fetch('/user_data'), 55 | projectList: fetch('/projectList').then(data => data.projects) 56 | } 57 | }; 58 | } 59 | ``` 60 | 61 | _Note that the middleware will only look for promises in the first level of the payload. If you need more, use a [path](#using-a-path-to-merge-asynchrounous-data-deeply-in-your-reducer-state)_ 62 | 63 | ### 3. Use the data in your components 64 | 65 | ```js 66 | import React from 'react'; 67 | import {connect} from 'react-redux' 68 | import {isReady} from 'eventual-values'; 69 | 70 | function MyComponent({user}) { 71 | if (isReady(user)) { 72 | return

Hello {user.name}

; 73 | } 74 | 75 | return Loading; 76 | } 77 | 78 | export default connect(({myReducer: {user}}) => ({user}))(MyComponent); 79 | ``` 80 | 81 | ## Advanced usage 82 | 83 | ### Using a path to merge asynchronous data deeply in your reducer state 84 | 85 | As mentioned the middleware will only look for promises at the first level in the payload. If the asynchronous data needs to be updated deeper into the reducer state, use a [path](https://github.com/algolia/redux-updeep#specifying-a-path). 86 | 87 | ```js 88 | const initialState = { 89 | user: undefined, 90 | projectList: undefined 91 | }; 92 | 93 | export default createReducer('USER', initialState); 94 | 95 | export function loadName() { 96 | return { 97 | type: 'USER/LOAD_NAME', 98 | payload: { 99 | name: fetch('/user_name') 100 | }, 101 | path: ['user'] 102 | }; 103 | } 104 | ``` 105 | 106 | ### Refreshing data & loading state 107 | 108 | By default, the middleware will only mark values as loading if they were `undefined` when the actions were dispatched. 109 | 110 | This means that if a key on the reducer's state has a value, and if then a promised is dispatched at this key, then nothing will happen to the reducer state until the promise is resolved or rejected. 111 | 112 | It is possible to override this behaviour and reset the value to a loading eventual value at every dispatch by using the overrideStatus key in the action: 113 | 114 | ```js 115 | const initialState = { 116 | user: undefined, 117 | projectList: undefined 118 | }; 119 | 120 | export default createReducer('USER', initialState); 121 | 122 | export function loadUserData() { 123 | return { 124 | type: 'USER/LOAD_USER_DATA', 125 | overrideStatus: true, 126 | payload: { 127 | user: fetch('/user_data') 128 | } 129 | }; 130 | } 131 | ``` 132 | 133 | 134 | [version-svg]: https://img.shields.io/npm/v/redux-magic-async-middleware.svg?style=flat-square 135 | [package-url]: https://npmjs.org/package/redux-magic-async-middleware 136 | [travis-svg]: https://img.shields.io/travis/algolia/redux-magic-async-middleware/master.svg?style=flat-square 137 | [travis-url]: https://travis-ci.org/algolia/redux-magic-async-middleware 138 | [license-image]: http://img.shields.io/badge/license-MIT-green.svg?style=flat-square 139 | [license-url]: LICENSE 140 | [downloads-image]: https://img.shields.io/npm/dm/redux-matic-async-middleware.svg?style=flat-square 141 | [downloads-url]: http://npm-stat.com/charts.html?package=redux-magic-async-middleware 142 | [redux-updeep]: https://github.com/algolia/redux-updeep -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/index.js').default; 2 | module.exports.version = require('./src/version.js'); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-magic-async-middleware", 3 | "version": "0.1.0", 4 | "description": "redux-magic-async-middleware is a reducer generator that uses magic-async-middleware to handle state updates", 5 | "main": "index.js", 6 | "repository": "https://github.com/algolia/redux-magic-async-middleware", 7 | "scripts": { 8 | "build": "LIBRARY_NAME=redux-magic-async-middleware ./scripts/build.sh", 9 | "release": "./scripts/release.sh", 10 | "test": "mocha --opts .mocharc ./src/**/__tests__/*.js" 11 | }, 12 | "author": "Alexandre Meunier ", 13 | "license": "MIT", 14 | "dependencies": { 15 | "eventual-values": "^0.1.2", 16 | "lodash": "^4.13.1", 17 | "updeep": "^0.16.1" 18 | }, 19 | "peerDependencies": { 20 | "redux-updeep": "^0.1.1" 21 | }, 22 | "devDependencies": { 23 | "babel-cli": "^6.10.1", 24 | "babel-plugin-transform-object-rest-spread": "^6.8.0", 25 | "babel-preset-es2015": "^6.9.0", 26 | "babel-register": "^6.9.0", 27 | "chai": "^3.5.0", 28 | "chai-as-promised": "^5.3.0", 29 | "conventional-changelog": "^1.1.0", 30 | "conventional-changelog-cli": "^1.2.0", 31 | "json": "^9.0.4", 32 | "mocha": "^2.5.3", 33 | "mversion": "^1.10.1", 34 | "promise-defer": "^1.0.0", 35 | "semver": "^5.2.0", 36 | "sinon": "^1.17.4", 37 | "sinon-chai": "^2.8.0", 38 | "source-map-support": "^0.4.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e # exit when error, no verbose 4 | 5 | printf "\nBuilding ${LIBRARY_NAME} library\n" 6 | 7 | dist_dir="dist" 8 | 9 | mkdir -p "${dist_dir}" 10 | rm -rf "${dist_dir:?}"/* 11 | 12 | BABEL_DISABLE_CACHE=1 BABEL_ENV=npm babel -q index.js -o "$dist_dir/index.js" 13 | BABEL_DISABLE_CACHE=1 BABEL_ENV=npm babel -q src/ --out-dir "$dist_dir/src/" --ignore __tests__ -------------------------------------------------------------------------------- /scripts/bump-package-version.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console:0 max-len:0 */ 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | import mversion from 'mversion'; 6 | 7 | import semver from 'semver'; 8 | import currentVersion from '../src/version.js'; 9 | 10 | if (!process.env.VERSION) { 11 | throw new Error('bump: Usage is VERSION=MAJOR.MINOR.PATCH scripts/bump-package-version.js'); 12 | } 13 | let newVersion = process.env.VERSION; 14 | 15 | if (!semver.valid(newVersion)) { 16 | throw new Error('bump: Provided new version (' + newVersion + ') is not a valid version per semver'); 17 | } 18 | 19 | if (semver.gte(currentVersion, newVersion)) { 20 | throw new Error('bump: Provided new version is not higher than current version (' + newVersion + ' <= ' + currentVersion + ')'); 21 | } 22 | 23 | console.log('Bumping ' + newVersion); 24 | 25 | console.log('..Updating src/version.js'); 26 | 27 | let versionFile = path.join(__dirname, '../src/version.js'); 28 | let newContent = "export default '" + newVersion + "';\n"; 29 | fs.writeFileSync(versionFile, newContent); 30 | 31 | console.log('..Updating package.json'); 32 | 33 | mversion.update(newVersion); -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e # exit when error 4 | 5 | printf "\nReleasing\n" 6 | 7 | if ! npm owner ls | grep -q "$(npm whoami)" 8 | then 9 | printf "Release: Not an owner of the npm repo, ask a contributor for access" 10 | exit 1 11 | fi 12 | 13 | currentBranch=`git rev-parse --abbrev-ref HEAD` 14 | if [ $currentBranch != 'master' ]; then 15 | printf "Release: You must be on master" 16 | exit 1 17 | fi 18 | 19 | if [[ -n $(git status --porcelain) ]]; then 20 | printf "Release: Working tree is not clean (git status)" 21 | exit 1 22 | fi 23 | 24 | printf "\n\nRelease: update working tree" 25 | git pull origin master 26 | git fetch origin --tags 27 | 28 | printf "Release: npm install" 29 | npm install 30 | 31 | currentVersion=`cat package.json | json version` 32 | 33 | # header 34 | printf "\n\nRelease: current version is %s" $currentVersion 35 | printf "\nRelease: a changelog will be generated only if a fix/feat/performance/breaking token is found in git log" 36 | printf "\nRelease: you must choose a new ve.rs.ion (semver)" 37 | printf "\nRelease: press q to exit the next screen\n\n" 38 | 39 | # preview changelog 40 | read -p "=> Release: press [ENTER] to view changes since latest version.." 41 | 42 | conventional-changelog --preset angular --output-unreleased | less 43 | 44 | # choose and bump new version 45 | # printf "\n\nRelease: Please enter the new chosen version > " 46 | printf "\n=> Release: please type the new chosen version > " 47 | read -e newVersion 48 | VERSION=$newVersion babel-node ./scripts/bump-package-version.js 49 | 50 | # build new version 51 | NODE_ENV=production VERSION=$newVersion npm run build 52 | 53 | # update changelog 54 | printf "\n\nRelease: update changelog" 55 | changelog=`conventional-changelog -p angular` 56 | conventional-changelog --preset angular --infile CHANGELOG.md --same-file 57 | 58 | # git add and tag 59 | commitMessage="v$newVersion\n\n$changelog" 60 | git add src/version.js package.json CHANGELOG.md README.md 61 | printf "%s" "$commitMessage" | git commit --file - 62 | git tag "v$newVersion" 63 | 64 | printf "\n\nRelease: almost done, check everything in another terminal tab.\n" 65 | read -p "=> Release: when ready, press [ENTER] to push to github and publish the package" 66 | 67 | printf "\n\nRelease: push to github, publish on npm\n" 68 | git push origin master 69 | git push origin --tags 70 | 71 | # We are gonna publish the package to npm, in a way 72 | # where only the dist cdn and npm are available 73 | cp package.json README.md LICENSE dist/ 74 | mkdir dist/dist 75 | cd dist 76 | npm publish 77 | rm package.json README.md LICENSE 78 | cd .. 79 | 80 | printf "\n\nRelease: 81 | Package was published to npm. 82 | A job on travis-ci will be automatically launched to finalize the release." 83 | -------------------------------------------------------------------------------- /src/__tests__/asyncDispatch.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import updeep from 'updeep'; 3 | import deferred from 'promise-defer'; 4 | 5 | import asyncDispatch from '../asyncDispatch'; 6 | import {isPending, isReady} from 'eventual-values'; 7 | 8 | describe('modux/asyncDispatch', () => { 9 | var dispatch; 10 | const type = 'NAME_SPACE/COMMIT'; 11 | const key = 'test'; 12 | var promise = new Promise(() => {}); 13 | 14 | beforeEach(() => { 15 | dispatch = sinon.spy(); 16 | }); 17 | 18 | it('should return the loading action', () => { 19 | var actual = asyncDispatch(dispatch, type, key, promise); 20 | 21 | expect(actual.type).to.eql('NAME_SPACE/COMMIT'); 22 | expect(actual.payload.test).to.be.a('function'); 23 | expect(isPending(updeep(actual.payload, {}).test)).to.eq(true); 24 | expect(isPending(updeep(actual.payload, {test: 'value'}).test)).to.eq(false); 25 | }); 26 | 27 | context('when overrideStatus is set to true', () => { 28 | it('should return the loading action', () => { 29 | var actual = asyncDispatch(dispatch, type, key, promise, true); 30 | 31 | expect(actual.type).to.eql('NAME_SPACE/COMMIT'); 32 | expect(isPending(updeep(actual.payload, {}).test)).to.eq(true); 33 | expect(isPending(updeep(actual.payload, {test: 'value'}).test)).to.eq(true); 34 | }); 35 | }); 36 | 37 | context('when the promise is resolved', () => { 38 | it('should dispatch the success action', () => { 39 | const def = deferred(); 40 | 41 | asyncDispatch(dispatch, type, key, def.promise); 42 | 43 | def.resolve({name: 'test'}); 44 | 45 | return def.promise.then(() => { 46 | expect(dispatch.args[0][0]).to.eql({ 47 | type: 'NAME_SPACE/COMMIT_SUCCESS', 48 | payload: { 49 | [key]: {name: 'test'} 50 | } 51 | }); 52 | 53 | expect(isReady(dispatch.args[0][0])).to.eql(true); 54 | }); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import deferred from 'promise-defer'; 3 | import omit from 'lodash/omit'; 4 | import updeep from 'updeep'; 5 | 6 | import middleware from '../'; 7 | import {isPending} from 'eventual-values'; 8 | 9 | 10 | describe('modux/magicAsyncMiddleware', () => { 11 | var store; 12 | var next; 13 | var partialMiddleware; 14 | 15 | before(() => { 16 | store = { 17 | dispatch: sinon.spy(), 18 | getState: sinon.spy() 19 | }; 20 | 21 | next = sinon.spy(); 22 | 23 | partialMiddleware = middleware(store)(next); 24 | }); 25 | 26 | describe('when dispatching a non-COMMIT action', () => { 27 | it('should just call next on the action', () => { 28 | const action = { 29 | type: 'NAME_SPACE/UNKNOWN', 30 | payload: { 31 | test: 'true' 32 | } 33 | }; 34 | 35 | partialMiddleware(action); 36 | 37 | expect(next).to.have.been.calledWithExactly(action); 38 | }); 39 | }); 40 | 41 | describe('when dispatching a COMMIT action', () => { 42 | const type = 'NAME_SPACE/COMMIT'; 43 | describe('when there are only sync payload keys', () => { 44 | it('should just call next on the action', () => { 45 | const action = { 46 | type, 47 | payload: { 48 | test: 'true', 49 | test2: ['test3'] 50 | } 51 | }; 52 | 53 | partialMiddleware(action); 54 | 55 | expect(next).to.have.been.calledWithExactly(action); 56 | }); 57 | }); 58 | 59 | describe('when there are async payload keys', () => { 60 | var def1; 61 | var def2; 62 | 63 | before(() => { 64 | def1 = deferred(); 65 | def2 = deferred(); 66 | }); 67 | 68 | it('should call next with the async keys action', () => { 69 | const action = { 70 | type, 71 | path: 'path', 72 | payload: { 73 | test: 'true', 74 | test2: ['test3'], 75 | async1: def1.promise, 76 | async2: def2.promise 77 | } 78 | }; 79 | 80 | partialMiddleware(action); 81 | 82 | const args = next.args[2][0]; 83 | 84 | expect(args.type).to.eql('NAME_SPACE/COMMIT'); 85 | expect(args.path).to.eql('path'); 86 | expect(omit(args.payload, 'async1', 'async2')).to.eql({ 87 | test: 'true', 88 | test2: ['test3'] 89 | }); 90 | 91 | expect(isPending(updeep(args.payload, {}).async1)).to.eq(true); 92 | expect(isPending(updeep(args.payload, {}).async2)).to.eq(true); 93 | }); 94 | 95 | it('should call next on the async key when they resolve', () => { 96 | def1.resolve({newValue: 'true'}); 97 | 98 | return def1.promise.then(() => { 99 | expect(store.dispatch).to.have.been.calledWithExactly({ 100 | type: `${type}_SUCCESS`, 101 | path: 'path', 102 | payload: { 103 | async1: {newValue: 'true'} 104 | } 105 | }); 106 | }); 107 | }); 108 | 109 | it('should call next on the async key when they are rejected', () => { 110 | def2.reject('error message'); 111 | 112 | return def1.promise.catch(() => { 113 | expect(store.dispatch).to.have.been.calledWithExactly({ 114 | type: `${type}_ERROR`, 115 | error: 'error message', 116 | path: 'path', 117 | payload: { 118 | async2Status: 'error' 119 | } 120 | }); 121 | }); 122 | }); 123 | }); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /src/__tests__/setup.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | global.sinon = require('sinon'); 3 | var sinonChai = require('sinon-chai'); 4 | chai.use(sinonChai); 5 | var chaiAsPromised = require('chai-as-promised'); 6 | chai.use(chaiAsPromised); 7 | -------------------------------------------------------------------------------- /src/__tests__/splitAsyncKeys.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import pick from 'lodash/pick'; 3 | import omit from 'lodash/omit'; 4 | 5 | import splitAsyncKeys from '../splitAsyncKeys'; 6 | 7 | describe('modux/splitAsyncKeys', () => { 8 | it('should properly bucket keys', () => { 9 | var data = { 10 | asyncKey: new Promise(() => {}), 11 | syncKey1: 'test', 12 | syncKey2: ['test2', 'test3'], 13 | nullKey: null 14 | }; 15 | 16 | var {syncDiff, asyncDiff} = splitAsyncKeys(data); 17 | 18 | expect(asyncDiff).to.eql(pick(data, 'asyncKey')); 19 | expect(syncDiff).to.eql(omit(data, 'asyncKey')); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/asyncDispatch.js: -------------------------------------------------------------------------------- 1 | import eventual from 'eventual-values'; 2 | import updeep from 'updeep'; 3 | 4 | function isNotReadyOrUndefined(value) { 5 | return value === undefined || !eventual.isReady(value); 6 | } 7 | 8 | function makeAction(type, key, path, status, overrideStatus, payload = null) { 9 | const action = { 10 | type: status === 'pending' ? type : `${type}_${status.toUpperCase()}` 11 | }; 12 | 13 | switch (status) { 14 | case 'success': 15 | action.payload = { 16 | [key]: eventual.resolve(payload) 17 | }; 18 | break; 19 | case 'error': 20 | action.payload = { 21 | [key]: eventual.reject(payload) 22 | }; 23 | 24 | break; 25 | default: 26 | action.payload = { 27 | [key]: overrideStatus ? 28 | eventual() : 29 | updeep.ifElse(isNotReadyOrUndefined, eventual, eventual.resolve) 30 | }; 31 | 32 | break; 33 | } 34 | 35 | if (path !== null) { 36 | action.path = path; 37 | } 38 | 39 | return action; 40 | } 41 | 42 | export default function asyncDispatch( 43 | dispatch, 44 | type, 45 | key, 46 | promise, 47 | overrideStatus = false, 48 | path = null 49 | ) { 50 | promise.then((data) => { 51 | dispatch(makeAction(type, key, path, 'success', overrideStatus, data)); 52 | }).catch((error) => { 53 | dispatch(makeAction(type, key, path, 'error', overrideStatus, error)); 54 | }); 55 | 56 | return makeAction(type, key, path, 'pending', overrideStatus); 57 | } 58 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import splitAsyncKeys from './splitAsyncKeys'; 2 | import asyncDispatch from './asyncDispatch'; 3 | 4 | export default function magicAsyncMiddleware(store) { 5 | return next => action => { 6 | if ( 7 | action.payload && 8 | typeof action.payload === 'object' && 9 | action.payload.promise === undefined && 10 | action.payload.length === undefined 11 | ) { 12 | const {syncDiff, asyncDiff} = splitAsyncKeys(action.payload); 13 | 14 | if (Object.keys(asyncDiff).length > 0) { 15 | const asyncAction = Object.keys(asyncDiff).reduce((memo, key) => { 16 | const promise = asyncDiff[key]; 17 | 18 | const values = asyncDispatch( 19 | store.dispatch, 20 | action.type, 21 | key, 22 | promise, 23 | action.overrideStatus, 24 | action.path 25 | ); 26 | 27 | return { 28 | ...memo, 29 | payload: { 30 | ...memo.payload, 31 | ...values.payload 32 | } 33 | }; 34 | }, { 35 | type: action.type, 36 | path: action.path, 37 | payload: syncDiff 38 | }); 39 | 40 | next(asyncAction); 41 | } else { 42 | next(action); 43 | } 44 | } else { 45 | next(action); 46 | } 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/splitAsyncKeys.js: -------------------------------------------------------------------------------- 1 | export default function splitAsyncKeys(stateDiff) { 2 | if (typeof stateDiff === 'function') { 3 | return {syncDiff: stateDiff, asyncDiff: {}}; 4 | } 5 | 6 | let syncDiff = {}; 7 | let asyncDiff = {}; 8 | 9 | Object.keys(stateDiff).forEach((key) => { 10 | const value = stateDiff[key]; 11 | 12 | if ( 13 | value !== null && 14 | typeof value === 'object' && 15 | typeof value.then === 'function' 16 | ) { 17 | asyncDiff[key] = value; 18 | } else { 19 | syncDiff[key] = value; 20 | } 21 | }); 22 | 23 | return {syncDiff, asyncDiff}; 24 | } 25 | -------------------------------------------------------------------------------- /src/version.js: -------------------------------------------------------------------------------- 1 | export default '0.1.0'; 2 | --------------------------------------------------------------------------------