Skip to content

Commit 7cdd615

Browse files
authored
Merge pull request #2078 from reduxjs/feature/final-1.8-cleanup
2 parents cb97bfa + 5acc6ef commit 7cdd615

File tree

5 files changed

+244
-86
lines changed

5 files changed

+244
-86
lines changed

docs/api/createListenerMiddleware.mdx

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ const store = configureStore({
144144
Adds a new listener entry to the middleware. Typically used to "statically" add new listeners during application setup.
145145

146146
```ts no-transpile
147-
const startListening = (options: AddListenerOptions) => Unsubscribe
147+
const startListening = (options: AddListenerOptions) => UnsubscribeListener
148148

149149
interface AddListenerOptions {
150150
// Four options for deciding when the listener will run:
@@ -170,6 +170,14 @@ type ListenerPredicate<Action extends AnyAction, State> = (
170170
currentState?: State,
171171
originalState?: State
172172
) => boolean
173+
174+
type UnsubscribeListener = (
175+
unsuscribeOptions?: UnsubscribeListenerOptions
176+
) => void
177+
178+
interface UnsubscribeListenerOptions {
179+
cancelActive?: true
180+
}
173181
```
174182

175183
**You must provide exactly _one_ of the four options for deciding when the listener will run: `type`, `actionCreator`, `matcher`, or `predicate`**. Every time an action is dispatched, each listener will be checked to see if it should run based on the current action vs the comparison option provided.
@@ -199,36 +207,55 @@ Note that the `predicate` option actually allows matching solely against state-r
199207

200208
The ["matcher" utility functions included in RTK](./matching-utilities.mdx) are acceptable as either the `matcher` or `predicate` option.
201209

202-
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.
210+
The return value is an `unsubscribe()` callback that will remove this listener. By default, unsubscribing will _not_ cancel any active instances of the listener. However, you may also pass in `{cancelActive: true}` to cancel running instances.
211+
212+
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.
203213

204214
The `effect` callback will receive the current action as its first argument, as well as a "listener API" object similar to the "thunk API" object in `createAsyncThunk`.
205215

206216
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.
207217

208218
### `stopListening`
209219

210-
Removes a given listener. It accepts the same arguments as `startListening()`. It checks for an existing listener entry by comparing the function references of `listener` and the provided `actionCreator/matcher/predicate` function or `type` string.
220+
Removes a given listener entry.
221+
222+
It accepts the same arguments as `startListening()`. It checks for an existing listener entry by comparing the function references of `listener` and the provided `actionCreator/matcher/predicate` function or `type` string.
223+
224+
By default, this does _not_ cancel any active running instances. However, you may also pass in `{cancelActive: true}` to cancel running instances.
211225

212226
```ts no-transpile
213-
const stopListening = (options: AddListenerOptions) => boolean
227+
const stopListening = (
228+
options: AddListenerOptions & UnsubscribeListenerOptions
229+
) => boolean
230+
231+
interface UnsubscribeListenerOptions {
232+
cancelActive?: true
233+
}
214234
```
215235

216236
Returns `true` if the `options.effect` listener has been removed, or `false` if no subscription matching the input provided has been found.
217237

218238
```js
239+
// Examples:
219240
// 1) Action type string
220-
listenerMiddleware.stopListening({ type: 'todos/todoAdded', listener })
241+
listenerMiddleware.stopListening({
242+
type: 'todos/todoAdded',
243+
listener,
244+
cancelActive: true,
245+
})
221246
// 2) RTK action creator
222247
listenerMiddleware.stopListening({ actionCreator: todoAdded, listener })
223248
// 3) RTK matcher function
224-
listenerMiddleware.stopListening({ matcher, listener })
249+
listenerMiddleware.stopListening({ matcher, listener, cancelActive: true })
225250
// 4) Listener predicate
226251
listenerMiddleware.stopListening({ predicate, listener })
227252
```
228253

229254
### `clearListeners`
230255

231-
Removes all current listener entries. This is most likely useful for test scenarios where a single middleware or store instance might be used in multiple tests, as well as some app cleanup situations.
256+
Removes all current listener entries. It also cancels all active running instances of those listeners as well.
257+
258+
This is most likely useful for test scenarios where a single middleware or store instance might be used in multiple tests, as well as some app cleanup situations.
232259

233260
```ts no-transpile
234261
const clearListeners = () => void;
@@ -253,18 +280,22 @@ const unsubscribe = store.dispatch(addListener({ predicate, listener }))
253280

254281
A standard RTK action creator, imported from the package. Dispatching this action tells the middleware to dynamically remove a listener at runtime. Accepts the same arguments as `stopListening()`.
255282

283+
By default, this does _not_ cancel any active running instances. However, you may also pass in `{cancelActive: true}` to cancel running instances.
284+
256285
Returns `true` if the `options.listener` listener has been removed, `false` if no subscription matching the input provided has been found.
257286

258287
```js
259-
store.dispatch(removeListener({ predicate, listener }))
288+
const wasRemoved = store.dispatch(
289+
removeListener({ predicate, listener, cancelActive: true })
290+
)
260291
```
261292

262-
### `removeAllListeners`
293+
### `clearAllListeners`
263294

264-
A standard RTK action creator, imported from the package. Dispatching this action tells the middleware to dynamically remove all listeners at runtime.
295+
A standard RTK action creator, imported from the package. Dispatching this action tells the middleware to remove all current listener entries. It also cancels all active running instances of those listeners as well.
265296

266297
```js
267-
store.dispatch(removeAllListeners())
298+
store.dispatch(clearAllListeners())
268299
```
269300

270301
## Listener API
@@ -284,7 +315,7 @@ The `listenerApi` object is the second argument to each listener callback. It co
284315

285316
### Listener Subscription Management
286317

287-
- `unsubscribe: () => void`: removes the listener entry from the middleware, and prevent future instances of the listener from running.
318+
- `unsubscribe: () => void`: removes the listener entry from the middleware, and prevent future instances of the listener from running. (This does _not_ cancel any active instances.)
288319
- `subscribe: () => void`: will re-subscribe the listener entry if it was previously removed, or no-op if currently subscribed
289320
- `cancelActiveListeners: () => void`: cancels all other running instances of this same listener _except_ for the one that made this call. (The cancellation will only have a meaningful effect if the other instances are paused using one of the cancellation-aware APIs like `take/cancel/pause/delay` - see "Cancelation and Task Management" in the "Usage" section for more details)
290321
- `signal: AbortSignal`: An [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) whose `aborted` property will be set to `true` if the listener execution is aborted or completed.

packages/toolkit/src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,8 @@ export type {
162162
TypedAddListener,
163163
TypedStopListening,
164164
TypedRemoveListener,
165-
Unsubscribe,
165+
UnsubscribeListener,
166+
UnsubscribeListenerOptions,
166167
ForkedTaskExecutor,
167168
ForkedTask,
168169
ForkedTaskAPI,
@@ -178,6 +179,6 @@ export {
178179
createListenerMiddleware,
179180
addListener,
180181
removeListener,
181-
removeAllListeners,
182+
clearAllListeners,
182183
TaskAbortError,
183184
} from './listenerMiddleware/index'

packages/toolkit/src/listenerMiddleware/index.ts

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ import type {
1414
FallbackAddListenerOptions,
1515
ListenerEntry,
1616
ListenerErrorHandler,
17-
Unsubscribe,
17+
UnsubscribeListener,
1818
TakePattern,
1919
ListenerErrorInfo,
2020
ForkedTaskExecutor,
2121
ForkedTask,
2222
TypedRemoveListener,
2323
TaskResult,
2424
AbortSignalWithReason,
25+
UnsubscribeListenerOptions,
2526
} from './types'
2627
import {
2728
abortControllerWithReason,
@@ -55,7 +56,8 @@ export type {
5556
TypedAddListener,
5657
TypedStopListening,
5758
TypedRemoveListener,
58-
Unsubscribe,
59+
UnsubscribeListener,
60+
UnsubscribeListenerOptions,
5961
ForkedTaskExecutor,
6062
ForkedTask,
6163
ForkedTaskAPI,
@@ -113,7 +115,11 @@ const createFork = (parentAbortSignal: AbortSignalWithReason<unknown>) => {
113115
}
114116

115117
const createTakePattern = <S>(
116-
startListening: AddListenerOverloads<Unsubscribe, S, Dispatch<AnyAction>>,
118+
startListening: AddListenerOverloads<
119+
UnsubscribeListener,
120+
S,
121+
Dispatch<AnyAction>
122+
>,
117123
signal: AbortSignal
118124
): TakePattern<S> => {
119125
/**
@@ -130,7 +136,7 @@ const createTakePattern = <S>(
130136
validateActive(signal)
131137

132138
// Placeholder unsubscribe function until the listener is added
133-
let unsubscribe: Unsubscribe = () => {}
139+
let unsubscribe: UnsubscribeListener = () => {}
134140

135141
const tuplePromise = new Promise<[AnyAction, S, S]>((resolve) => {
136142
// Inside the Promise, we synchronously add the listener.
@@ -223,11 +229,7 @@ const createClearListenerMiddleware = (
223229
listenerMap: Map<string, ListenerEntry>
224230
) => {
225231
return () => {
226-
listenerMap.forEach((entry) => {
227-
entry.pending.forEach((controller) => {
228-
abortControllerWithReason(controller, listenerCancelled)
229-
})
230-
})
232+
listenerMap.forEach(cancelActiveListeners)
231233

232234
listenerMap.clear()
233235
}
@@ -257,19 +259,19 @@ const safelyNotifyError = (
257259
}
258260

259261
/**
260-
* @alpha
262+
* @public
261263
*/
262264
export const addListener = createAction(
263265
`${alm}/add`
264266
) as TypedAddListener<unknown>
265267

266268
/**
267-
* @alpha
269+
* @public
268270
*/
269-
export const removeAllListeners = createAction(`${alm}/removeAll`)
271+
export const clearAllListeners = createAction(`${alm}/removeAll`)
270272

271273
/**
272-
* @alpha
274+
* @public
273275
*/
274276
export const removeListener = createAction(
275277
`${alm}/remove`
@@ -279,8 +281,16 @@ const defaultErrorHandler: ListenerErrorHandler = (...args: unknown[]) => {
279281
console.error(`${alm}/error`, ...args)
280282
}
281283

284+
const cancelActiveListeners = (
285+
entry: ListenerEntry<unknown, Dispatch<AnyAction>>
286+
) => {
287+
entry.pending.forEach((controller) => {
288+
abortControllerWithReason(controller, listenerCancelled)
289+
})
290+
}
291+
282292
/**
283-
* @alpha
293+
* @public
284294
*/
285295
export function createListenerMiddleware<
286296
S = unknown,
@@ -296,7 +306,12 @@ export function createListenerMiddleware<
296306
entry.unsubscribe = () => listenerMap.delete(entry!.id)
297307

298308
listenerMap.set(entry.id, entry)
299-
return entry.unsubscribe
309+
return (cancelOptions?: UnsubscribeListenerOptions) => {
310+
entry.unsubscribe()
311+
if (cancelOptions?.cancelActive) {
312+
cancelActiveListeners(entry)
313+
}
314+
}
300315
}
301316

302317
const findListenerEntry = (
@@ -323,7 +338,9 @@ export function createListenerMiddleware<
323338
return insertEntry(entry)
324339
}
325340

326-
const stopListening = (options: FallbackAddListenerOptions): boolean => {
341+
const stopListening = (
342+
options: FallbackAddListenerOptions & UnsubscribeListenerOptions
343+
): boolean => {
327344
const { type, effect, predicate } = getListenerEntryPropsFrom(options)
328345

329346
const entry = findListenerEntry((entry) => {
@@ -335,7 +352,12 @@ export function createListenerMiddleware<
335352
return matchPredicateOrType && entry.effect === effect
336353
})
337354

338-
entry?.unsubscribe()
355+
if (entry) {
356+
entry.unsubscribe()
357+
if (options.cancelActive) {
358+
cancelActiveListeners(entry)
359+
}
360+
}
339361

340362
return !!entry
341363
}
@@ -405,7 +427,7 @@ export function createListenerMiddleware<
405427
return startListening(action.payload)
406428
}
407429

408-
if (removeAllListeners.match(action)) {
430+
if (clearAllListeners.match(action)) {
409431
clearListenerMiddleware()
410432
return
411433
}

0 commit comments

Comments
 (0)