Skip to content

Commit ee00174

Browse files
committed
support passing an external abortsignal to createAsyncThunk
1 parent 18ddd7e commit ee00174

File tree

3 files changed

+92
-16
lines changed

3 files changed

+92
-16
lines changed

packages/toolkit/src/createAsyncThunk.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,36 +246,53 @@ export type AsyncThunkAction<
246246
unwrap: () => Promise<Returned>
247247
}
248248

249+
export interface AsyncThunkDispatchConfig {
250+
signal?: AbortSignal
251+
}
252+
249253
type AsyncThunkActionCreator<
250254
Returned,
251255
ThunkArg,
252256
ThunkApiConfig extends AsyncThunkConfig,
253257
> = IsAny<
254258
ThunkArg,
255259
// any handling
256-
(arg: ThunkArg) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>,
260+
(
261+
arg: ThunkArg,
262+
config?: AsyncThunkDispatchConfig,
263+
) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>,
257264
// unknown handling
258265
unknown extends ThunkArg
259-
? (arg: ThunkArg) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig> // argument not specified or specified as void or undefined
266+
? (
267+
arg: ThunkArg,
268+
config?: AsyncThunkDispatchConfig,
269+
) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig> // argument not specified or specified as void or undefined
260270
: [ThunkArg] extends [void] | [undefined]
261-
? () => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig> // argument contains void
271+
? (
272+
arg?: undefined,
273+
config?: AsyncThunkDispatchConfig,
274+
) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig> // argument contains void
262275
: [void] extends [ThunkArg] // make optional
263276
? (
264277
arg?: ThunkArg,
278+
config?: AsyncThunkDispatchConfig,
265279
) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig> // argument contains undefined
266280
: [undefined] extends [ThunkArg]
267281
? WithStrictNullChecks<
268282
// with strict nullChecks: make optional
269283
(
270284
arg?: ThunkArg,
285+
config?: AsyncThunkDispatchConfig,
271286
) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>,
272287
// without strict null checks this will match everything, so don't make it optional
273288
(
274289
arg: ThunkArg,
290+
config?: AsyncThunkDispatchConfig,
275291
) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>
276292
> // default case: normal argument
277293
: (
278294
arg: ThunkArg,
295+
config?: AsyncThunkDispatchConfig,
279296
) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>
280297
>
281298

@@ -492,6 +509,8 @@ type CreateAsyncThunk<CurriedThunkApiConfig extends AsyncThunkConfig> =
492509
>
493510
}
494511

512+
const externalAbortMessage = 'External signal was aborted'
513+
495514
export const createAsyncThunk = /* @__PURE__ */ (() => {
496515
function createAsyncThunk<
497516
Returned,
@@ -575,6 +594,7 @@ export const createAsyncThunk = /* @__PURE__ */ (() => {
575594

576595
function actionCreator(
577596
arg: ThunkArg,
597+
{ signal }: AsyncThunkDispatchConfig = {},
578598
): AsyncThunkAction<Returned, ThunkArg, Required<ThunkApiConfig>> {
579599
return (dispatch, getState, extra) => {
580600
const requestId = options?.idGenerator
@@ -590,6 +610,14 @@ export const createAsyncThunk = /* @__PURE__ */ (() => {
590610
abortController.abort()
591611
}
592612

613+
if (signal) {
614+
if (signal.aborted) {
615+
abort(externalAbortMessage)
616+
} else {
617+
signal.addEventListener('abort', () => abort(externalAbortMessage))
618+
}
619+
}
620+
593621
const promise = (async function () {
594622
let finalAction: ReturnType<typeof fulfilled | typeof rejected>
595623
try {

packages/toolkit/src/tests/createAsyncThunk.test-d.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import type { TSVersion } from '@phryneas/ts-version'
1616
import type { AxiosError } from 'axios'
1717
import apiRequest from 'axios'
18+
import type { AsyncThunkDispatchConfig } from '@internal/createAsyncThunk'
1819

1920
const defaultDispatch = (() => {}) as ThunkDispatch<{}, any, UnknownAction>
2021
const unknownAction = { type: 'foo' } as UnknownAction
@@ -269,7 +270,9 @@ describe('type tests', () => {
269270

270271
expectTypeOf(asyncThunk).toMatchTypeOf<() => any>()
271272

272-
expectTypeOf(asyncThunk).parameters.toEqualTypeOf<[]>()
273+
expectTypeOf(asyncThunk).parameters.toEqualTypeOf<
274+
[undefined?, AsyncThunkDispatchConfig?]
275+
>()
273276

274277
expectTypeOf(asyncThunk).returns.toBeFunction()
275278
})
@@ -279,7 +282,9 @@ describe('type tests', () => {
279282

280283
expectTypeOf(asyncThunk).toMatchTypeOf<() => any>()
281284

282-
expectTypeOf(asyncThunk).parameters.toEqualTypeOf<[]>()
285+
expectTypeOf(asyncThunk).parameters.toEqualTypeOf<
286+
[undefined?, AsyncThunkDispatchConfig?]
287+
>()
283288
})
284289

285290
test('one argument, specified as void: asyncThunk has no argument', () => {
@@ -388,13 +393,14 @@ describe('type tests', () => {
388393

389394
expectTypeOf(asyncThunk).toBeCallableWith()
390395

391-
// @ts-expect-error cannot be called with an argument, even if the argument is `undefined`
392396
expectTypeOf(asyncThunk).toBeCallableWith(undefined)
393397

394398
// cannot be called with an argument
395399
expectTypeOf(asyncThunk).parameter(0).not.toBeAny()
396400

397-
expectTypeOf(asyncThunk).parameters.toEqualTypeOf<[]>()
401+
expectTypeOf(asyncThunk).parameters.toEqualTypeOf<
402+
[undefined?, AsyncThunkDispatchConfig?]
403+
>()
398404
})
399405

400406
test('two arguments, first specified as void: asyncThunk has no argument', () => {
@@ -409,7 +415,9 @@ describe('type tests', () => {
409415
// cannot be called with an argument
410416
expectTypeOf(asyncThunk).parameter(0).not.toBeAny()
411417

412-
expectTypeOf(asyncThunk).parameters.toEqualTypeOf<[]>()
418+
expectTypeOf(asyncThunk).parameters.toEqualTypeOf<
419+
[undefined?, AsyncThunkDispatchConfig?]
420+
>()
413421
})
414422

415423
test('two arguments, first specified as number|undefined: asyncThunk has optional number argument', () => {

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

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -879,17 +879,18 @@ test('`condition` will see state changes from a synchronously invoked asyncThunk
879879
expect(onStart).toHaveBeenCalledTimes(2)
880880
})
881881

882+
const getNewStore = () =>
883+
configureStore({
884+
reducer(actions: UnknownAction[] = [], action) {
885+
return [...actions, action]
886+
},
887+
})
888+
882889
describe('meta', () => {
883-
const getNewStore = () =>
884-
configureStore({
885-
reducer(actions = [], action) {
886-
return [...actions, action]
887-
},
888-
})
889-
const store = getNewStore()
890+
let store = getNewStore()
890891

891892
beforeEach(() => {
892-
const store = getNewStore()
893+
store = getNewStore()
893894
})
894895

895896
test('pendingMeta', () => {
@@ -1003,3 +1004,42 @@ describe('meta', () => {
10031004
expect(result.error).toEqual('serialized!')
10041005
})
10051006
})
1007+
1008+
describe('dispatch config', () => {
1009+
let store = getNewStore()
1010+
1011+
beforeEach(() => {
1012+
store = getNewStore()
1013+
})
1014+
test('accepts external signal', async () => {
1015+
const asyncThunk = createAsyncThunk('test', async (_: void, { signal }) => {
1016+
signal.throwIfAborted()
1017+
const { promise, reject } = Promise.withResolvers()
1018+
signal.addEventListener('abort', () => reject(signal.reason))
1019+
return promise
1020+
})
1021+
1022+
const abortController = new AbortController()
1023+
const promise = store.dispatch(
1024+
asyncThunk(undefined, { signal: abortController.signal }),
1025+
)
1026+
abortController.abort()
1027+
await expect(promise.unwrap()).rejects.toThrow(
1028+
'External signal was aborted',
1029+
)
1030+
})
1031+
test('handles already aborted external signal', async () => {
1032+
const asyncThunk = createAsyncThunk('test', async (_: void, { signal }) => {
1033+
signal.throwIfAborted()
1034+
const { promise, reject } = Promise.withResolvers()
1035+
signal.addEventListener('abort', () => reject(signal.reason))
1036+
return promise
1037+
})
1038+
1039+
const signal = AbortSignal.abort()
1040+
const promise = store.dispatch(asyncThunk(undefined, { signal }))
1041+
await expect(promise.unwrap()).rejects.toThrow(
1042+
'Aborted due to condition callback returning false.',
1043+
)
1044+
})
1045+
})

0 commit comments

Comments
 (0)