Skip to content

Commit dc4512b

Browse files
committed
add experimental createAsyncThunk.rejectWithValue
1 parent a6e5e5f commit dc4512b

File tree

3 files changed

+83
-12
lines changed

3 files changed

+83
-12
lines changed

etc/redux-toolkit.api.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,31 +103,37 @@ export function createAction<P = void, T extends string = string>(type: T): Payl
103103
export function createAction<PA extends PrepareAction<any>, T extends string = string>(type: T, prepareAction: PA): PayloadActionCreator<ReturnType<PA>['payload'], T, PA>;
104104

105105
// @alpha (undocumented)
106-
export function createAsyncThunk<Returned, ThunkArg = void, ThunkApiConfig extends AsyncThunkConfig = {}>(type: string, payloadCreator: (arg: ThunkArg, thunkAPI: GetThunkAPI<ThunkApiConfig>) => Promise<Returned> | Returned): ((arg: ThunkArg) => (dispatch: GetDispatch<ThunkApiConfig>, getState: () => GetState<ThunkApiConfig>, extra: GetExtra<ThunkApiConfig>) => Promise<PayloadAction<Returned, string, {
106+
export function createAsyncThunk<Returned, ThunkArg = void, ThunkApiConfig extends AsyncThunkConfig = {}>(type: string, payloadCreator: (arg: ThunkArg, thunkAPI: GetThunkAPI<ThunkApiConfig>) => Promise<Returned> | Returned | Promise<RejectWithValue<GetRejectValue<ThunkApiConfig>>> | RejectWithValue<GetRejectValue<ThunkApiConfig>>): ((arg: ThunkArg) => (dispatch: GetDispatch<ThunkApiConfig>, getState: () => GetState<ThunkApiConfig>, extra: GetExtra<ThunkApiConfig>) => Promise<PayloadAction<Returned, string, {
107107
arg: ThunkArg;
108108
requestId: string;
109-
}, never> | PayloadAction<undefined, string, {
109+
}, never> | PayloadAction<GetRejectValue<ThunkApiConfig> | undefined, string, {
110110
arg: ThunkArg;
111111
requestId: string;
112-
aborted: boolean;
112+
aborted: boolean | null;
113113
}, any>> & {
114114
abort: (reason?: string | undefined) => void;
115115
}) & {
116116
pending: ActionCreatorWithPreparedPayload<[string, ThunkArg], undefined, string, never, {
117117
arg: ThunkArg;
118118
requestId: string;
119119
}>;
120-
rejected: ActionCreatorWithPreparedPayload<[Error, string, ThunkArg], undefined, string, any, {
120+
rejected: ActionCreatorWithPreparedPayload<[Error | null, string, ThunkArg, (GetRejectValue<ThunkApiConfig> | undefined)?], GetRejectValue<ThunkApiConfig> | undefined, string, any, {
121121
arg: ThunkArg;
122122
requestId: string;
123-
aborted: boolean;
123+
aborted: boolean | null;
124124
}>;
125125
fulfilled: ActionCreatorWithPreparedPayload<[Returned, string, ThunkArg], Returned, string, never, {
126126
arg: ThunkArg;
127127
requestId: string;
128128
}>;
129129
};
130130

131+
// @public (undocumented)
132+
export namespace createAsyncThunk {
133+
var // (undocumented)
134+
rejectWithValue: <RejectValue>(value: RejectValue) => RejectWithValue<RejectValue>;
135+
}
136+
131137
// @alpha (undocumented)
132138
export function createEntityAdapter<T>(options?: {
133139
selectId?: IdSelector<T>;

src/createAsyncThunk.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,30 @@ export interface SerializedError {
2929
code?: string
3030
}
3131

32-
const commonProperties: (keyof SerializedError)[] = [
32+
const commonProperties: Array<keyof SerializedError> = [
3333
'name',
3434
'message',
3535
'stack',
3636
'code'
3737
]
3838

39+
const rejectionSymbol: unique symbol = Symbol('rejectWithValue')
40+
type RejectWithValue<RejectValue> = {
41+
[rejectionSymbol]: 'reject'
42+
value: RejectValue
43+
}
44+
function rejectWithValue<RejectValue>(
45+
value: RejectValue
46+
): RejectWithValue<RejectValue> {
47+
return {
48+
[rejectionSymbol]: 'reject',
49+
value
50+
}
51+
}
52+
function isRejectWithValue(value: any): value is RejectWithValue<unknown> {
53+
return value[rejectionSymbol] === 'reject'
54+
}
55+
3956
// Reworked from https://github.com/sindresorhus/serialize-error
4057
export const miniSerializeError = (value: any): any => {
4158
if (typeof value === 'object' && value !== null) {
@@ -56,6 +73,7 @@ type AsyncThunkConfig = {
5673
state?: unknown
5774
dispatch?: Dispatch
5875
extra?: unknown
76+
rejectValue?: unknown
5977
}
6078

6179
type GetState<ThunkApiConfig> = ThunkApiConfig extends {
@@ -84,6 +102,11 @@ type GetThunkAPI<ThunkApiConfig> = BaseThunkAPI<
84102
GetExtra<ThunkApiConfig>,
85103
GetDispatch<ThunkApiConfig>
86104
>
105+
type GetRejectValue<ThunkApiConfig> = ThunkApiConfig extends {
106+
rejectValue: infer RejectValue
107+
}
108+
? RejectValue
109+
: undefined
87110

88111
/**
89112
*
@@ -101,8 +124,14 @@ export function createAsyncThunk<
101124
payloadCreator: (
102125
arg: ThunkArg,
103126
thunkAPI: GetThunkAPI<ThunkApiConfig>
104-
) => Promise<Returned> | Returned
127+
) =>
128+
| Promise<Returned>
129+
| Returned
130+
| Promise<RejectWithValue<GetRejectValue<ThunkApiConfig>>>
131+
| RejectWithValue<GetRejectValue<ThunkApiConfig>>
105132
) {
133+
type RejectedValue = GetRejectValue<ThunkApiConfig>
134+
106135
const fulfilled = createAction(
107136
type + '/fulfilled',
108137
(result: Returned, requestId: string, arg: ThunkArg) => {
@@ -125,10 +154,15 @@ export function createAsyncThunk<
125154

126155
const rejected = createAction(
127156
type + '/rejected',
128-
(error: Error, requestId: string, arg: ThunkArg) => {
157+
(
158+
error: Error | null,
159+
requestId: string,
160+
arg: ThunkArg,
161+
payload?: RejectedValue
162+
) => {
129163
const aborted = error && error.name === 'AbortError'
130164
return {
131-
payload: undefined,
165+
payload,
132166
error: miniSerializeError(error),
133167
meta: {
134168
arg,
@@ -175,7 +209,12 @@ export function createAsyncThunk<
175209
requestId,
176210
signal: abortController.signal
177211
})
178-
).then(result => fulfilled(result, requestId, arg))
212+
).then(result => {
213+
if (isRejectWithValue(result)) {
214+
return rejected(null, requestId, arg, result.value)
215+
}
216+
return fulfilled(result, requestId, arg)
217+
})
179218
])
180219
} catch (err) {
181220
finalAction = rejected(err, requestId, arg)
@@ -198,6 +237,7 @@ export function createAsyncThunk<
198237
fulfilled
199238
})
200239
}
240+
createAsyncThunk.rejectWithValue = rejectWithValue
201241

202242
/**
203243
* @alpha

type-tests/files/createAsyncThunk.typetest.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { createAsyncThunk, Dispatch, createReducer, AnyAction } from 'src'
22
import { ThunkDispatch } from 'redux-thunk'
3-
import { promises } from 'fs'
43
import { unwrapResult } from 'src/createAsyncThunk'
54

65
function expectType<T>(t: T) {
@@ -97,4 +96,30 @@ const defaultDispatch = (() => {}) as ThunkDispatch<{}, any, AnyAction>
9796
correctDispatch(fetchBooksTAC(1))
9897
// typings:expect-error
9998
defaultDispatch(fetchBooksTAC(1))
100-
})()
99+
})()(
100+
/**
101+
* returning a rejected action from the promise creator is possible
102+
*/
103+
async () => {
104+
type ReturnValue = { data: 'success' }
105+
type RejectValue = { data: 'error' }
106+
107+
const fetchBooksTAC = createAsyncThunk<
108+
ReturnValue,
109+
number,
110+
{
111+
rejectValue: RejectValue
112+
}
113+
>('books/fetch', async arg => {
114+
return createAsyncThunk.rejectWithValue<RejectValue>({ data: 'error' })
115+
})
116+
117+
const returned = await defaultDispatch(fetchBooksTAC(1))
118+
if (fetchBooksTAC.rejected.match(returned)) {
119+
expectType<undefined | RejectValue>(returned.payload)
120+
expectType<RejectValue>(returned.payload!)
121+
} else {
122+
expectType<ReturnValue>(returned.payload)
123+
}
124+
}
125+
)()

0 commit comments

Comments
 (0)