Skip to content

Commit 01e8518

Browse files
author
Lenz Weber
authored
fix unwrapResult behaviour (#704)
* fix unwrapResult behaviour this fixes #701 * fix inconsistent type, add api report * udpate docs * remove another useless `unwrapResult`
1 parent 7fda27a commit 01e8518

File tree

6 files changed

+98
-57
lines changed

6 files changed

+98
-57
lines changed

docs/api/createAsyncThunk.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ const onClick = () => {
234234

235235
The thunks generated by `createAsyncThunk` **will always return a resolved promise** with either the `fulfilled` action object or `rejected` action object inside, as appropriate.
236236

237-
The calling logic may wish to treat these actions as if they were the original promise contents. Redux Toolkit exports an `unwrapResult` function that can be used to extract the `payload` or `error` from the action, and return or throw the result appropriately:
237+
The calling logic may wish to treat these actions as if they were the original promise contents. Redux Toolkit exports an `unwrapResult` function that can be used to extract the `payload` of a `fulfilled` action or to throw either the `error` or, if available, `payload` created by `rejectWithValue` from a `rejected` action:
238238

239239
```js
240240
import { unwrapResult } from '@reduxjs/toolkit'
@@ -244,7 +244,7 @@ const onClick = () => {
244244
dispatch(fetchUserById(userId))
245245
.then(unwrapResult)
246246
.then(originalPromiseResult => {})
247-
.catch(serializedError => {})
247+
.catch(rejectedValueOrSerializedError => {})
248248
}
249249
```
250250

@@ -437,7 +437,7 @@ const fetchUserById = createAsyncThunk(
437437
- Requesting a user by ID, with loading state, and only one request at a time:
438438

439439
```js
440-
import { createAsyncThunk, createSlice, unwrapResult } from '@reduxjs/toolkit'
440+
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
441441
import { userAPI } from './userAPI'
442442

443443
const fetchUserById = createAsyncThunk(
@@ -494,7 +494,7 @@ const UsersComponent = () => {
494494
const fetchOneUser = async userId => {
495495
try {
496496
const resultAction = await dispatch(fetchUserById(userId))
497-
const user = unwrapResult(resultAction)
497+
const user = resultAction.payload
498498
showToast('success', `Fetched ${user.name}`)
499499
} catch (err) {
500500
showToast('error', `Fetch failed: ${err.message}`)

docs/usage/usage-with-typescript.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -582,7 +582,7 @@ const usersSlice = createSlice({
582582
const handleUpdateUser = async userData => {
583583
const resultAction = await dispatch(updateUser(userData))
584584
if (updateUser.fulfilled.match(resultAction)) {
585-
const user = unwrapResult(resultAction)
585+
const user = resultAction.payload
586586
showToast('success', `Updated ${user.name}`)
587587
} else {
588588
if (resultAction.payload) {

etc/redux-toolkit.api.md

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,7 @@ export type AsyncThunk<Returned, ThunkArg, ThunkApiConfig extends AsyncThunkConf
7171
};
7272

7373
// @public
74-
export type AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig extends AsyncThunkConfig> = (dispatch: GetDispatch<ThunkApiConfig>, getState: () => GetState<ThunkApiConfig>, extra: GetExtra<ThunkApiConfig>) => Promise<PayloadAction<Returned, string, {
75-
arg: ThunkArg;
76-
requestId: string;
77-
}> | PayloadAction<undefined | GetRejectValue<ThunkApiConfig>, string, {
78-
arg: ThunkArg;
79-
requestId: string;
80-
aborted: boolean;
81-
condition: boolean;
82-
}, SerializedError>> & {
74+
export type AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig extends AsyncThunkConfig> = (dispatch: GetDispatch<ThunkApiConfig>, getState: () => GetState<ThunkApiConfig>, extra: GetExtra<ThunkApiConfig>) => Promise<ReturnType<AsyncThunkFulfilledActionCreator<Returned, ThunkArg>> | ReturnType<AsyncThunkRejectedActionCreator<ThunkArg, ThunkApiConfig>>> & {
8375
abort(reason?: string): void;
8476
requestId: string;
8577
arg: ThunkArg;
@@ -398,7 +390,7 @@ export { ThunkAction }
398390
export { ThunkDispatch }
399391

400392
// @public (undocumented)
401-
export function unwrapResult<R extends ActionTypesWithOptionalErrorAction>(returned: R): PayloadForActionTypesExcludingErrorActions<R>;
393+
export function unwrapResult<R extends UnwrappableAction>(action: R): UnwrappedActionPayload<R>;
402394

403395
// @public (undocumented)
404396
export type Update<T> = {

src/createAsyncThunk.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,7 @@ describe('conditional skipping of asyncThunks', () => {
587587
meta: {
588588
aborted: false,
589589
arg: arg,
590+
rejectedWithValue: false,
590591
condition: true,
591592
requestId: expect.stringContaining('')
592593
},
@@ -596,3 +597,42 @@ describe('conditional skipping of asyncThunks', () => {
596597
)
597598
})
598599
})
600+
describe('unwrapResult', () => {
601+
const getState = jest.fn(() => ({}))
602+
const dispatch = jest.fn((x: any) => x)
603+
const extra = {}
604+
test('fulfilled case', async () => {
605+
const asyncThunk = createAsyncThunk('test', () => {
606+
return 'fulfilled!'
607+
})
608+
609+
const unwrapPromise = asyncThunk()(dispatch, getState, extra).then(
610+
unwrapResult
611+
)
612+
613+
await expect(unwrapPromise).resolves.toBe('fulfilled!')
614+
})
615+
test('error case', async () => {
616+
const error = new Error('Panic!')
617+
const asyncThunk = createAsyncThunk('test', () => {
618+
throw error
619+
})
620+
621+
const unwrapPromise = asyncThunk()(dispatch, getState, extra).then(
622+
unwrapResult
623+
)
624+
625+
await expect(unwrapPromise).rejects.toEqual(miniSerializeError(error))
626+
})
627+
test('rejectWithValue case', async () => {
628+
const asyncThunk = createAsyncThunk('test', (_, { rejectWithValue }) => {
629+
return rejectWithValue('rejectWithValue!')
630+
})
631+
632+
const unwrapPromise = asyncThunk()(dispatch, getState, extra).then(
633+
unwrapResult
634+
)
635+
636+
await expect(unwrapPromise).rejects.toBe('rejectWithValue!')
637+
})
638+
})

src/createAsyncThunk.ts

Lines changed: 33 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ const commonProperties: Array<keyof SerializedError> = [
4343
]
4444

4545
class RejectWithValue<RejectValue> {
46-
constructor(public readonly value: RejectValue) {}
46+
public name = 'RejectWithValue'
47+
public message = 'Rejected'
48+
constructor(public readonly payload: RejectValue) {}
4749
}
4850

4951
// Reworked from https://github.com/sindresorhus/serialize-error
@@ -148,18 +150,8 @@ export type AsyncThunkAction<
148150
getState: () => GetState<ThunkApiConfig>,
149151
extra: GetExtra<ThunkApiConfig>
150152
) => Promise<
151-
| PayloadAction<Returned, string, { arg: ThunkArg; requestId: string }>
152-
| PayloadAction<
153-
undefined | GetRejectValue<ThunkApiConfig>,
154-
string,
155-
{
156-
arg: ThunkArg
157-
requestId: string
158-
aborted: boolean
159-
condition: boolean
160-
},
161-
SerializedError
162-
>
153+
| ReturnType<AsyncThunkFulfilledActionCreator<Returned, ThunkArg>>
154+
| ReturnType<AsyncThunkRejectedActionCreator<ThunkArg, ThunkApiConfig>>
163155
> & {
164156
abort(reason?: string): void
165157
requestId: string
@@ -234,18 +226,14 @@ type AsyncThunkRejectedActionCreator<
234226
ThunkArg,
235227
ThunkApiConfig
236228
> = ActionCreatorWithPreparedPayload<
237-
[
238-
Error | null,
239-
string,
240-
ThunkArg,
241-
(GetRejectValue<ThunkApiConfig> | undefined)?
242-
],
229+
[Error | null, string, ThunkArg],
243230
GetRejectValue<ThunkApiConfig> | undefined,
244231
string,
245232
SerializedError,
246233
{
247234
arg: ThunkArg
248235
requestId: string
236+
rejectedWithValue: boolean
249237
aborted: boolean
250238
condition: boolean
251239
}
@@ -323,20 +311,18 @@ export function createAsyncThunk<
323311

324312
const rejected = createAction(
325313
typePrefix + '/rejected',
326-
(
327-
error: Error | null,
328-
requestId: string,
329-
arg: ThunkArg,
330-
payload?: RejectedValue
331-
) => {
314+
(error: Error | null, requestId: string, arg: ThunkArg) => {
315+
const rejectedWithValue = error instanceof RejectWithValue
332316
const aborted = !!error && error.name === 'AbortError'
333317
const condition = !!error && error.name === 'ConditionError'
318+
334319
return {
335-
payload,
336-
error: miniSerializeError(error || 'Rejected'),
320+
payload: error instanceof RejectWithValue ? error.payload : undefined,
321+
error: miniSerializeError(error),
337322
meta: {
338323
arg,
339324
requestId,
325+
rejectedWithValue,
340326
aborted,
341327
condition
342328
}
@@ -426,7 +412,7 @@ If you want to use the AbortController to react to \`abort\` events, please cons
426412
})
427413
).then(result => {
428414
if (result instanceof RejectWithValue) {
429-
return rejected(null, requestId, arg, result.value)
415+
return rejected(result, requestId, arg)
430416
}
431417
return fulfilled(result, requestId, arg)
432418
})
@@ -469,25 +455,30 @@ If you want to use the AbortController to react to \`abort\` events, please cons
469455
)
470456
}
471457

472-
type ActionTypesWithOptionalErrorAction =
473-
| { error: any }
474-
| { error?: never; payload: any }
475-
type PayloadForActionTypesExcludingErrorActions<T> = T extends { error: any }
476-
? never
477-
: T extends { payload: infer P }
478-
? P
479-
: never
458+
interface UnwrappableAction {
459+
payload: any
460+
meta?: any
461+
error?: any
462+
}
463+
464+
type UnwrappedActionPayload<T extends UnwrappableAction> = Exclude<
465+
T,
466+
{ error: any }
467+
>['payload']
480468

481469
/**
482470
* @public
483471
*/
484-
export function unwrapResult<R extends ActionTypesWithOptionalErrorAction>(
485-
returned: R
486-
): PayloadForActionTypesExcludingErrorActions<R> {
487-
if ('error' in returned) {
488-
throw returned.error
472+
export function unwrapResult<R extends UnwrappableAction>(
473+
action: R
474+
): UnwrappedActionPayload<R> {
475+
if (action.meta && action.meta.rejectedWithValue) {
476+
throw action.payload
477+
}
478+
if (action.error) {
479+
throw action.error
489480
}
490-
return (returned as any).payload
481+
return action.payload
491482
}
492483

493484
type WithStrictNullChecks<True, False> = undefined extends boolean

type-tests/files/createAsyncThunk.typetest.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable no-lone-blocks */
12
import { createAsyncThunk, Dispatch, createReducer, AnyAction } from 'src'
23
import { ThunkDispatch } from 'redux-thunk'
34
import { unwrapResult, SerializedError } from 'src/createAsyncThunk'
@@ -129,6 +130,10 @@ const defaultDispatch = (() => {}) as ThunkDispatch<{}, any, AnyAction>
129130
} else {
130131
expectType<ReturnValue>(returned.payload)
131132
}
133+
134+
expectType<ReturnValue>(unwrapResult(returned))
135+
// typings:expect-error
136+
expectType<RejectValue>(unwrapResult(returned))
132137
})()
133138

134139
{
@@ -184,6 +189,19 @@ const defaultDispatch = (() => {}) as ThunkDispatch<{}, any, AnyAction>
184189
expectType<IsAny<typeof result['error'], true, false>>(true)
185190
}
186191
}
192+
defaultDispatch(fetchLiveCallsError('asd'))
193+
.then(result => {
194+
expectType<Item[] | ErrorFromServer | undefined>(result.payload)
195+
// typings:expect-error
196+
expectType<Item[]>(unwrapped)
197+
return result
198+
})
199+
.then(unwrapResult)
200+
.then(unwrapped => {
201+
expectType<Item[]>(unwrapped)
202+
// typings:expect-error
203+
expectType<ErrorFromServer>(unwrapResult(unwrapped))
204+
})
187205
})
188206
}
189207

0 commit comments

Comments
 (0)