Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

createAsyncThunk return fulfilled/rejected action instead of re-… #361

Merged
merged 2 commits into from
Feb 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
})
})()