Skip to content

Commit a4047db

Browse files
Support namespacing action creators (#196)
1 parent 69f7be1 commit a4047db

12 files changed

+450
-63
lines changed

README.md

+45-7
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ If you don’t use [npm](https://www.npmjs.com), you may grab the latest [UMD](h
2424
import { createAction } from 'redux-actions';
2525
```
2626

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

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

@@ -89,22 +89,24 @@ createAction('ADD_TODO')('Use Redux');
8989

9090
`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.
9191

92-
### `createActions(?actionsMap, ?...identityActions)`
92+
### `createActions(?actionMap, ?...identityActions)`
9393

9494
```js
9595
import { createActions } from 'redux-actions';
9696
```
9797

98-
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.
98+
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.
9999

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

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

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

109+
108110
```js
109111
const { actionOne, actionTwo, actionThree } = createActions({
110112
// function form; payload creator defined inline
@@ -136,6 +138,42 @@ expect(actionThree(3)).to.deep.equal({
136138
});
137139
```
138140

141+
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:
142+
143+
```js
144+
const actionCreators = createActions({
145+
APP: {
146+
COUNTER: {
147+
INCREMENT: [
148+
amount => ({ amount }),
149+
amount => ({ key: 'value', amount })
150+
],
151+
DECREMENT: amount => ({ amount: -amount })
152+
},
153+
NOTIFY: [
154+
(username, message) => ({ message: `${username}: ${message}` }),
155+
(username, message) => ({ username, message })
156+
]
157+
}
158+
});
159+
160+
expect(actionCreators.app.counter.increment(1)).to.deep.equal({
161+
type: 'APP/COUNTER/INCREMENT',
162+
payload: { amount: 1 },
163+
meta: { key: 'value', amount: 1 }
164+
});
165+
expect(actionCreators.app.counter.decrement(1)).to.deep.equal({
166+
type: 'APP/COUNTER/DECREMENT',
167+
payload: { amount: -1 }
168+
});
169+
expect(actionCreators.app.notify('yangmillstheory', 'Hello World')).to.deep.equal({
170+
type: 'APP/NOTIFY',
171+
payload: { message: 'yangmillstheory: Hello World' },
172+
meta: { username: 'yangmillstheory', message: 'Hello World' }
173+
});
174+
```
175+
When using this form, you can pass an object with key `namespace` as the last positional argument, instead of the default `/`.
176+
139177
### `handleAction(type, reducer | reducerMap = Identity, defaultState)`
140178

141179
```js
@@ -155,7 +193,7 @@ handleAction('FETCH_DATA', {
155193
}, defaultState);
156194
```
157195

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

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

@@ -187,9 +225,9 @@ const reducer = handleActions({
187225
}, { counter: 0 });
188226
```
189227

190-
### `combineActions(...actionTypes)`
228+
### `combineActions(...types)`
191229

192-
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.
230+
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.
193231

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

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "redux-actions",
3-
"version": "1.2.2",
3+
"version": "2.0.0",
44
"description": "Flux Standard Action utlities for Redux",
55
"main": "lib/index.js",
66
"module": "es/index.js",

src/__tests__/combineActions-test.js

+3-6
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,9 @@ describe('combineActions', () => {
1313
it('should accept action creators and action type strings', () => {
1414
const { action1, action2 } = createActions('ACTION_1', 'ACTION_2');
1515

16-
expect(() => combineActions('ACTION_1', 'ACTION_2'))
17-
.not.to.throw(Error);
18-
expect(() => combineActions(action1, action2))
19-
.not.to.throw(Error);
20-
expect(() => combineActions(action1, action2, 'ACTION_3'))
21-
.not.to.throw(Error);
16+
expect(() => combineActions('ACTION_1', 'ACTION_2')).not.to.throw(Error);
17+
expect(() => combineActions(action1, action2)).not.to.throw(Error);
18+
expect(() => combineActions(action1, action2, 'ACTION_3')).not.to.throw(Error);
2219
});
2320

2421
it('should return a stringifiable object', () => {

src/__tests__/createActions-test.js

+108-11
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,6 @@ describe('createActions', () => {
1111
});
1212

1313
it('should throw an error when given bad payload creators', () => {
14-
expect(
15-
() => createActions({ ACTION_1: {} })
16-
).to.throw(
17-
Error,
18-
'Expected function, undefined, or array with payload and meta functions for ACTION_1'
19-
);
20-
2114
expect(
2215
() => createActions({
2316
ACTION_1: () => {},
@@ -106,16 +99,16 @@ describe('createActions', () => {
10699
});
107100

108101
it('should honor special delimiters in action types', () => {
109-
const { 'p/actionOne': pActionOne, 'q/actionTwo': qActionTwo } = createActions({
102+
const { p: { actionOne }, q: { actionTwo } } = createActions({
110103
'P/ACTION_ONE': (key, value) => ({ [key]: value }),
111104
'Q/ACTION_TWO': (first, second) => ([first, second])
112105
});
113106

114-
expect(pActionOne('value', 1)).to.deep.equal({
107+
expect(actionOne('value', 1)).to.deep.equal({
115108
type: 'P/ACTION_ONE',
116109
payload: { value: 1 }
117110
});
118-
expect(qActionTwo('value', 2)).to.deep.equal({
111+
expect(actionTwo('value', 2)).to.deep.equal({
119112
type: 'Q/ACTION_TWO',
120113
payload: ['value', 2]
121114
});
@@ -185,7 +178,7 @@ describe('createActions', () => {
185178
});
186179
});
187180

188-
it('should create actions from an actions map and action types', () => {
181+
it('should create actions from an action map and action types', () => {
189182
const { action1, action2, action3, action4 } = createActions({
190183
ACTION_1: (key, value) => ({ [key]: value }),
191184
ACTION_2: [
@@ -212,4 +205,108 @@ describe('createActions', () => {
212205
payload: 4
213206
});
214207
});
208+
209+
it('should create actions from a namespaced action map', () => {
210+
const actionCreators = createActions({
211+
APP: {
212+
COUNTER: {
213+
INCREMENT: amount => ({ amount }),
214+
DECREMENT: amount => ({ amount: -amount })
215+
},
216+
NOTIFY: (username, message) => ({ message: `${username}: ${message}` })
217+
},
218+
LOGIN: username => ({ username })
219+
}, 'ACTION_ONE', 'ACTION_TWO');
220+
221+
expect(actionCreators.app.counter.increment(1)).to.deep.equal({
222+
type: 'APP/COUNTER/INCREMENT',
223+
payload: { amount: 1 }
224+
});
225+
expect(actionCreators.app.counter.decrement(1)).to.deep.equal({
226+
type: 'APP/COUNTER/DECREMENT',
227+
payload: { amount: -1 }
228+
});
229+
expect(actionCreators.app.notify('yangmillstheory', 'Hello World')).to.deep.equal({
230+
type: 'APP/NOTIFY',
231+
payload: { message: 'yangmillstheory: Hello World' }
232+
});
233+
expect(actionCreators.login('yangmillstheory')).to.deep.equal({
234+
type: 'LOGIN',
235+
payload: { username: 'yangmillstheory' }
236+
});
237+
expect(actionCreators.actionOne('one')).to.deep.equal({
238+
type: 'ACTION_ONE',
239+
payload: 'one'
240+
});
241+
expect(actionCreators.actionTwo('two')).to.deep.equal({
242+
type: 'ACTION_TWO',
243+
payload: 'two'
244+
});
245+
});
246+
247+
it('should create namespaced actions with payload creators in array form', () => {
248+
const actionCreators = createActions({
249+
APP: {
250+
COUNTER: {
251+
INCREMENT: [
252+
amount => ({ amount }),
253+
amount => ({ key: 'value', amount })
254+
],
255+
DECREMENT: amount => ({ amount: -amount })
256+
},
257+
NOTIFY: [
258+
(username, message) => ({ message: `${username}: ${message}` }),
259+
(username, message) => ({ username, message })
260+
]
261+
}
262+
});
263+
264+
expect(actionCreators.app.counter.increment(1)).to.deep.equal({
265+
type: 'APP/COUNTER/INCREMENT',
266+
payload: { amount: 1 },
267+
meta: { key: 'value', amount: 1 }
268+
});
269+
expect(actionCreators.app.counter.decrement(1)).to.deep.equal({
270+
type: 'APP/COUNTER/DECREMENT',
271+
payload: { amount: -1 }
272+
});
273+
expect(actionCreators.app.notify('yangmillstheory', 'Hello World')).to.deep.equal({
274+
type: 'APP/NOTIFY',
275+
payload: { message: 'yangmillstheory: Hello World' },
276+
meta: { username: 'yangmillstheory', message: 'Hello World' }
277+
});
278+
});
279+
280+
it('should create namespaced actions with a chosen namespace string', () => {
281+
const actionCreators = createActions({
282+
APP: {
283+
COUNTER: {
284+
INCREMENT: [
285+
amount => ({ amount }),
286+
amount => ({ key: 'value', amount })
287+
],
288+
DECREMENT: amount => ({ amount: -amount })
289+
},
290+
NOTIFY: [
291+
(username, message) => ({ message: `${username}: ${message}` }),
292+
(username, message) => ({ username, message })
293+
]
294+
}
295+
}, { namespace: '--' });
296+
297+
expect(actionCreators.app.counter.increment(1)).to.deep.equal({
298+
type: 'APP--COUNTER--INCREMENT',
299+
payload: { amount: 1 },
300+
meta: { key: 'value', amount: 1 }
301+
});
302+
expect(actionCreators.app.counter.decrement(1)).to.deep.equal({
303+
type: 'APP--COUNTER--DECREMENT',
304+
payload: { amount: -1 }
305+
});
306+
expect(actionCreators.app.notify('yangmillstheory', 'Hello World')).to.deep.equal({
307+
type: 'APP--NOTIFY',
308+
payload: { message: 'yangmillstheory: Hello World' },
309+
meta: { username: 'yangmillstheory', message: 'Hello World' }
310+
});
311+
});
215312
});

src/__tests__/handleActions-test.js

+52
Original file line numberDiff line numberDiff line change
@@ -184,4 +184,56 @@ describe('handleActions', () => {
184184
counter: 7
185185
});
186186
});
187+
188+
it('should work with namespaced actions', () => {
189+
const {
190+
app: {
191+
counter: {
192+
increment,
193+
decrement
194+
},
195+
notify
196+
}
197+
} = createActions({
198+
APP: {
199+
COUNTER: {
200+
INCREMENT: [
201+
amount => ({ amount }),
202+
amount => ({ key: 'value', amount })
203+
],
204+
DECREMENT: amount => ({ amount: -amount })
205+
},
206+
NOTIFY: [
207+
(username, message) => ({ message: `${username}: ${message}` }),
208+
(username, message) => ({ username, message })
209+
]
210+
}
211+
});
212+
213+
// note: we should be using combineReducers in production, but this is just a test
214+
const reducer = handleActions({
215+
[combineActions(increment, decrement)]: ({ counter, message }, { payload: { amount } }) => ({
216+
counter: counter + amount,
217+
message
218+
}),
219+
220+
[notify]: ({ counter, message }, { payload }) => ({
221+
counter,
222+
message: `${message}---${payload.message}`
223+
})
224+
}, { counter: 0, message: '' });
225+
226+
expect(reducer({ counter: 3, message: 'hello' }, increment(2))).to.deep.equal({
227+
counter: 5,
228+
message: 'hello'
229+
});
230+
expect(reducer({ counter: 10, message: 'hello' }, decrement(3))).to.deep.equal({
231+
counter: 7,
232+
message: 'hello'
233+
});
234+
expect(reducer({ counter: 10, message: 'hello' }, notify('me', 'goodbye'))).to.deep.equal({
235+
counter: 10,
236+
message: 'hello---me: goodbye'
237+
});
238+
});
187239
});

0 commit comments

Comments
 (0)