Skip to content

Commit 421a657

Browse files
committed
put abort on the promise returned by dispatch(asyncThunk())
1 parent 074d82e commit 421a657

File tree

3 files changed

+58
-90
lines changed

3 files changed

+58
-90
lines changed

etc/redux-toolkit.api.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,20 @@ export function createAction<P = void, T extends string = string>(type: T): Payl
102102
export function createAction<PA extends PrepareAction<any>, T extends string = string>(type: T, prepareAction: PA): PayloadActionCreator<ReturnType<PA>['payload'], T, PA>;
103103

104104
// @alpha (undocumented)
105-
export function createAsyncThunk<ActionType extends string, Returned, ActionParams = void, TA extends AsyncThunksArgs<any, any, any> = AsyncThunksArgs<unknown, unknown, Dispatch>>(type: ActionType, payloadCreator: (args: ActionParams, thunkArgs: TA) => Promise<Returned> | Returned): ((args: ActionParams) => ((dispatch: TA["dispatch"], getState: TA["getState"], extra: TA["extra"]) => Promise<import("./createAction").PayloadAction<Returned, string, {
105+
export function createAsyncThunk<ActionType extends string, Returned, ActionParams = void, TA extends AsyncThunksArgs<any, any, any> = AsyncThunksArgs<unknown, unknown, Dispatch>>(type: ActionType, payloadCreator: (args: ActionParams, thunkArgs: TA) => Promise<Returned> | Returned): ((args: ActionParams) => (dispatch: TA["dispatch"], getState: TA["getState"], extra: TA["extra"]) => Promise<import("./createAction").PayloadAction<undefined, string, {
106106
args: ActionParams;
107107
requestId: string;
108-
}, never> | import("./createAction").PayloadAction<undefined, string, {
108+
aborted: boolean;
109+
abortReason: string;
110+
} | {
111+
args: ActionParams;
112+
requestId: string;
113+
aborted?: undefined;
114+
abortReason?: undefined;
115+
}, any> | import("./createAction").PayloadAction<Returned, string, {
109116
args: ActionParams;
110117
requestId: string;
111-
}, any>>) & {
118+
}, never>> & {
112119
abort: (reason?: string) => void;
113120
}) & {
114121
pending: import("./createAction").ActionCreatorWithPreparedPayload<[string, ActionParams], undefined, string, never, {
@@ -118,6 +125,13 @@ export function createAsyncThunk<ActionType extends string, Returned, ActionPara
118125
rejected: import("./createAction").ActionCreatorWithPreparedPayload<[Error, string, ActionParams], undefined, string, any, {
119126
args: ActionParams;
120127
requestId: string;
128+
aborted: boolean;
129+
abortReason: string;
130+
} | {
131+
args: ActionParams;
132+
requestId: string;
133+
aborted?: undefined;
134+
abortReason?: undefined;
121135
}>;
122136
fulfilled: import("./createAction").ActionCreatorWithPreparedPayload<[Returned, string, ActionParams], Returned, string, never, {
123137
args: ActionParams;

src/createAsyncThunk.test.ts

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,8 @@ describe('createAsyncThunk with abortController', () => {
155155
})
156156

157157
test('abort after dispatch', async () => {
158-
const thunkAction = asyncThunk({})
159-
const promise = store.dispatch(thunkAction)
160-
thunkAction.abort('AbortReason')
158+
const promise = store.dispatch(asyncThunk({}))
159+
promise.abort('AbortReason')
161160
const result = await promise
162161
const expectedAbortedAction = {
163162
type: 'test/rejected',
@@ -191,29 +190,4 @@ describe('createAsyncThunk with abortController', () => {
191190
})
192191
)
193192
})
194-
195-
test('abort before dispatch', async () => {
196-
const thunkAction = asyncThunk({})
197-
thunkAction.abort('AbortReason')
198-
const result = await store.dispatch(thunkAction)
199-
200-
const expectedAbortedAction = {
201-
type: 'test/rejected',
202-
error: {
203-
message: 'AbortReason',
204-
name: 'AbortError'
205-
},
206-
meta: { aborted: true, abortReason: 'AbortReason' }
207-
}
208-
// abortedAction with reason is dispatched without test/pending being dispatched
209-
expect(store.getState()).toMatchObject([{}, expectedAbortedAction])
210-
211-
// same abortedAction is returned
212-
expect(result).toMatchObject(expectedAbortedAction)
213-
214-
// unwrapResult throws AbortedError from returned action
215-
expect(() => unwrapResult(result)).toThrowError(
216-
expect.objectContaining(expectedAbortedAction.error)
217-
)
218-
})
219193
})

src/createAsyncThunk.ts

Lines changed: 39 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -104,83 +104,63 @@ export function createAsyncThunk<
104104
function actionCreator(args: ActionParams) {
105105
const abortController = new AbortController()
106106

107-
let dispatchAbort:
108-
| undefined
109-
| ((reason: string) => ReturnType<typeof rejected>)
110-
111-
let abortReason: string
112-
113-
function abort(reason: string = 'Aborted.') {
114-
abortController.abort()
115-
if (dispatchAbort) {
116-
dispatchAbort(reason)
117-
} else {
118-
abortReason = reason
119-
}
120-
}
121-
122-
async function thunkAction(
107+
return function thunkAction(
123108
dispatch: TA['dispatch'],
124109
getState: TA['getState'],
125110
extra: TA['extra']
126111
) {
127112
const requestId = nanoid()
128113
let abortAction: ReturnType<typeof rejected> | undefined
129-
dispatchAbort = reason => {
114+
115+
function abort(reason: string = 'Aborted.') {
116+
abortController.abort()
130117
abortAction = rejected(
131118
new DOMException(reason, 'AbortError'),
132119
requestId,
133120
args
134121
)
135122
dispatch(abortAction)
136-
return abortAction
137-
}
138-
// if the thunkAction.abort() method has been called before the thunkAction was dispatched,
139-
// just dispatch an aborted-action and never start with the thunk
140-
if (abortController.signal.aborted) {
141-
return dispatchAbort(abortReason)
142123
}
143124

144-
let finalAction: ReturnType<typeof fulfilled | typeof rejected>
145-
try {
146-
dispatch(pending(requestId, args))
147-
148-
finalAction = fulfilled(
149-
await payloadCreator(args, {
150-
dispatch,
151-
getState,
152-
extra,
125+
const promise = (async function() {
126+
let finalAction: ReturnType<typeof fulfilled | typeof rejected>
127+
try {
128+
dispatch(pending(requestId, args))
129+
130+
finalAction = fulfilled(
131+
await payloadCreator(args, {
132+
dispatch,
133+
getState,
134+
extra,
135+
requestId,
136+
signal: abortController.signal
137+
} as TA),
153138
requestId,
154-
signal: abortController.signal
155-
} as TA),
156-
requestId,
157-
args
158-
)
159-
} catch (err) {
160-
if (
161-
err instanceof DOMException &&
162-
err.name === 'AbortError' &&
163-
abortAction
164-
) {
165-
// abortAction has already been dispatched, no further action should be dispatched
166-
// by this thunk.
167-
// return a copy of the dispatched abortAction, but attach the AbortError to it.
168-
return Object.assign({}, abortAction, {
169-
error: miniSerializeError(err)
170-
})
139+
args
140+
)
141+
} catch (err) {
142+
if (
143+
err instanceof DOMException &&
144+
err.name === 'AbortError' &&
145+
abortAction
146+
) {
147+
// abortAction has already been dispatched, no further action should be dispatched
148+
// by this thunk.
149+
// return a copy of the dispatched abortAction, but attach the AbortError to it.
150+
return { ...abortAction, error: miniSerializeError(err) }
151+
}
152+
finalAction = rejected(err, requestId, args)
171153
}
172-
finalAction = rejected(err, requestId, args)
173-
}
174154

175-
// We dispatch "success" _after_ the catch, to avoid having any errors
176-
// here get swallowed by the try/catch block,
177-
// per https://twitter.com/dan_abramov/status/770914221638942720
178-
// and https://redux-toolkit.js.org/tutorials/advanced-tutorial#async-error-handling-logic-in-thunks
179-
dispatch(finalAction)
180-
return finalAction
155+
// We dispatch "success" _after_ the catch, to avoid having any errors
156+
// here get swallowed by the try/catch block,
157+
// per https://twitter.com/dan_abramov/status/770914221638942720
158+
// and https://redux-toolkit.js.org/tutorials/advanced-tutorial#async-error-handling-logic-in-thunks
159+
dispatch(finalAction)
160+
return finalAction
161+
})()
162+
return Object.assign(promise, { abort })
181163
}
182-
183-
return Object.assign(thunkAction, { abort })
184164
}
185165

186166
return Object.assign(actionCreator, {

0 commit comments

Comments
 (0)