Skip to content

Commit

Permalink
createAsyncThunk return fulfilled/rejected action instead of re-… (#361)
Browse files Browse the repository at this point in the history
* createAsyncThunk return fulfilled/rejected action instead of re-trowing errors

* add unwrapResult helper
  • Loading branch information
phryneas authored Feb 15, 2020
1 parent fbba32d commit d13d26a
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 24 deletions.
16 changes: 14 additions & 2 deletions etc/redux-toolkit.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,13 @@ export function createAction<P = void, T extends string = string>(type: T): Payl
export function createAction<PA extends PrepareAction<any>, T extends string = string>(type: T, prepareAction: PA): PayloadActionCreator<ReturnType<PA>['payload'], T, PA>;

// @alpha (undocumented)
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<any>;
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, {
args: ActionParams;
requestId: string;
}, never> | import("./createAction").PayloadAction<undefined, string, {
args: ActionParams;
requestId: string;
}, Error>>) & {
pending: import("./createAction").ActionCreatorWithPreparedPayload<[string, ActionParams], undefined, string, never, {
args: ActionParams;
requestId: string;
Expand All @@ -116,6 +121,13 @@ export function createAsyncThunk<ActionType extends string, Returned, ActionPara
args: ActionParams;
requestId: string;
}>;
unwrapResult: (returned: import("./createAction").PayloadAction<Returned, string, {
args: ActionParams;
requestId: string;
}, never> | import("./createAction").PayloadAction<undefined, string, {
args: ActionParams;
requestId: string;
}, Error>) => Returned;
};

// @alpha (undocumented)
Expand Down
45 changes: 30 additions & 15 deletions src/createAsyncThunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Dispatch } from 'redux'
import nanoid from 'nanoid'
import { createAction } from './createAction'

type Await<P> = P extends PromiseLike<infer T> ? T : P

type AsyncThunksArgs<S, E, D extends Dispatch = Dispatch> = {
dispatch: D
getState: S
Expand Down Expand Up @@ -101,34 +103,47 @@ export function createAsyncThunk<
) => {
const requestId = nanoid()

let result: Returned
let finalAction: ReturnType<typeof fulfilled | typeof rejected>
try {
dispatch(pending(requestId, args))

result = (await payloadCreator(args, {
dispatch,
getState,
extra,
requestId
} as TA)) as Returned
finalAction = fulfilled(
await payloadCreator(args, {
dispatch,
getState,
extra,
requestId
} as TA),
requestId,
args
)
} catch (err) {
const serializedError = miniSerializeError(err)
dispatch(rejected(serializedError, requestId, args))
// Rethrow this so the user can handle if desired
throw err
finalAction = rejected(serializedError, requestId, args)
}

// We dispatch "success" _after_ the catch, to avoid having any errors
// here get swallowed by the try/catch block,
// per https://twitter.com/dan_abramov/status/770914221638942720
// and https://redux-toolkit.js.org/tutorials/advanced-tutorial#async-error-handling-logic-in-thunks
return dispatch(fulfilled(result!, requestId, args))
dispatch(finalAction)
return finalAction
}
}

actionCreator.pending = pending
actionCreator.rejected = rejected
actionCreator.fulfilled = fulfilled
function unwrapResult(
returned: Await<ReturnType<ReturnType<typeof actionCreator>>>
) {
if (rejected.match(returned)) {
throw returned.error
}
return returned.payload
}

return actionCreator
return Object.assign(actionCreator, {
pending,
rejected,
fulfilled,
unwrapResult
})
}
44 changes: 37 additions & 7 deletions type-tests/files/createAsyncThunk.typetest.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,56 @@
import { createAsyncThunk, Dispatch, createReducer } from 'src'
import { createAsyncThunk, Dispatch, createReducer, AnyAction } from 'src'
import { ThunkDispatch } from 'redux-thunk'
import { promises } from 'fs'

function expectType<T>(t: T) {
return t
}
function fn() {}

// basic usage
{
const dispatch = fn as ThunkDispatch<any, any, any>
;(async function() {
const dispatch = fn as ThunkDispatch<{}, any, AnyAction>

const async = createAsyncThunk('test', (id: number) =>
Promise.resolve(id * 2)
)
dispatch(async(3))

const reducer = createReducer({}, builder =>
builder
.addCase(async.pending, (_, action) => {})
.addCase(async.pending, (_, action) => {
expectType<ReturnType<typeof async['pending']>>(action)
})
.addCase(async.fulfilled, (_, action) => {
expectType<ReturnType<typeof async['fulfilled']>>(action)
expectType<number>(action.payload)
})
.addCase(async.rejected, (_, action) => {})
.addCase(async.rejected, (_, action) => {
expectType<ReturnType<typeof async['rejected']>>(action)
expectType<Error>(action.error)
})
)
}

const promise = dispatch(async(3))
const result = await promise

if (async.fulfilled.match(result)) {
expectType<ReturnType<typeof async['fulfilled']>>(result)
// typings:expect-error
expectType<ReturnType<typeof async['rejected']>>(result)
} else {
expectType<ReturnType<typeof async['rejected']>>(result)
// typings:expect-error
expectType<ReturnType<typeof async['fulfilled']>>(result)
}

promise
.then(async.unwrapResult)
.then(result => {
expectType<number>(result)
// typings:expect-error
expectType<Error>(result)
})
.catch(error => {
// catch is always any-typed, nothing we can do here
})
})()

0 comments on commit d13d26a

Please sign in to comment.