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

Lines changed: 45 additions & 7 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 3 additions & 6 deletions
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

Lines changed: 108 additions & 11 deletions
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

Lines changed: 52 additions & 0 deletions
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)