Skip to content

Commit

Permalink
Support namespacing action creators (#196)
Browse files Browse the repository at this point in the history
  • Loading branch information
yangmillstheory committed Mar 4, 2017
1 parent 69f7be1 commit a4047db
Show file tree
Hide file tree
Showing 12 changed files with 450 additions and 63 deletions.
52 changes: 45 additions & 7 deletions README.md
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
@@ -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
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
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({
'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
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'
});
});
});

0 comments on commit a4047db

Please sign in to comment.