Skip to content

Support namespacing action creators #196

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 43 commits into from
Mar 4, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
cb2c5eb
WIP
yangmillstheory Feb 26, 2017
f4fe4f5
WIP
yangmillstheory Feb 26, 2017
f2eb47c
WIP
yangmillstheory Feb 26, 2017
235a02c
WIP
yangmillstheory Feb 26, 2017
9cfe6dd
WIP
yangmillstheory Feb 26, 2017
b6456b1
WIP
yangmillstheory Feb 26, 2017
b4cc6d2
WIP
yangmillstheory Feb 26, 2017
2e04444
WIP
yangmillstheory Feb 26, 2017
c0824f2
WIP
yangmillstheory Feb 26, 2017
598b44f
WIP
yangmillstheory Feb 26, 2017
fb05634
WIP
yangmillstheory Feb 26, 2017
24aca37
WIP
yangmillstheory Feb 26, 2017
40e3ceb
WIP
yangmillstheory Feb 27, 2017
025a9b0
WIP
yangmillstheory Feb 27, 2017
0689564
WIP
yangmillstheory Feb 27, 2017
ab5d761
WIP
yangmillstheory Feb 27, 2017
2772ad6
WIP
yangmillstheory Feb 27, 2017
8a3c7c1
WIP
yangmillstheory Feb 27, 2017
5a86f94
WIP
yangmillstheory Feb 27, 2017
c9dd5b4
WIP
yangmillstheory Feb 27, 2017
c0e83e7
WIP
yangmillstheory Feb 27, 2017
5af1874
WIP
yangmillstheory Feb 27, 2017
c7d0387
WIP
yangmillstheory Feb 27, 2017
82b13e4
WIP
yangmillstheory Feb 27, 2017
9fee0c4
WIP
yangmillstheory Feb 27, 2017
14893bd
formatting
yangmillstheory Feb 27, 2017
ef31110
naming
yangmillstheory Feb 27, 2017
8401372
clean
yangmillstheory Feb 27, 2017
f6abe48
Update README.md
yangmillstheory Feb 27, 2017
b5c2343
clean
yangmillstheory Mar 1, 2017
5717271
clean
yangmillstheory Mar 1, 2017
f7aed81
future-proof
yangmillstheory Mar 1, 2017
732ca37
clean
yangmillstheory Mar 1, 2017
4fe59e2
naming
yangmillstheory Mar 1, 2017
ea18e26
refactor
yangmillstheory Mar 1, 2017
6b0ea01
clean
yangmillstheory Mar 1, 2017
0d2963b
clean
yangmillstheory Mar 1, 2017
5458e8c
clean
yangmillstheory Mar 1, 2017
38b2b36
clean
yangmillstheory Mar 1, 2017
8cd88dc
clean
yangmillstheory Mar 1, 2017
f02ba4a
clean
yangmillstheory Mar 1, 2017
ce3e3d3
hint
yangmillstheory Mar 1, 2017
49c3a09
WIP
yangmillstheory Mar 2, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 45 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ If you don’t use [npm](https://www.npmjs.com), you may grab the latest [UMD](h
import { createAction } from 'redux-actions';
```

Wraps an action creator so that its return value is the payload of a Flux Standard Action.
Wraps an action creator so that its return value is the payload of a Flux Standard Action.

`payloadCreator` must be a function, `undefined`, or `null`. If `payloadCreator` is `undefined` or `null`, the identity function is used.

Expand Down Expand Up @@ -89,22 +89,24 @@ createAction('ADD_TODO')('Use Redux');

`metaCreator` is an optional function that creates metadata for the payload. It receives the same arguments as the payload creator, but its result becomes the meta field of the resulting action. If `metaCreator` is undefined or not a function, the meta field is omitted.

### `createActions(?actionsMap, ?...identityActions)`
### `createActions(?actionMap, ?...identityActions)`

```js
import { createActions } from 'redux-actions';
```

Returns an object mapping action types to action creators. The keys of this object are camel-cased from the keys in `actionsMap` and the string literals of `identityActions`; the values are the action creators.
Returns an object mapping action types to action creators. The keys of this object are camel-cased from the keys in `actionMap` and the string literals of `identityActions`; the values are the action creators.

`actionsMap` is an optional object with action types as keys, and whose values **must** be either
`actionMap` is an optional object and a recursive data structure, with action types as keys, and whose values **must** be either

- a function, which is the payload creator for that action
- an array with `payload` and `meta` functions in that order, as in [`createAction`](#createactiontype-payloadcreator--identity-metacreator)
- `meta` is **required** in this case (otherwise use the function form above)
- an `actionMap`

`identityActions` is an optional list of positional string arguments that are action type strings; these action types will use the identity payload creator.


```js
const { actionOne, actionTwo, actionThree } = createActions({
// function form; payload creator defined inline
Expand Down Expand Up @@ -136,6 +138,42 @@ expect(actionThree(3)).to.deep.equal({
});
```

If `actionMap` has a recursive structure, its leaves are used as payload and meta creators, and the action type for each leaf is the combined path to that leaf:

```js
const actionCreators = createActions({
APP: {
COUNTER: {
INCREMENT: [
amount => ({ amount }),
amount => ({ key: 'value', amount })
],
DECREMENT: amount => ({ amount: -amount })
},
NOTIFY: [
(username, message) => ({ message: `${username}: ${message}` }),
(username, message) => ({ username, message })
]
}
});

expect(actionCreators.app.counter.increment(1)).to.deep.equal({
type: 'APP/COUNTER/INCREMENT',
payload: { amount: 1 },
meta: { key: 'value', amount: 1 }
});
expect(actionCreators.app.counter.decrement(1)).to.deep.equal({
type: 'APP/COUNTER/DECREMENT',
payload: { amount: -1 }
});
expect(actionCreators.app.notify('yangmillstheory', 'Hello World')).to.deep.equal({
type: 'APP/NOTIFY',
payload: { message: 'yangmillstheory: Hello World' },
meta: { username: 'yangmillstheory', message: 'Hello World' }
});
```
When using this form, you can pass an object with key `namespace` as the last positional argument, instead of the default `/`.

### `handleAction(type, reducer | reducerMap = Identity, defaultState)`

```js
Expand All @@ -155,7 +193,7 @@ handleAction('FETCH_DATA', {
}, defaultState);
```

If either `next()` or `throw()` are `undefined` or `null`, then the identity function is used for that reducer.
If either `next()` or `throw()` are `undefined` or `null`, then the identity function is used for that reducer.

If the reducer argument (`reducer | reducerMap`) is `undefined`, then the identity function is used.

Expand Down Expand Up @@ -187,9 +225,9 @@ const reducer = handleActions({
}, { counter: 0 });
```

### `combineActions(...actionTypes)`
### `combineActions(...types)`

Combine any number of action types or action creators. `actionTypes` is a list of positional arguments which can be action type strings, symbols, or action creators.
Combine any number of action types or action creators. `types` is a list of positional arguments which can be action type strings, symbols, or action creators.

This allows you to reduce multiple distinct actions with the same reducer.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "redux-actions",
"version": "1.2.2",
"version": "2.0.0",
"description": "Flux Standard Action utlities for Redux",
"main": "lib/index.js",
"module": "es/index.js",
Expand Down
9 changes: 3 additions & 6 deletions src/__tests__/combineActions-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,9 @@ describe('combineActions', () => {
it('should accept action creators and action type strings', () => {
const { action1, action2 } = createActions('ACTION_1', 'ACTION_2');

expect(() => combineActions('ACTION_1', 'ACTION_2'))
.not.to.throw(Error);
expect(() => combineActions(action1, action2))
.not.to.throw(Error);
expect(() => combineActions(action1, action2, 'ACTION_3'))
.not.to.throw(Error);
expect(() => combineActions('ACTION_1', 'ACTION_2')).not.to.throw(Error);
expect(() => combineActions(action1, action2)).not.to.throw(Error);
expect(() => combineActions(action1, action2, 'ACTION_3')).not.to.throw(Error);
});

it('should return a stringifiable object', () => {
Expand Down
119 changes: 108 additions & 11 deletions src/__tests__/createActions-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,6 @@ describe('createActions', () => {
});

it('should throw an error when given bad payload creators', () => {
expect(
() => createActions({ ACTION_1: {} })
).to.throw(
Error,
'Expected function, undefined, or array with payload and meta functions for ACTION_1'
);

expect(
() => createActions({
ACTION_1: () => {},
Expand Down Expand Up @@ -106,16 +99,16 @@ describe('createActions', () => {
});

it('should honor special delimiters in action types', () => {
const { 'p/actionOne': pActionOne, 'q/actionTwo': qActionTwo } = createActions({
const { p: { actionOne }, q: { actionTwo } } = createActions({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes this technically a breaking change, so we need a major version bump.

'P/ACTION_ONE': (key, value) => ({ [key]: value }),
'Q/ACTION_TWO': (first, second) => ([first, second])
});

expect(pActionOne('value', 1)).to.deep.equal({
expect(actionOne('value', 1)).to.deep.equal({
type: 'P/ACTION_ONE',
payload: { value: 1 }
});
expect(qActionTwo('value', 2)).to.deep.equal({
expect(actionTwo('value', 2)).to.deep.equal({
type: 'Q/ACTION_TWO',
payload: ['value', 2]
});
Expand Down Expand Up @@ -185,7 +178,7 @@ describe('createActions', () => {
});
});

it('should create actions from an actions map and action types', () => {
it('should create actions from an action map and action types', () => {
const { action1, action2, action3, action4 } = createActions({
ACTION_1: (key, value) => ({ [key]: value }),
ACTION_2: [
Expand All @@ -212,4 +205,108 @@ describe('createActions', () => {
payload: 4
});
});

it('should create actions from a namespaced action map', () => {
const actionCreators = createActions({
APP: {
COUNTER: {
INCREMENT: amount => ({ amount }),
DECREMENT: amount => ({ amount: -amount })
},
NOTIFY: (username, message) => ({ message: `${username}: ${message}` })
},
LOGIN: username => ({ username })
}, 'ACTION_ONE', 'ACTION_TWO');

expect(actionCreators.app.counter.increment(1)).to.deep.equal({
type: 'APP/COUNTER/INCREMENT',
payload: { amount: 1 }
});
expect(actionCreators.app.counter.decrement(1)).to.deep.equal({
type: 'APP/COUNTER/DECREMENT',
payload: { amount: -1 }
});
expect(actionCreators.app.notify('yangmillstheory', 'Hello World')).to.deep.equal({
type: 'APP/NOTIFY',
payload: { message: 'yangmillstheory: Hello World' }
});
expect(actionCreators.login('yangmillstheory')).to.deep.equal({
type: 'LOGIN',
payload: { username: 'yangmillstheory' }
});
expect(actionCreators.actionOne('one')).to.deep.equal({
type: 'ACTION_ONE',
payload: 'one'
});
expect(actionCreators.actionTwo('two')).to.deep.equal({
type: 'ACTION_TWO',
payload: 'two'
});
});

it('should create namespaced actions with payload creators in array form', () => {
const actionCreators = createActions({
APP: {
COUNTER: {
INCREMENT: [
amount => ({ amount }),
amount => ({ key: 'value', amount })
],
DECREMENT: amount => ({ amount: -amount })
},
NOTIFY: [
(username, message) => ({ message: `${username}: ${message}` }),
(username, message) => ({ username, message })
]
}
});

expect(actionCreators.app.counter.increment(1)).to.deep.equal({
type: 'APP/COUNTER/INCREMENT',
payload: { amount: 1 },
meta: { key: 'value', amount: 1 }
});
expect(actionCreators.app.counter.decrement(1)).to.deep.equal({
type: 'APP/COUNTER/DECREMENT',
payload: { amount: -1 }
});
expect(actionCreators.app.notify('yangmillstheory', 'Hello World')).to.deep.equal({
type: 'APP/NOTIFY',
payload: { message: 'yangmillstheory: Hello World' },
meta: { username: 'yangmillstheory', message: 'Hello World' }
});
});

it('should create namespaced actions with a chosen namespace string', () => {
const actionCreators = createActions({
APP: {
COUNTER: {
INCREMENT: [
amount => ({ amount }),
amount => ({ key: 'value', amount })
],
DECREMENT: amount => ({ amount: -amount })
},
NOTIFY: [
(username, message) => ({ message: `${username}: ${message}` }),
(username, message) => ({ username, message })
]
}
}, { namespace: '--' });

expect(actionCreators.app.counter.increment(1)).to.deep.equal({
type: 'APP--COUNTER--INCREMENT',
payload: { amount: 1 },
meta: { key: 'value', amount: 1 }
});
expect(actionCreators.app.counter.decrement(1)).to.deep.equal({
type: 'APP--COUNTER--DECREMENT',
payload: { amount: -1 }
});
expect(actionCreators.app.notify('yangmillstheory', 'Hello World')).to.deep.equal({
type: 'APP--NOTIFY',
payload: { message: 'yangmillstheory: Hello World' },
meta: { username: 'yangmillstheory', message: 'Hello World' }
});
});
});
52 changes: 52 additions & 0 deletions src/__tests__/handleActions-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,56 @@ describe('handleActions', () => {
counter: 7
});
});

it('should work with namespaced actions', () => {
const {
app: {
counter: {
increment,
decrement
},
notify
}
} = createActions({
APP: {
COUNTER: {
INCREMENT: [
amount => ({ amount }),
amount => ({ key: 'value', amount })
],
DECREMENT: amount => ({ amount: -amount })
},
NOTIFY: [
(username, message) => ({ message: `${username}: ${message}` }),
(username, message) => ({ username, message })
]
}
});

// note: we should be using combineReducers in production, but this is just a test
const reducer = handleActions({
[combineActions(increment, decrement)]: ({ counter, message }, { payload: { amount } }) => ({
counter: counter + amount,
message
}),

[notify]: ({ counter, message }, { payload }) => ({
counter,
message: `${message}---${payload.message}`
})
}, { counter: 0, message: '' });

expect(reducer({ counter: 3, message: 'hello' }, increment(2))).to.deep.equal({
counter: 5,
message: 'hello'
});
expect(reducer({ counter: 10, message: 'hello' }, decrement(3))).to.deep.equal({
counter: 7,
message: 'hello'
});
expect(reducer({ counter: 10, message: 'hello' }, notify('me', 'goodbye'))).to.deep.equal({
counter: 10,
message: 'hello---me: goodbye'
});
});
});
Loading