├── .babelrc
├── .editorconfig
├── .gitignore
├── .npmignore
├── .travis.yml
├── README.md
├── package.json
├── src
└── index.js
└── test
└── index.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "stage": 0,
3 | "loose": "all"
4 | }
5 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | lib/
2 | node_modules/
3 | coverage/
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | examples
3 | test
4 | .babelrc
5 | .npmignore
6 | .travis.yml
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "0.12"
4 | - "iojs"
5 | script: "npm run-script test-travis"
6 | # Send coverage data to Coveralls
7 | after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js"
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | redux-async
2 | =============
3 |
4 | [![NPM version][npm-image]][npm-url]
5 | [![Build status][travis-image]][travis-url]
6 | [![Test coverage][coveralls-image]][coveralls-url]
7 | [![Downloads][downloads-image]][downloads-url]
8 |
9 | [RSA](https://github.com/kolodny/redux-standard-action)-compliant actions which resolve when any prop is a promise [middleware](https://github.com/gaearon/redux/blob/master/docs/middleware.md) for Redux.
10 |
11 |
12 | ## Install
13 |
14 | ```js
15 | npm install --save redux-async
16 | ```
17 |
18 | ## Adding as middleware
19 |
20 | ```js
21 | import asyncMiddleware from 'redux-async';
22 | let createStoreWithMiddleware = applyMiddleware(
23 | asyncMiddleware,
24 | )(createStore);
25 | ```
26 |
27 | ## Usage
28 |
29 | ```js
30 | // action-creators.js
31 | export const loadUsersForAdmin = adminId => {
32 | return {
33 | types: [GET_USERS_REQUEST, GET_USERS_SUCCESS, GET_USERS_FAILURE],
34 | payload: {
35 | users: api.getUsersForAdmin(adminId).then(response => response.data.users),
36 | adminId
37 | }
38 | };
39 | }
40 |
41 | // reducers.js
42 | import { createReducer } from 'redux-create-reducer';
43 | import { GET_USERS_REQUEST, GET_USERS_SUCCESS, GET_USERS_FAILURE } from '../constants/actions';
44 |
45 | const initialState = {};
46 |
47 | export default createReducer(initialState, {
48 | [GET_USERS_REQUEST](state, action) {
49 | const { adminId } = action.payload;
50 |
51 | return {
52 | isFetching: true,
53 | adminId // we always have access to all non promise properties
54 | };
55 | },
56 | [GET_USERS_SUCCESS](state, action) {
57 | const { adminId, users } = action.payload;
58 |
59 | return {
60 | isFetching: false,
61 | users, // all promise properties resolved
62 | adminId // we always have access to all non promise properties - same as above
63 | };
64 | },
65 | [GET_USERS_FAILURE](state, action) {
66 | // assert(action.error === true && action.payload instanceof Error);
67 |
68 | // when a property gets rejected then the non promise properties go in the meta object
69 | // assert(action.meta.adminId);
70 |
71 | return {errorMessage: action.payload.message}; // from Error.prototype.message
72 | },
73 | });
74 |
75 |
76 | // smart-container.js
77 | // ... snipped to the middle of the render function
78 |
79 | {
80 | !users ?
81 |
:
82 | (isFetching) ? (
isFetching for {adminId}...) : (
{JSON.stringify(users, null, 2)}
)
83 | }
84 | { errorMessage &&
errorMessage
}
85 |
86 | ```
87 |
88 |
89 | [npm-image]: https://img.shields.io/npm/v/redux-async.svg?style=flat-square
90 | [npm-url]: https://npmjs.org/package/redux-async
91 | [travis-image]: https://img.shields.io/travis/symbiont-io/redux-async.svg?style=flat-square
92 | [travis-url]: https://travis-ci.org/symbiont-io/redux-async
93 | [coveralls-image]: https://img.shields.io/coveralls/kolodny/redux-async.svg?style=flat-square
94 | [coveralls-url]: https://coveralls.io/r/kolodny/redux-async
95 | [downloads-image]: http://img.shields.io/npm/dm/redux-async.svg?style=flat-square
96 | [downloads-url]: https://npmjs.org/package/redux-async
97 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-async",
3 | "version": "2.0.1",
4 | "description": "dispatch async actions in redux",
5 | "main": "./lib/index.js",
6 | "scripts": {
7 | "build": "babel src --out-dir lib",
8 | "prepublish": "npm run test && npm run build",
9 | "test-cov": "node ./node_modules/istanbul/lib/cli.js cover node_modules/mocha/bin/_mocha 'test/**/*.js' -- --reporter dot --require babel/register",
10 | "test-travis": "node ./node_modules/istanbul/lib/cli.js cover ./node_modules/mocha/bin/_mocha 'test/**/*.js' -- -R spec --require babel/register",
11 | "test": "mocha --compilers js:babel/register --recursive"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/kolodny/redux-async.git"
16 | },
17 | "keywords": [
18 | "redux",
19 | "react"
20 | ],
21 | "author": "Moshe Kolodny",
22 | "license": "MIT",
23 | "bugs": {
24 | "url": "https://github.com/kolodny/redux-async/issues"
25 | },
26 | "homepage": "https://github.com/kolodny/redux-async#readme",
27 | "devDependencies": {
28 | "babel": "^5.8.23",
29 | "babel-core": "^5.8.22",
30 | "coveralls": "^2.11.4",
31 | "expect": "^1.9.0",
32 | "istanbul": "^0.3.19",
33 | "mocha": "^2.3.1",
34 | "redux": "^2.0.0"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | const isPromise = obj => obj && typeof obj.then === 'function';
2 | const hasPromiseProps = obj => Object.keys(obj).some(key => isPromise(obj[key]));
3 |
4 | const resolveProps = obj => {
5 | const props = Object.keys(obj);
6 | const values = props.map(prop => obj[prop]);
7 |
8 | return Promise.all(values).then(resolvedArray => {
9 | return props.reduce((acc, prop, index) => {
10 | acc[prop] = resolvedArray[index];
11 | return acc;
12 | }, {});
13 | });
14 | };
15 |
16 | const getNonPromiseProperties = obj => {
17 | return Object.keys(obj).filter(key => !isPromise(obj[key])).reduce((acc, key) => {
18 | acc[key] = obj[key];
19 | return acc;
20 | }, {});
21 | };
22 |
23 |
24 | export default function promisePropsMiddleware() {
25 | return next => action => {
26 | const { types, payload } = action;
27 | if (!types || !hasPromiseProps(action.payload)) {
28 | return next(action);
29 | }
30 |
31 | const nonPromiseProperties = getNonPromiseProperties(payload);
32 |
33 | const [PENDING, RESOLVED, REJECTED] = types;
34 |
35 | const pendingAction = { type: PENDING, payload: nonPromiseProperties };
36 | const successAction = { type: RESOLVED };
37 | const failureAction = { type: REJECTED, error: true, meta: nonPromiseProperties };
38 | if (action.meta) {
39 | [pendingAction, successAction, failureAction].forEach(nextAction => {
40 | nextAction.meta = { ...nextAction.meta, ...action.meta }
41 | });
42 | }
43 |
44 | next(pendingAction);
45 | return resolveProps(payload).then(
46 | results => next({ ...successAction, payload: results }),
47 | error => next({ ...failureAction, payload: error })
48 | );
49 | };
50 | }
51 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | import expect from 'expect';
2 | import { createStore, applyMiddleware } from 'redux';
3 |
4 | import middleware from '../src';
5 |
6 | const getNewStore = (saveAction) => {
7 | const reducer = function(state, action) {
8 | if (saveAction) saveAction.action = action;
9 | return action;
10 | }
11 | const store = applyMiddleware(middleware)(createStore)(reducer);
12 | return store;
13 | };
14 |
15 |
16 | describe('redux-async', () => {
17 |
18 | it('handles a normal case', (done) => {
19 | const randomData = {randomData: Math.random()};
20 | const store = getNewStore();
21 | store.subscribe(() => {
22 | expect(store.getState()).toEqual({type: 'SOMETHING_RESOLVED', payload: randomData});
23 | done();
24 | });
25 | store.dispatch({type: 'SOMETHING_RESOLVED', payload: randomData});
26 | });
27 |
28 | it('handles a resolved promise case', (done) => {
29 | const store = getNewStore();
30 | const thingsToHappen = [
31 | { type: 'SOMETHING_PENDING', payload: { rest: 'ing'} },
32 | { type: 'SOMETHING_RESOLVED', payload: {isOk: true, rest: 'ing'} }
33 | ];
34 | store.subscribe(() => {
35 | const expectedCurrentState = thingsToHappen.shift();
36 | expect(store.getState()).toEqual(expectedCurrentState);
37 | if (!thingsToHappen.length) done();
38 | });
39 | store.dispatch({
40 | types: ['SOMETHING_PENDING', 'SOMETHING_RESOLVED', 'SOMETHING_REJECTED'],
41 | payload: {
42 | isOk: Promise.resolve(true),
43 | rest: 'ing'
44 | }
45 | });
46 | });
47 |
48 | it("doesn't overwrite the meta property", () => {
49 | const saveAction = {};
50 | const store = getNewStore(saveAction);
51 | store.dispatch({
52 | types: ['SOMETHING_PENDING', 'SOMETHING_RESOLVED', 'SOMETHING_REJECTED'],
53 | meta: { so: 'meta' },
54 | payload: {
55 | isOk: Promise.resolve(true),
56 | rest: 'ing'
57 | }
58 | });
59 | expect(saveAction.action.meta).toEqual({so: 'meta'});
60 | });
61 |
62 | it('handles a rejected promise case', (done) => {
63 | const store = getNewStore();
64 | const thingsToHappen = [ { type: 'SOMETHING_PENDING', payload: { rest: 'ing'} }];
65 | store.subscribe(() => {
66 | if (thingsToHappen.length) {
67 | const expectedCurrentState = thingsToHappen.shift();
68 | expect(store.getState()).toEqual(expectedCurrentState);
69 | } else {
70 | const state = store.getState();
71 | expect(state.type).toEqual('SOMETHING_REJECTED');
72 | expect(state.error).toBeTruthy();
73 | expect(state.meta).toEqual({rest: 'ing'});
74 | expect(state.payload).toBeAn(Error);
75 | expect(state.payload.message).toEqual('something went wrong');
76 | done();
77 | }
78 | });
79 | store.dispatch({
80 | types: ['SOMETHING_PENDING', 'SOMETHING_RESOLVED', 'SOMETHING_REJECTED'],
81 | payload: {
82 | isOk: Promise.reject(new Error('something went wrong')),
83 | rest: 'ing',
84 | }
85 | });
86 | });
87 |
88 | it('handles resolved and rejected promises case in the same payload', (done) => {
89 | const store = getNewStore();
90 | const thingsToHappen = [ { type: 'SOMETHING_PENDING', payload: { rest: 'ing'} }];
91 | store.subscribe(() => {
92 | if (thingsToHappen.length) {
93 | const expectedCurrentState = thingsToHappen.shift();
94 | expect(store.getState()).toEqual(expectedCurrentState);
95 | } else {
96 | const state = store.getState();
97 | expect(state.type).toEqual('SOMETHING_REJECTED');
98 | expect(state.error).toBeTruthy();
99 | expect(state.meta).toEqual({rest: 'ing'});
100 | expect(state.payload).toBeAn(Error);
101 | expect(state.payload.message).toEqual('something went wrong');
102 | done();
103 | }
104 | });
105 | store.dispatch({
106 | types: ['SOMETHING_PENDING', 'SOMETHING_RESOLVED', 'SOMETHING_REJECTED'],
107 | payload: {
108 | isOk: Promise.reject(new Error('something went wrong')),
109 | isOk2: Promise.resolve(33),
110 | rest: 'ing',
111 | }
112 | });
113 | });
114 |
115 | });
116 |
--------------------------------------------------------------------------------