Skip to content

Commit 673e306

Browse files
authored
Merge pull request #1973 from FaberVitale/feat/alm-change-remove-listener-signature
2 parents d5b0eb5 + efc2de5 commit 673e306

File tree

4 files changed

+188
-121
lines changed

4 files changed

+188
-121
lines changed

packages/action-listener-middleware/README.md

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ Current options are:
114114

115115
- `onError`: an optional error handler that gets called with synchronous and async errors raised by `listener` and synchronous errors thrown by `predicate`.
116116

117-
### `listenerMiddleware.addListener(options?: AddListenerOptions) : Unsubscribe`
117+
### `listenerMiddleware.addListener(options: AddListenerOptions) : Unsubscribe`
118118

119119
Statically adds a new listener callback to the middleware.
120120

@@ -166,6 +166,7 @@ middleware.addListener({
166166
})
167167
```
168168

169+
It throws error if listener is not a function.
169170
The ["matcher" utility functions included in RTK](https://redux-toolkit.js.org/api/matching-utilities) are acceptable as predicates.
170171

171172
The return value is a standard `unsubscribe()` callback that will remove this listener. If you try to add a listener entry but another entry with this exact function reference already exists, no new entry will be added, and the existing `unsubscribe` method will be returned.
@@ -174,14 +175,26 @@ The `listener` callback will receive the current action as its first argument, a
174175

175176
All listener predicates and callbacks are checked _after_ the root reducer has already processed the action and updated the state. The `listenerApi.getOriginalState()` method can be used to get the state value that existed before the action that triggered this listener was processed.
176177

177-
### `listenerMiddleware.removeListener(typeOrActionCreator, listener)`
178+
### `listenerMiddleware.removeListener(options: AddListenerOptions): boolean`
178179

179-
Removes a given listener. Accepts two arguments:
180+
Removes a given listener. Accepts the same arguments as `middleware.addListener()` and throws error if listener is not a function.
181+
Returns `true` if the `options.listener` listener has been removed, `false` if no subscription matching the input provided has been found.
180182

181-
- `typeOrActionCreator: string | ActionCreator`: the same action type / action creator that was used to add the listener
182-
- `listener: ListenerCallback`: the same listener callback reference that was added originally
183-
184-
Note that matcher-based listeners currently cannot be removed with this approach - you must use the `unsubscribe()` callback that was returned when adding the listener.
183+
```ts
184+
// 1) Action type string
185+
middleware.removeListener({ type: 'todos/todoAdded', listener })
186+
// 2) RTK action creator
187+
middleware.removeListener({ actionCreator: todoAdded, listener })
188+
// 3) RTK matcher function
189+
middleware.removeListener({ matcher: isAnyOf(todoAdded, todoToggled), listener })
190+
// 4) Listener predicate
191+
middleware.removeListener({
192+
predicate: (action, currentState, previousState) => {
193+
// return true when the listener should run
194+
},
195+
listener,
196+
})
197+
```
185198

186199
### `addListenerAction`
187200

@@ -197,9 +210,11 @@ const unsubscribe = store.dispatch(addListenerAction({ predicate, listener }))
197210
### `removeListenerAction`
198211

199212
A standard RTK action creator that tells the middleware to remove a listener at runtime. Accepts the same arguments as `middleware.removeListener()`:
213+
Returns `true` if the `options.listener` listener has been removed, `false` if no subscription matching the input provided has been found.
214+
200215

201216
```js
202-
store.dispatch(removeListenerAction('todos/todoAdded', listener))
217+
store.dispatch(removeListenerAction({ predicate, listener }))
203218
```
204219

205220
### `listenerApi`

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

Lines changed: 39 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,14 @@ import type {
99
import { createAction, nanoid } from '@reduxjs/toolkit'
1010

1111
import type {
12-
ActionListener,
12+
ActionListenerMiddleware,
1313
AddListenerOverloads,
14-
BaseActionCreator,
1514
AnyActionListenerPredicate,
1615
CreateListenerMiddlewareOptions,
1716
TypedActionCreator,
1817
TypedAddListener,
1918
TypedAddListenerAction,
2019
TypedCreateListenerEntry,
21-
RemoveListenerAction,
2220
FallbackAddListenerOptions,
2321
ListenerEntry,
2422
ListenerErrorHandler,
@@ -28,6 +26,8 @@ import type {
2826
ListenerErrorInfo,
2927
ForkedTaskExecutor,
3028
ForkedTask,
29+
TypedRemoveListenerAction,
30+
TypedRemoveListener,
3131
} from './types'
3232
import { assertFunction, catchRejection } from './utils'
3333
import { TaskAbortError } from './exceptions'
@@ -48,6 +48,8 @@ export type {
4848
ListenerErrorHandler,
4949
TypedAddListener,
5050
TypedAddListenerAction,
51+
TypedRemoveListener,
52+
TypedRemoveListenerAction,
5153
Unsubscribe,
5254
ForkedTaskExecutor,
5355
ForkedTask,
@@ -162,10 +164,7 @@ const createTakePattern = <S>(
162164
) => catchRejection(take(predicate, timeout))) as TakePattern<S>
163165
}
164166

165-
/** Accepts the possible options for creating a listener, and returns a formatted listener entry */
166-
export const createListenerEntry: TypedCreateListenerEntry<unknown> = (
167-
options: FallbackAddListenerOptions
168-
) => {
167+
const getListenerEntryPropsFrom = (options: FallbackAddListenerOptions) => {
169168
let { type, actionCreator, matcher, predicate, listener } = options
170169

171170
if (type) {
@@ -179,10 +178,21 @@ export const createListenerEntry: TypedCreateListenerEntry<unknown> = (
179178
// pass
180179
} else {
181180
throw new Error(
182-
'Creating a listener requires one of the known fields for matching an action'
181+
'Creating or removing a listener requires one of the known fields for matching an action'
183182
)
184183
}
185184

185+
assertFunction(listener, 'options.listener')
186+
187+
return { predicate, type, listener }
188+
}
189+
190+
/** Accepts the possible options for creating a listener, and returns a formatted listener entry */
191+
export const createListenerEntry: TypedCreateListenerEntry<unknown> = (
192+
options: FallbackAddListenerOptions
193+
) => {
194+
const { type, predicate, listener } = getListenerEntryPropsFrom(options)
195+
186196
const id = nanoid()
187197
const entry: ListenerEntry<unknown> = {
188198
id,
@@ -239,17 +249,7 @@ const safelyNotifyError = (
239249
* @alpha
240250
*/
241251
export const addListenerAction = createAction(
242-
`${alm}/add`,
243-
(options: unknown) => {
244-
const entry = createListenerEntry(
245-
// Fake out TS here
246-
options as Parameters<AddListenerOverloads<unknown>>[0]
247-
)
248-
249-
return {
250-
payload: entry,
251-
}
252-
}
252+
`${alm}/add`
253253
) as TypedAddListenerAction<unknown>
254254

255255
/**
@@ -261,37 +261,8 @@ export const clearListenerMiddlewareAction = createAction(`${alm}/clear`)
261261
* @alpha
262262
*/
263263
export const removeListenerAction = createAction(
264-
`${alm}/remove`,
265-
(
266-
typeOrActionCreator: string | TypedActionCreator<string>,
267-
listener: ActionListener<any, any, any>
268-
) => {
269-
const type =
270-
typeof typeOrActionCreator === 'string'
271-
? typeOrActionCreator
272-
: (typeOrActionCreator as TypedActionCreator<string>).type
273-
274-
return {
275-
payload: {
276-
type,
277-
listener,
278-
},
279-
}
280-
}
281-
) as BaseActionCreator<
282-
{ type: string; listener: ActionListener<any, any, any> },
283-
'actionListenerMiddleware/remove'
284-
> & {
285-
<C extends TypedActionCreator<any>, S, D extends Dispatch>(
286-
actionCreator: C,
287-
listener: ActionListener<ReturnType<C>, S, D>
288-
): RemoveListenerAction<ReturnType<C>, S, D>
289-
290-
<S, D extends Dispatch>(
291-
type: string,
292-
listener: ActionListener<AnyAction, S, D>
293-
): RemoveListenerAction<AnyAction, S, D>
294-
}
264+
`${alm}/remove`
265+
) as TypedRemoveListenerAction<unknown>
295266

296267
const defaultErrorHandler: ListenerErrorHandler = (...args: unknown[]) => {
297268
console.error(`${alm}/error`, ...args)
@@ -330,7 +301,7 @@ export function createActionListenerMiddleware<
330301
return undefined
331302
}
332303

333-
const addListener = ((options: FallbackAddListenerOptions) => {
304+
const addListener = (options: FallbackAddListenerOptions) => {
334305
let entry = findListenerEntry(
335306
(existingEntry) => existingEntry.listener === options.listener
336307
)
@@ -340,39 +311,23 @@ export function createActionListenerMiddleware<
340311
}
341312

342313
return insertEntry(entry)
343-
}) as TypedAddListener<S, D>
344-
345-
type RemoveListener = {
346-
<C extends TypedActionCreator<any>>(
347-
actionCreator: C,
348-
listener: ActionListener<ReturnType<C>, S, D>
349-
): boolean
350-
(type: string, listener: ActionListener<AnyAction, S, D>): boolean
351-
(
352-
typeOrActionCreator: string | TypedActionCreator<any>,
353-
listener: ActionListener<AnyAction, S, D>
354-
): boolean
355314
}
356315

357-
const removeListener: RemoveListener = (
358-
typeOrActionCreator: string | TypedActionCreator<any>,
359-
listener: ActionListener<AnyAction, S, D>
360-
): boolean => {
361-
const type =
362-
typeof typeOrActionCreator === 'string'
363-
? typeOrActionCreator
364-
: typeOrActionCreator.type
316+
const removeListener = (options: FallbackAddListenerOptions): boolean => {
317+
const { type, listener, predicate } = getListenerEntryPropsFrom(options)
365318

366-
let entry = findListenerEntry(
367-
(entry) => entry.type === type && entry.listener === listener
368-
)
319+
const entry = findListenerEntry((entry) => {
320+
const matchPredicateOrType =
321+
typeof type === 'string'
322+
? entry.type === type
323+
: entry.predicate === predicate
369324

370-
if (!entry) {
371-
return false
372-
}
325+
return matchPredicateOrType && entry.listener === listener
326+
})
373327

374-
listenerMap.delete(entry.id)
375-
return true
328+
entry?.unsubscribe()
329+
330+
return !!entry
376331
}
377332

378333
const notifyListener = async (
@@ -439,15 +394,7 @@ export function createActionListenerMiddleware<
439394
D
440395
> = (api) => (next) => (action) => {
441396
if (addListenerAction.match(action)) {
442-
let entry = findListenerEntry(
443-
(existingEntry) => existingEntry.listener === action.payload.listener
444-
)
445-
446-
if (!entry) {
447-
entry = action.payload
448-
}
449-
450-
return insertEntry(entry)
397+
return addListener(action.payload)
451398
}
452399

453400
if (clearListenerMiddlewareAction.match(action)) {
@@ -456,8 +403,7 @@ export function createActionListenerMiddleware<
456403
}
457404

458405
if (removeListenerAction.match(action)) {
459-
removeListener(action.payload.type, action.payload.listener)
460-
return
406+
return removeListener(action.payload)
461407
}
462408

463409
// Need to get this state _before_ the reducer processes the action
@@ -514,10 +460,9 @@ export function createActionListenerMiddleware<
514460
return assign(
515461
middleware,
516462
{
517-
addListener,
518-
removeListener,
519-
clear: clearListenerMiddleware,
520-
addListenerAction: addListenerAction as TypedAddListenerAction<S>,
463+
addListener: addListener as TypedAddListener<S, D>,
464+
removeListener: removeListener as TypedRemoveListener<S, D>,
465+
clearListeners: clearListenerMiddleware,
521466
},
522467
{} as WithMiddlewareType<typeof middleware>
523468
)

0 commit comments

Comments
 (0)