Skip to content

Commit 90cb9be

Browse files
committed
intial commit
0 parents  commit 90cb9be

14 files changed

+438
-0
lines changed

.babelrc

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"stage": 0,
3+
"loose": "all"
4+
}

.editorconfig

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
lib/
2+
node_modules/
3+
coverage/

.npmignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
src
2+
examples
3+
test.js
4+
.babelrc
5+
.npmignore
6+
.travis.yml
7+
oldtest.js
8+
loader.js

.travis.yml

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
language: node_js
2+
node_js:
3+
- "0.12"
4+
- "iojs"
5+
- "4"
6+
- "5"
7+
script: "npm run-script test-travis"
8+
# Send coverage data to Coveralls
9+
after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js"

README.md

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
redux-await
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+
Manage async redux actions sanely
10+
11+
## Install
12+
13+
```js
14+
npm install --save redux-await
15+
```
16+
17+
## Usage
18+
19+
This module exposes a middleware and higher order reducer to take care of async state in a redux app. You'll need to:
20+
21+
1. Apply the middleware:
22+
23+
```js
24+
import { middleware as awaitMiddleware } from 'redux-await';
25+
let createStoreWithMiddleware = applyMiddleware(
26+
awaitMiddleware,
27+
)(createStore);
28+
```
29+
30+
2. Wrap your reducers
31+
32+
```js
33+
const intialState = {
34+
users: [],
35+
}
36+
37+
const reducer = (state = [], action = {}) => {
38+
if (action.type === GET_USERS) {
39+
return { ...state, users: action.payload.users };
40+
}
41+
if (action.type === ADD_USER) {
42+
return { ...state, users: state.users.concat(action.payload.user) };
43+
}
44+
return state;
45+
}
46+
47+
// old code
48+
// export default reducer;
49+
50+
// new code
51+
import { createReducer } from 'redux-await';
52+
export default createReducer(reducer);
53+
```
54+
55+
Note, if you are using `combineReducers` then you need to wrap each reducer that you are combining independently and not the master reducer that `combineReducers` returns
56+
57+
Now your action creators can contain promises:
58+
59+
```js
60+
// old code
61+
//export const getUsers = users => ({
62+
// type: ADD_USER,
63+
// payload: {
64+
// users: users,
65+
// },
66+
//});
67+
//export const addUser = user => ({
68+
// type: ADD_USER,
69+
// payload: {
70+
// user: user,
71+
// },
72+
//});
73+
74+
// new code
75+
import { AWAIT_MARKER } from 'redux-await';
76+
export const getUsers = users => ({
77+
type: ADD_USER,
78+
AWAIT_MARKER,
79+
payload: {
80+
users: api.getUsers(), // returns promise
81+
},
82+
});
83+
export const addUser = user => ({
84+
type: ADD_USER,
85+
AWAIT_MARKER,
86+
payload: {
87+
user: api.generateUser(), // returns promise
88+
},
89+
});
90+
```
91+
92+
Now your containers can hardly need to change at all:
93+
94+
```js
95+
import { getInfo } from 'redux-await'
96+
97+
class Container extends Component {
98+
render() {
99+
const { users, user } = this.props;
100+
101+
// old code
102+
//return <div>
103+
// <MyTable data={users} />
104+
//</div>;
105+
106+
// new code
107+
return <div>
108+
{ getInfo(users).status === 'pending' && <div>Loading...</div> }
109+
{ getInfo(users).status === 'success' && <MyTable data={users} /> }
110+
{ getInfo(users).status === 'failure' && <div>Opps: {getInfo(users).error.message}</div> }
111+
{ getInfo(user).status === 'pending' && <div>Saving new user</div> }
112+
{ getInfo(user).status === 'failure' && <div>There was an error saving</div> }
113+
</div>;
114+
}
115+
}
116+
```
117+
118+
## Advanced Stuff
119+
120+
By default your reducer is called with the type specified in the action only on the success stage, you can listen to pending an fail events too by listening for `getPendingActionType(type)` and `getFailureActionType(type)` types, also your reducer is called every time (pending, success, failure) after the higher order reducer does it's thing, but you can still get the old state as the third parameter (not sure why you would ever need to though)
121+
122+
It's a little weird using the prop of the action creator to inject it's status info into state, but I couldn't really think of a better way to do it (WeakMaps somehow?)
123+
124+
[npm-image]: https://img.shields.io/npm/v/redux-await.svg?style=flat-square
125+
[npm-url]: https://npmjs.org/package/redux-await
126+
[travis-image]: https://img.shields.io/travis/symbiont-io/redux-await.svg?style=flat-square
127+
[travis-url]: https://travis-ci.org/symbiont-io/redux-await
128+
[coveralls-image]: https://img.shields.io/coveralls/kolodny/redux-await.svg?style=flat-square
129+
[coveralls-url]: https://coveralls.io/r/kolodny/redux-await
130+
[downloads-image]: http://img.shields.io/npm/dm/redux-await.svg?style=flat-square
131+
[downloads-url]: https://npmjs.org/package/redux-await

package.json

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"name": "redux-await",
3+
"version": "1.0.0",
4+
"description": "Manage async redux actions sanely",
5+
"main": "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+
"author": "Moshe Kolodny",
14+
"license": "MIT",
15+
"devDependencies": {
16+
"babel": "^5.8.34",
17+
"coveralls": "^2.11.4",
18+
"expect": "^1.13.0",
19+
"istanbul": "^0.4.1",
20+
"mocha": "^2.3.4",
21+
"redux": "^3.0.4"
22+
},
23+
"dependencies": {
24+
"expect": "^1.13.0",
25+
"redux": "^3.0.4",
26+
"babel": "^5.8.34"
27+
},
28+
"repository": {
29+
"type": "git",
30+
"url": "git+https://github.com/kolodny/redux-await.git"
31+
},
32+
"keywords": [
33+
"redux",
34+
"async",
35+
"await"
36+
],
37+
"bugs": {
38+
"url": "https://github.com/kolodny/redux-await/issues"
39+
},
40+
"homepage": "https://github.com/kolodny/redux-await#readme"
41+
}

src/constants.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const AWAIT_MARKER = '@@redux-await/AWAIT_MARKER';
2+
export const AWAIT_META_CONTAINER = '@@redux-await/AWAIT_META_CONTAINER';
3+
export const AWAIT_INFO_CONTAINER = '@@redux-await/AWAIT_INFO_CONTAINER';

src/get-info.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { AWAIT_INFO_CONTAINER } from './constants';
2+
3+
export default object => {
4+
if (!object || !object[AWAIT_INFO_CONTAINER]) {
5+
return { status: 'success', value: object };
6+
}
7+
8+
return object[AWAIT_INFO_CONTAINER];
9+
}

src/index.js

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export { AWAIT_MARKER, AWAIT_META_CONTAINER, AWAIT_INFO_CONTAINER, PENDING, SUCCESS, FAILURE } from './constants';
2+
export { middleware, getPendingActionType, getFailedActionType } from './middleware';
3+
export createReducer from './reducer';
4+
export getInfo from './get-info';

src/middleware.js

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { resolveProps, getNonPromiseProperties, getPromiseKeys } from './utils';
2+
3+
import { AWAIT_MARKER, AWAIT_META_CONTAINER } from './constants';
4+
5+
export const getPendingActionType = type => `${AWAIT_MARKER}/pending/${type}`;
6+
export const getFailedActionType = type => `${AWAIT_MARKER}/fail/${type}`;
7+
8+
export const middleware = ({ dispatch }) => next => action => {
9+
const { payload, type, meta } = action;
10+
11+
if (payload && action.AWAIT_MARKER === AWAIT_MARKER) {
12+
13+
const promiseKeys = getPromiseKeys(payload);
14+
const scalarValues = getNonPromiseProperties(payload);
15+
const pendingMeta = { [AWAIT_META_CONTAINER]: { promiseKeys, scalarValues, status: 'pending' } };
16+
const successMeta = { [AWAIT_META_CONTAINER]: { promiseKeys, scalarValues, status: 'success' } };
17+
const failureMeta = { [AWAIT_META_CONTAINER]: { promiseKeys, scalarValues, status: 'failure' } };
18+
19+
const newAction = { ...action };
20+
newAction.type = undefined;
21+
newAction.payload = undefined;
22+
newAction.AWAIT_MARKER = undefined;
23+
24+
dispatch({
25+
...newAction,
26+
type: getPendingActionType(type),
27+
meta: { ...meta, ...pendingMeta, type },
28+
});
29+
30+
const successCallback = payload => {
31+
dispatch({
32+
...newAction,
33+
type,
34+
payload,
35+
meta: { ...meta, ...successMeta },
36+
});
37+
};
38+
39+
const failureCallback = error => {
40+
dispatch({
41+
...newAction,
42+
type: getFailedActionType(type),
43+
payload: error,
44+
meta: { ...meta, ...failureMeta, type },
45+
});
46+
}
47+
48+
resolveProps(payload).then(successCallback, failureCallback);
49+
50+
} else {
51+
next(action);
52+
}
53+
};

src/reducer.js

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { AWAIT_META_CONTAINER, AWAIT_INFO_CONTAINER } from './constants';
2+
import { getPromiseKeys, objectWithoutProperties } from './utils';
3+
4+
export default reducer => (state, action) => {
5+
debugger;
6+
let nextState;
7+
if (action.meta && action.meta[AWAIT_META_CONTAINER]) {
8+
const awaitMeta = action.meta[AWAIT_META_CONTAINER];
9+
10+
if (awaitMeta.status === 'pending') {
11+
const pendingProperties = {};
12+
awaitMeta.promiseKeys.forEach(prop => pendingProperties[prop] = {
13+
[AWAIT_INFO_CONTAINER]: { status: 'pending', error: null }
14+
});
15+
nextState = { ...state, ...pendingProperties };
16+
17+
} else if (awaitMeta.status === 'success') {
18+
nextState = objectWithoutProperties(state, awaitMeta.promiseKeys);
19+
20+
} else if (awaitMeta.status === 'failure') {
21+
const errorProperties = {};
22+
awaitMeta.promiseKeys.forEach(prop => errorProperties[prop] = {
23+
[AWAIT_INFO_CONTAINER]: { status: 'failure', error: action.payload }
24+
});
25+
nextState = { ...state, ...errorProperties };
26+
}
27+
28+
} else {
29+
nextState = state
30+
}
31+
return reducer(nextState, action, state);
32+
};

src/utils.js

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
export const isPromise = obj => obj && typeof obj.then === 'function';
2+
export const getPromiseKeys = obj => Object.keys(obj).filter(key => isPromise(obj[key]));
3+
4+
export 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+
export 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+
export const objectWithoutProperties = (obj, keys) => {
24+
var target = {};
25+
for (var i in obj) {
26+
if (keys.indexOf(i) >= 0) continue;
27+
28+
/* istanbul ignore if */
29+
if (!Object.prototype.hasOwnProperty.call(obj, i)) continue;
30+
target[i] = obj[i];
31+
}
32+
return target;
33+
}

0 commit comments

Comments
 (0)