Skip to content

Commit 4e17514

Browse files
committed
Adds the ability to listen to multiple targets
1 parent 65c3d6c commit 4e17514

File tree

7 files changed

+166
-24
lines changed

7 files changed

+166
-24
lines changed

index.d.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -279,12 +279,16 @@ export type Thunk<
279279
result: Result;
280280
};
281281

282-
type ListenToTarget<TargetPayload> =
282+
type Target<TargetPayload> =
283283
| Action<any, TargetPayload>
284284
| Thunk<any, TargetPayload>
285285
| string
286286
| void;
287287

288+
type ListenToTarget<TargetPayload> =
289+
| Target<TargetPayload>
290+
| Array<Target<TargetPayload>>;
291+
288292
/**
289293
* Declares an thunk action type against your model.
290294
*
@@ -459,7 +463,7 @@ type Arguments =
459463
| [any, any, any, any]
460464
| [any, any, any, any, any];
461465

462-
type OptionalArguments = void | Arguments;
466+
type OptionalArguments = [] | Arguments;
463467

464468
/**
465469
* A selector type.
@@ -478,11 +482,11 @@ type OptionalArguments = void | Arguments;
478482
export type Selector<
479483
Model extends Object = {},
480484
Result = any,
481-
Args extends Arguments = any,
482-
RunTimeArgs extends OptionalArguments = any,
485+
ResolvedState extends Arguments = any,
486+
RuntimeArgs extends OptionalArguments = any,
483487
StoreModel extends Object = {}
484488
> = {
485-
(resolvedArgs: Args, runTimeArgs: RunTimeArgs): Result;
489+
(resolvedArgs: ResolvedState, runTimeArgs: RuntimeArgs): Result;
486490
type: 'selector';
487491
result: Result;
488492
};

src/__tests__/listener-actions.test.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,60 @@ it('listening to an string, firing a thunk', done => {
165165
// act
166166
store.dispatch({ type: 'MATH_ADD', payload: 10 });
167167
});
168+
169+
it('action listening to multiple actions', async () => {
170+
// arrange
171+
const actionTarget = action(() => {});
172+
const thunkTarget = thunk(() => {});
173+
const model = {
174+
logs: [],
175+
actionTarget,
176+
thunkTarget,
177+
onActions: action(
178+
(state, payload) => {
179+
state.logs.push(payload);
180+
},
181+
{ listenTo: [actionTarget, thunkTarget] },
182+
),
183+
};
184+
const store = createStore(model);
185+
186+
// act
187+
store.getActions().actionTarget('action payload');
188+
await store.getActions().thunkTarget('thunk payload');
189+
190+
// assert
191+
expect(store.getState().logs).toEqual(['action payload', 'thunk payload']);
192+
});
193+
194+
it('thunk listening to multiple actions', async () => {
195+
// arrange
196+
const thunkSpy = jest.fn();
197+
const actionTarget = action(() => {});
198+
const thunkTarget = thunk(() => {});
199+
const model = {
200+
logs: [],
201+
actionTarget,
202+
thunkTarget,
203+
onActions: thunk(thunkSpy, { listenTo: [actionTarget, thunkTarget] }),
204+
};
205+
const store = createStore(model);
206+
207+
// act
208+
store.getActions().actionTarget('action payload');
209+
await store.getActions().thunkTarget('thunk payload');
210+
211+
// assert
212+
await new Promise(resolve => setTimeout(resolve, 100));
213+
expect(thunkSpy).toHaveBeenCalledTimes(2);
214+
expect(thunkSpy).toHaveBeenCalledWith(
215+
expect.anything(),
216+
'action payload',
217+
expect.anything(),
218+
);
219+
expect(thunkSpy).toHaveBeenCalledWith(
220+
expect.anything(),
221+
'thunk payload',
222+
expect.anything(),
223+
);
224+
});

src/__tests__/typescript/action.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,22 @@ const listeningInvalidFunc: Action<ListeningModel, string> = action(
116116
listenTo: () => undefined,
117117
},
118118
);
119+
120+
const multiListeningAction: Action<ListeningModel, string> = action(
121+
(state, payload) => {
122+
state.logs.push(payload);
123+
},
124+
{
125+
listenTo: [targetModel.doAction, targetModel.doThunk],
126+
},
127+
);
128+
129+
const multiListeningActionInvalid: Action<ListeningModel, string> = action(
130+
(state, payload) => {
131+
state.logs.push(payload);
132+
},
133+
// typings:expect-error
134+
{
135+
listenTo: [targetModel.doAction, targetModel.doThunkInvalid],
136+
},
137+
);

src/__tests__/typescript/selector.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ interface Todo {
77
text: string;
88
}
99

10-
type CountSelector = Selector<TodosModel, number, [Array<Todo>], void>;
10+
type CountSelector = Selector<TodosModel, number, [Array<Todo>]>;
1111

1212
interface TodosModel {
1313
items: Array<Todo>;
@@ -21,7 +21,7 @@ interface StatusModel {
2121
StatusModel,
2222
number,
2323
[SelectorRef<CountSelector>],
24-
void,
24+
[],
2525
StoreModel
2626
>;
2727
}

src/__tests__/typescript/thunk.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,22 @@ const listenInvalidFunc: Thunk<AuditModel, string> = thunk(
139139
listenTo: () => undefined,
140140
},
141141
);
142+
143+
const multiListenAction: Thunk<ListeningModel, string> = thunk(
144+
(actions, payload) => {
145+
actions.log(payload);
146+
},
147+
{
148+
listenTo: [targetModel.doAction, targetModel.doThunk],
149+
},
150+
);
151+
152+
const multiListenActionInvalidThunk: Thunk<ListeningModel, string> = thunk(
153+
(actions, payload) => {
154+
actions.log(payload);
155+
},
156+
// typings:expect-error
157+
{
158+
listenTo: [targetModel.doAction, targetModel.doThunkInvalid],
159+
},
160+
);

src/create-store-internals.js

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -279,22 +279,31 @@ export default function createStoreInternals({
279279
config: { listenTo },
280280
} = listenerAction[actionStateSymbol] || listenerAction[thunkStateSymbol];
281281
let targetName;
282-
if (
283-
typeof listenTo === 'function' &&
284-
listenTo[actionNameSymbol] &&
285-
actionCreatorDict[listenTo[actionNameSymbol]]
286-
) {
287-
if (listenTo[thunkSymbol]) {
288-
targetName = helpers.thunkCompleteName(listenTo);
289-
} else {
290-
targetName = listenTo[actionNameSymbol];
282+
283+
const processListenTo = target => {
284+
if (
285+
typeof target === 'function' &&
286+
target[actionNameSymbol] &&
287+
actionCreatorDict[target[actionNameSymbol]]
288+
) {
289+
if (target[thunkSymbol]) {
290+
targetName = helpers.thunkCompleteName(target);
291+
} else {
292+
targetName = target[actionNameSymbol];
293+
}
294+
} else if (typeof target === 'string') {
295+
targetName = target;
291296
}
292-
} else if (typeof listenTo === 'string') {
293-
targetName = listenTo;
297+
const listenerReg = listenerActionMap[targetName] || [];
298+
listenerReg.push(actionCreatorDict[listenerAction[actionNameSymbol]]);
299+
listenerActionMap[targetName] = listenerReg;
300+
};
301+
302+
if (Array.isArray(listenTo)) {
303+
listenTo.forEach(processListenTo);
304+
} else {
305+
processListenTo(listenTo);
294306
}
295-
const listenerReg = listenerActionMap[targetName] || [];
296-
listenerReg.push(actionCreatorDict[listenerAction[actionNameSymbol]]);
297-
listenerActionMap[targetName] = listenerReg;
298307
});
299308

300309
selectorReducers.forEach(selector => {

website/docs/docs/api/action.md

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ action((state, payload) => {
2727
Additional configuration for the [action](/docs/api/action). It current supports the following
2828
properties:
2929

30-
- `listenTo` ([action](/docs/api/action) reference | [thunk](/docs/api/thunk) reference | string, *optional*)
30+
- `listenTo` ([action](/docs/api/action) reference | [thunk](/docs/api/thunk) reference | string | Array, *optional*)
3131

32-
Setting this makes your [action](/docs/api/action) *listen* to provided *target* [action](/docs/api/action), [thunk](/docs/api/thunk), or string named action. Any time the *target* is successfully processed then this [action](/docs/api/action) will be fired.
32+
Setting this makes your [action](/docs/api/action) *listen* to provided *target(s)* [action(s)](/docs/api/action), [thunk(s)](/docs/api/thunk), or string named action(s). Any time the *target(s)* is successfully processed then this [action](/docs/api/action) will be fired.
3333

34-
The *listener* will receive the same payload as was supplied to the *target*.
34+
The *listener* will receive the same payload as was supplied to the *target(s)*.
3535

3636
```javascript
3737
const auditModel = {
@@ -100,6 +100,40 @@ In the example above note that the `onAddTodo` [action](/docs/api/action) has be
100100

101101
Any time the `addTodo` [action](/docs/api/action) completes successfully, the `onAddTodo` will be fired, receiving the same payload as what `addTodo` received.
102102

103+
## Listening to multiple actions
104+
105+
It is possible for a *listening* action to listen to multiple *targets*. Simply provide an array of *targets* against the `listenTo` configuration.
106+
107+
```javascript
108+
const fooModel = {
109+
items: [],
110+
// 👇 the first target action
111+
firstAction: action((state, payload) => {
112+
state.items.push(payload);
113+
}),
114+
// 👇 the second target action
115+
secondAction action((state, payload) => {
116+
state.items.push(payload);
117+
}),
118+
};
119+
120+
const auditModel = {
121+
logs: [],
122+
onAddTodo: action(
123+
(state, payload) => {
124+
state.logs.push(payload);
125+
},
126+
{
127+
// 👇 declare the targets within an array
128+
listenTo: [
129+
fooModel.firstAction,
130+
fooModel.secondAction
131+
]
132+
}
133+
)
134+
};
135+
```
136+
103137
## Using console.log within actions
104138

105139
Despite the Redux Dev Tools extension being available there may be cases in which you would like to perform a `console.log` within the body of your [actions](/docs/api/action) to aid debugging.

0 commit comments

Comments
 (0)