Skip to content

Commit 0ffabd9

Browse files
authored
Merge pull request #4049 from aryaemami59/withTypes
Introduce pre-typed hooks via `listenerMiddleware.withTypes<RootState, AppDispatch>()` method
2 parents 904ef2b + 763a598 commit 0ffabd9

File tree

10 files changed

+704
-165
lines changed

10 files changed

+704
-165
lines changed

docs/api/createListenerMiddleware.mdx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -486,21 +486,16 @@ To fix this, the middleware provides types for defining "pre-typed" versions of
486486
```ts no-transpile
487487
// listenerMiddleware.ts
488488
import { createListenerMiddleware, addListener } from '@reduxjs/toolkit'
489-
import type { TypedStartListening, TypedAddListener } from '@reduxjs/toolkit'
490-
491489
import type { RootState, AppDispatch } from './store'
492490

493491
export const listenerMiddleware = createListenerMiddleware()
494492

495-
export type AppStartListening = TypedStartListening<RootState, AppDispatch>
496-
497-
export const startAppListening =
498-
listenerMiddleware.startListening as AppStartListening
499-
500-
export const addAppListener = addListener as TypedAddListener<
493+
export const startAppListening = listenerMiddleware.startListening.withTypes<
501494
RootState,
502495
AppDispatch
503-
>
496+
>()
497+
498+
export const addAppListener = addListener.withTypes<RootState, AppDispatch>()
504499
```
505500

506501
Then import and use those pre-typed methods in your components.

packages/toolkit/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
"@testing-library/user-event": "^13.1.5",
5858
"@types/json-stringify-safe": "^5.0.0",
5959
"@types/nanoid": "^2.1.0",
60-
"@types/node": "^10.14.4",
60+
"@types/node": "^20.11.0",
6161
"@types/query-string": "^6.3.0",
6262
"@types/react": "^18.0.12",
6363
"@types/react-dom": "^18.0.5",

packages/toolkit/src/listenerMiddleware/index.ts

Lines changed: 95 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -4,72 +4,72 @@ import type { ThunkDispatch } from 'redux-thunk'
44
import { createAction } from '../createAction'
55
import { nanoid } from '../nanoid'
66

7+
import { find } from '../utils'
8+
import {
9+
TaskAbortError,
10+
listenerCancelled,
11+
listenerCompleted,
12+
taskCancelled,
13+
taskCompleted,
14+
} from './exceptions'
15+
import {
16+
createDelay,
17+
createPause,
18+
raceWithSignal,
19+
runTask,
20+
validateActive,
21+
} from './task'
722
import type {
8-
ListenerMiddleware,
9-
ListenerMiddlewareInstance,
23+
AbortSignalWithReason,
1024
AddListenerOverloads,
1125
AnyListenerPredicate,
1226
CreateListenerMiddlewareOptions,
13-
TypedAddListener,
14-
TypedCreateListenerEntry,
1527
FallbackAddListenerOptions,
28+
ForkOptions,
29+
ForkedTask,
30+
ForkedTaskExecutor,
1631
ListenerEntry,
1732
ListenerErrorHandler,
18-
UnsubscribeListener,
19-
TakePattern,
2033
ListenerErrorInfo,
21-
ForkedTaskExecutor,
22-
ForkedTask,
23-
TypedRemoveListener,
34+
ListenerMiddleware,
35+
ListenerMiddlewareInstance,
36+
TakePattern,
2437
TaskResult,
25-
AbortSignalWithReason,
38+
TypedAddListener,
39+
TypedCreateListenerEntry,
40+
TypedRemoveListener,
41+
UnsubscribeListener,
2642
UnsubscribeListenerOptions,
27-
ForkOptions,
2843
} from './types'
2944
import {
3045
abortControllerWithReason,
3146
addAbortSignalListener,
3247
assertFunction,
3348
catchRejection,
3449
} from './utils'
35-
import {
36-
listenerCancelled,
37-
listenerCompleted,
38-
TaskAbortError,
39-
taskCancelled,
40-
taskCompleted,
41-
} from './exceptions'
42-
import {
43-
runTask,
44-
validateActive,
45-
createPause,
46-
createDelay,
47-
raceWithSignal,
48-
} from './task'
49-
import { find } from '../utils'
5050
export { TaskAbortError } from './exceptions'
5151
export type {
52-
ListenerEffect,
53-
ListenerMiddleware,
54-
ListenerEffectAPI,
55-
ListenerMiddlewareInstance,
52+
AsyncTaskExecutor,
5653
CreateListenerMiddlewareOptions,
57-
ListenerErrorHandler,
58-
TypedStartListening,
59-
TypedAddListener,
60-
TypedStopListening,
61-
TypedRemoveListener,
62-
UnsubscribeListener,
63-
UnsubscribeListenerOptions,
64-
ForkedTaskExecutor,
6554
ForkedTask,
6655
ForkedTaskAPI,
67-
AsyncTaskExecutor,
56+
ForkedTaskExecutor,
57+
ListenerEffect,
58+
ListenerEffectAPI,
59+
ListenerErrorHandler,
60+
ListenerMiddleware,
61+
ListenerMiddlewareInstance,
6862
SyncTaskExecutor,
6963
TaskCancelled,
7064
TaskRejected,
7165
TaskResolved,
7266
TaskResult,
67+
TypedAddListener,
68+
TypedRemoveListener,
69+
TypedStartListening,
70+
TypedStopListening,
71+
UnsubscribeListener,
72+
UnsubscribeListenerOptions,
7373
} from './types'
7474

7575
//Overly-aggressive byte-shaving
@@ -215,25 +215,27 @@ const getListenerEntryPropsFrom = (options: FallbackAddListenerOptions) => {
215215
}
216216

217217
/** Accepts the possible options for creating a listener, and returns a formatted listener entry */
218-
export const createListenerEntry: TypedCreateListenerEntry<unknown> = (
219-
options: FallbackAddListenerOptions
220-
) => {
221-
const { type, predicate, effect } = getListenerEntryPropsFrom(options)
222-
223-
const id = nanoid()
224-
const entry: ListenerEntry<unknown> = {
225-
id,
226-
effect,
227-
type,
228-
predicate,
229-
pending: new Set<AbortController>(),
230-
unsubscribe: () => {
231-
throw new Error('Unsubscribe not initialized')
232-
},
233-
}
218+
export const createListenerEntry: TypedCreateListenerEntry<unknown> =
219+
Object.assign(
220+
(options: FallbackAddListenerOptions) => {
221+
const { type, predicate, effect } = getListenerEntryPropsFrom(options)
222+
223+
const id = nanoid()
224+
const entry: ListenerEntry<unknown> = {
225+
id,
226+
effect,
227+
type,
228+
predicate,
229+
pending: new Set<AbortController>(),
230+
unsubscribe: () => {
231+
throw new Error('Unsubscribe not initialized')
232+
},
233+
}
234234

235-
return entry
236-
}
235+
return entry
236+
},
237+
{ withTypes: () => createListenerEntry }
238+
) as unknown as TypedCreateListenerEntry<unknown>
237239

238240
const cancelActiveListeners = (
239241
entry: ListenerEntry<unknown, Dispatch<UnknownAction>>
@@ -279,9 +281,9 @@ const safelyNotifyError = (
279281
/**
280282
* @public
281283
*/
282-
export const addListener = createAction(
283-
`${alm}/add`
284-
) as TypedAddListener<unknown>
284+
export const addListener = Object.assign(createAction(`${alm}/add`), {
285+
withTypes: () => addListener,
286+
}) as unknown as TypedAddListener<unknown>
285287

286288
/**
287289
* @public
@@ -291,9 +293,9 @@ export const clearAllListeners = createAction(`${alm}/removeAll`)
291293
/**
292294
* @public
293295
*/
294-
export const removeListener = createAction(
295-
`${alm}/remove`
296-
) as TypedRemoveListener<unknown>
296+
export const removeListener = Object.assign(createAction(`${alm}/remove`), {
297+
withTypes: () => removeListener,
298+
}) as unknown as TypedRemoveListener<unknown>
297299

298300
const defaultErrorHandler: ListenerErrorHandler = (...args: unknown[]) => {
299301
console.error(`${alm}/error`, ...args)
@@ -302,11 +304,17 @@ const defaultErrorHandler: ListenerErrorHandler = (...args: unknown[]) => {
302304
/**
303305
* @public
304306
*/
305-
export function createListenerMiddleware<
306-
S = unknown,
307-
D extends Dispatch<Action> = ThunkDispatch<S, unknown, UnknownAction>,
307+
export const createListenerMiddleware = <
308+
StateType = unknown,
309+
DispatchType extends Dispatch<Action> = ThunkDispatch<
310+
StateType,
311+
unknown,
312+
UnknownAction
313+
>,
308314
ExtraArgument = unknown
309-
>(middlewareOptions: CreateListenerMiddlewareOptions<ExtraArgument> = {}) {
315+
>(
316+
middlewareOptions: CreateListenerMiddlewareOptions<ExtraArgument> = {}
317+
) => {
310318
const listenerMap = new Map<string, ListenerEntry>()
311319
const { extra, onError = defaultErrorHandler } = middlewareOptions
312320

@@ -324,7 +332,7 @@ export function createListenerMiddleware<
324332
}
325333
}
326334

327-
const startListening = (options: FallbackAddListenerOptions) => {
335+
const startListening = ((options: FallbackAddListenerOptions) => {
328336
let entry = find(
329337
Array.from(listenerMap.values()),
330338
(existingEntry) => existingEntry.effect === options.effect
@@ -335,7 +343,11 @@ export function createListenerMiddleware<
335343
}
336344

337345
return insertEntry(entry)
338-
}
346+
}) as AddListenerOverloads<any>
347+
348+
Object.assign(startListening, {
349+
withTypes: () => startListening,
350+
})
339351

340352
const stopListening = (
341353
options: FallbackAddListenerOptions & UnsubscribeListenerOptions
@@ -361,15 +373,19 @@ export function createListenerMiddleware<
361373
return !!entry
362374
}
363375

376+
Object.assign(stopListening, {
377+
withTypes: () => stopListening,
378+
})
379+
364380
const notifyListener = async (
365381
entry: ListenerEntry<unknown, Dispatch<UnknownAction>>,
366382
action: unknown,
367383
api: MiddlewareAPI,
368-
getOriginalState: () => S
384+
getOriginalState: () => StateType
369385
) => {
370386
const internalTaskController = new AbortController()
371387
const take = createTakePattern(
372-
startListening,
388+
startListening as AddListenerOverloads<any>,
373389
internalTaskController.signal
374390
)
375391
const autoJoinPromises: Promise<any>[] = []
@@ -433,15 +449,15 @@ export function createListenerMiddleware<
433449

434450
const clearListenerMiddleware = createClearListenerMiddleware(listenerMap)
435451

436-
const middleware: ListenerMiddleware<S, D, ExtraArgument> =
452+
const middleware: ListenerMiddleware<StateType, DispatchType, ExtraArgument> =
437453
(api) => (next) => (action) => {
438454
if (!isAction(action)) {
439455
// we only want to notify listeners for action objects
440456
return next(action)
441457
}
442458

443459
if (addListener.match(action)) {
444-
return startListening(action.payload)
460+
return startListening(action.payload as any)
445461
}
446462

447463
if (clearAllListeners.match(action)) {
@@ -454,18 +470,18 @@ export function createListenerMiddleware<
454470
}
455471

456472
// Need to get this state _before_ the reducer processes the action
457-
let originalState: S | typeof INTERNAL_NIL_TOKEN = api.getState()
473+
let originalState: StateType | typeof INTERNAL_NIL_TOKEN = api.getState()
458474

459475
// `getOriginalState` can only be called synchronously.
460476
// @see https://github.com/reduxjs/redux-toolkit/discussions/1648#discussioncomment-1932820
461-
const getOriginalState = (): S => {
477+
const getOriginalState = (): StateType => {
462478
if (originalState === INTERNAL_NIL_TOKEN) {
463479
throw new Error(
464480
`${alm}: getOriginalState can only be called synchronously`
465481
)
466482
}
467483

468-
return originalState as S
484+
return originalState as StateType
469485
}
470486

471487
let result: unknown
@@ -475,10 +491,10 @@ export function createListenerMiddleware<
475491
result = next(action)
476492

477493
if (listenerMap.size > 0) {
478-
let currentState = api.getState()
494+
const currentState = api.getState()
479495
// Work around ESBuild+TS transpilation issue
480496
const listenerEntries = Array.from(listenerMap.values())
481-
for (let entry of listenerEntries) {
497+
for (const entry of listenerEntries) {
482498
let runListener = false
483499

484500
try {
@@ -511,5 +527,5 @@ export function createListenerMiddleware<
511527
startListening,
512528
stopListening,
513529
clearListeners: clearListenerMiddleware,
514-
} as ListenerMiddlewareInstance<S, D, ExtraArgument>
530+
} as ListenerMiddlewareInstance<StateType, DispatchType, ExtraArgument>
515531
}

packages/toolkit/src/listenerMiddleware/tests/listenerMiddleware.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,7 @@ describe('createListenerMiddleware', () => {
564564
typeof store.getState,
565565
typeof store.dispatch
566566
>,
567-
'effect'
567+
'effect' | 'withTypes'
568568
>
569569
][] = [
570570
['predicate', { predicate: () => true }],
@@ -1760,3 +1760,4 @@ describe('createListenerMiddleware', () => {
17601760
})
17611761
})
17621762
})
1763+

0 commit comments

Comments
 (0)