Skip to content

Commit 2f2c4d2

Browse files
committed
Remove stopPropagation, clarify phase names, add "both" option
1 parent 5f97ff4 commit 2f2c4d2

File tree

2 files changed

+27
-142
lines changed

2 files changed

+27
-142
lines changed

packages/action-listener-middleware/src/index.ts

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ export type WithMiddlewareType<T extends Middleware<any, any, any>> = {
4545
[declaredMiddlewareType]: T
4646
}
4747

48-
export type When = 'before' | 'after' | undefined
48+
export type MiddlewarePhase = 'beforeReducer' | 'afterReducer'
49+
50+
export type When = MiddlewarePhase | 'both' | undefined
4951
type WhenFromOptions<O extends ActionListenerOptions> =
5052
O extends ActionListenerOptions ? O['when'] : never
5153

@@ -57,8 +59,8 @@ export interface ActionListenerMiddlewareAPI<
5759
D extends Dispatch<AnyAction>,
5860
O extends ActionListenerOptions
5961
> extends MiddlewareAPI<D, S> {
60-
stopPropagation: WhenFromOptions<O> extends 'before' ? () => void : undefined
6162
unsubscribe(): void
63+
currentPhase: MiddlewarePhase
6264
}
6365

6466
/**
@@ -191,6 +193,9 @@ export const removeListenerAction = createAction(
191193
): RemoveListenerAction<AnyAction, S, D>
192194
}
193195

196+
const defaultWhen: MiddlewarePhase = 'afterReducer'
197+
const actualMiddlewarePhases = ['beforeReducer', 'afterReducer'] as const
198+
194199
/**
195200
* @alpha
196201
*/
@@ -233,46 +238,24 @@ export function createActionListenerMiddleware<
233238

234239
let stateBefore = api.getState()
235240

236-
const defaultWhen: When = 'after'
237241
let result: unknown
238-
for (const phase of ['before', 'after'] as const) {
242+
for (const currentPhase of actualMiddlewarePhases) {
239243
let stateNow = api.getState()
240244
for (let entry of listenerMap.values()) {
241-
if (
242-
(entry.when || defaultWhen) !== phase ||
243-
!entry.predicate(action, stateNow)
244-
) {
245+
const runThisPhase =
246+
entry.when === 'both' || entry.when === currentPhase
247+
const runListener = runThisPhase && entry.predicate(action, stateNow)
248+
if (!runListener) {
245249
continue
246250
}
247251

248-
let stoppedPropagation = false
249-
let currentPhase = phase
250-
let synchronousListenerFinished = false
251252
entry.listener(action, {
252253
...api,
253-
stopPropagation() {
254-
if (currentPhase === 'before') {
255-
if (!synchronousListenerFinished) {
256-
stoppedPropagation = true
257-
} else {
258-
throw new Error(
259-
'stopPropagation can only be called synchronously'
260-
)
261-
}
262-
} else {
263-
throw new Error(
264-
'stopPropagation can only be called by action listeners with the `when` option set to "before"'
265-
)
266-
}
267-
},
254+
currentPhase,
268255
unsubscribe: entry.unsubscribe,
269256
})
270-
synchronousListenerFinished = true
271-
if (stoppedPropagation) {
272-
return action
273-
}
274257
}
275-
if (phase === 'before') {
258+
if (currentPhase === 'beforeReducer') {
276259
result = next(action)
277260
} else {
278261
return result
@@ -345,6 +328,7 @@ export function createActionListenerMiddleware<
345328
const id = nanoid()
346329
const unsubscribe = () => listenerMap.delete(id)
347330
entry = {
331+
when: defaultWhen,
348332
...options,
349333
id,
350334
listener,

packages/action-listener-middleware/src/tests/listenerMiddleware.test.ts

Lines changed: 12 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
const middlewareApi = {
1616
getState: expect.any(Function),
1717
dispatch: expect.any(Function),
18-
stopPropagation: expect.any(Function),
18+
currentPhase: expect.stringMatching(/beforeReducer|afterReducer/),
1919
unsubscribe: expect.any(Function),
2020
}
2121

@@ -233,14 +233,15 @@ describe('createActionListenerMiddleware', () => {
233233
])
234234
})
235235

236-
const whenMap: [When, string, string][] = [
237-
[undefined, 'reducer', 'listener'],
238-
['before', 'listener', 'reducer'],
239-
['after', 'reducer', 'listener'],
236+
const whenMap: [When, string, string, number][] = [
237+
[undefined, 'reducer', 'listener', 1],
238+
['beforeReducer', 'listener', 'reducer', 1],
239+
['afterReducer', 'reducer', 'listener', 1],
240+
['both', 'reducer', 'listener', 2],
240241
]
241242
test.each(whenMap)(
242243
'with "when" set to %s, %s runs before %s',
243-
(when, _, shouldRunLast) => {
244+
(when, _, shouldRunLast, listenerCalls) => {
244245
let whoRanLast = ''
245246

246247
reducer.mockClear()
@@ -255,7 +256,7 @@ describe('createActionListenerMiddleware', () => {
255256

256257
store.dispatch(testAction1('a'))
257258
expect(reducer).toHaveBeenCalledTimes(1)
258-
expect(listener).toHaveBeenCalledTimes(1)
259+
expect(listener).toHaveBeenCalledTimes(listenerCalls)
259260
expect(whoRanLast).toBe(shouldRunLast)
260261
}
261262
)
@@ -275,48 +276,17 @@ describe('createActionListenerMiddleware', () => {
275276
calls.push(after2)
276277
}
277278

278-
middleware.addListener(testAction1, before1, { when: 'before' })
279-
middleware.addListener(testAction1, before2, { when: 'before' })
280-
middleware.addListener(testAction1, after1, { when: 'after' })
281-
middleware.addListener(testAction1, after2, { when: 'after' })
279+
middleware.addListener(testAction1, before1, { when: 'beforeReducer' })
280+
middleware.addListener(testAction1, before2, { when: 'beforeReducer' })
281+
middleware.addListener(testAction1, after1, { when: 'afterReducer' })
282+
middleware.addListener(testAction1, after2, { when: 'afterReducer' })
282283

283284
store.dispatch(testAction1('a'))
284285
store.dispatch(testAction2('a'))
285286

286287
expect(calls).toEqual([before1, before2, after1, after2])
287288
})
288289

289-
test('mixing "before" and "after" with stopPropagation', () => {
290-
const calls: Function[] = []
291-
function before1() {
292-
calls.push(before1)
293-
}
294-
function before2(_: any, api: any) {
295-
calls.push(before2)
296-
api.stopPropagation()
297-
}
298-
function before3() {
299-
calls.push(before3)
300-
}
301-
function after1() {
302-
calls.push(after1)
303-
}
304-
function after2() {
305-
calls.push(after2)
306-
}
307-
308-
middleware.addListener(testAction1, before1, { when: 'before' })
309-
middleware.addListener(testAction1, before2, { when: 'before' })
310-
middleware.addListener(testAction1, before3, { when: 'before' })
311-
middleware.addListener(testAction1, after1, { when: 'after' })
312-
middleware.addListener(testAction1, after2, { when: 'after' })
313-
314-
store.dispatch(testAction1('a'))
315-
store.dispatch(testAction2('a'))
316-
317-
expect(calls).toEqual([before1, before2])
318-
})
319-
320290
test('by default, actions are forwarded to the store', () => {
321291
reducer.mockClear()
322292

@@ -328,73 +298,4 @@ describe('createActionListenerMiddleware', () => {
328298

329299
expect(reducer.mock.calls).toEqual([[{}, testAction1('a')]])
330300
})
331-
332-
test('calling `api.stopPropagation` in the listeners prevents actions from being forwarded to the store', () => {
333-
reducer.mockClear()
334-
335-
middleware.addListener(
336-
testAction1,
337-
(action: TestAction1, api) => {
338-
if (action.payload === 'b') {
339-
api.stopPropagation()
340-
}
341-
},
342-
{ when: 'before' }
343-
)
344-
345-
store.dispatch(testAction1('a'))
346-
store.dispatch(testAction1('b'))
347-
store.dispatch(testAction1('c'))
348-
349-
expect(reducer.mock.calls).toEqual([
350-
[{}, testAction1('a')],
351-
[{}, testAction1('c')],
352-
])
353-
})
354-
355-
test('calling `api.stopPropagation` with `when` set to "after" causes an error to be thrown', () => {
356-
reducer.mockClear()
357-
358-
middleware.addListener(
359-
testAction1,
360-
(action: TestAction1, api) => {
361-
if (action.payload === 'b') {
362-
// @ts-ignore TypeScript would already prevent this from being called with "after"
363-
api.stopPropagation()
364-
}
365-
},
366-
{ when: 'after' }
367-
)
368-
369-
store.dispatch(testAction1('a'))
370-
expect(() => {
371-
store.dispatch(testAction1('b'))
372-
}).toThrowErrorMatchingInlineSnapshot(
373-
`"stopPropagation can only be called by action listeners with the \`when\` option set to \\"before\\""`
374-
)
375-
})
376-
377-
test('calling `api.stopPropagation` asynchronously causes an error to be thrown', (finish) => {
378-
reducer.mockClear()
379-
380-
middleware.addListener(
381-
testAction1,
382-
(action: TestAction1, api) => {
383-
if (action.payload === 'b') {
384-
setTimeout(() => {
385-
expect(() => {
386-
api.stopPropagation()
387-
}).toThrowErrorMatchingInlineSnapshot(
388-
`"stopPropagation can only be called synchronously"`
389-
)
390-
finish()
391-
})
392-
}
393-
},
394-
{ when: 'before' }
395-
)
396-
397-
store.dispatch(testAction1('a'))
398-
store.dispatch(testAction1('b'))
399-
})
400301
})

0 commit comments

Comments
 (0)