Skip to content

Commit 13ae697

Browse files
author
Lenz Weber
committed
prevent dispatching of further actions if asyncThunk has been cancelled, even if the payloadCreator didn't react to the abort request
1 parent 4c7c92a commit 13ae697

File tree

2 files changed

+43
-7
lines changed

2 files changed

+43
-7
lines changed

src/createAsyncThunk.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,4 +188,38 @@ describe('createAsyncThunk with abortController', () => {
188188
})
189189
)
190190
})
191+
192+
test('even when the payloadCreator does not directly support the signal, no further actions ', async () => {
193+
const unawareAsyncThunk = createAsyncThunk('unaware', async () => {
194+
await new Promise(resolve => setTimeout(resolve, 100))
195+
return 'finished'
196+
})
197+
198+
const promise = store.dispatch(unawareAsyncThunk())
199+
promise.abort('AbortReason')
200+
const result = await promise
201+
202+
const expectedAbortedAction = {
203+
type: 'unaware/rejected',
204+
error: {
205+
message: 'AbortReason',
206+
name: 'AbortError'
207+
}
208+
}
209+
210+
// abortedAction with reason is dispatched after test/pending is dispatched
211+
expect(store.getState()).toEqual([
212+
expect.any(Object),
213+
expect.objectContaining({ type: 'unaware/pending' }),
214+
expect.objectContaining(expectedAbortedAction)
215+
])
216+
217+
// same abortedAction is returned, but with the AbortError from the abortablePayloadCreator
218+
expect(result).toMatchObject(expectedAbortedAction)
219+
220+
// calling unwrapResult on the returned object re-throws the error from the abortablePayloadCreator
221+
expect(() => unwrapResult(result)).toThrowError(
222+
expect.objectContaining(expectedAbortedAction.error)
223+
)
224+
})
191225
})

src/createAsyncThunk.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -178,20 +178,22 @@ export function createAsyncThunk<
178178
)
179179
} catch (err) {
180180
if (err && err.name === 'AbortError' && abortAction) {
181-
// abortAction has already been dispatched, no further action should be dispatched
182-
// by this thunk.
183-
// return a copy of the dispatched abortAction, but attach the AbortError to it.
184-
return { ...abortAction, error: miniSerializeError(err) }
181+
abortAction = { ...abortAction, error: miniSerializeError(err) }
185182
}
186183
finalAction = rejected(err, requestId, arg)
187184
}
188-
189185
// We dispatch the result action _after_ the catch, to avoid having any errors
190186
// here get swallowed by the try/catch block,
191187
// per https://twitter.com/dan_abramov/status/770914221638942720
192188
// and https://redux-toolkit.js.org/tutorials/advanced-tutorial#async-error-handling-logic-in-thunks
193-
dispatch(finalAction)
194-
return finalAction
189+
190+
// If abortAction has been set, we return that and do not dispatch any more fulfilled/rejected actions.
191+
if (abortAction) {
192+
return abortAction
193+
} else {
194+
dispatch(finalAction)
195+
return finalAction
196+
}
195197
})()
196198
return Object.assign(promise, { abort })
197199
}

0 commit comments

Comments
 (0)