Skip to content

Commit

Permalink
add {unwrap: true} option to async thunks
Browse files Browse the repository at this point in the history
  • Loading branch information
phryneas committed Nov 18, 2020
1 parent e143b2f commit 94c67a2
Show file tree
Hide file tree
Showing 3 changed files with 227 additions and 12 deletions.
46 changes: 46 additions & 0 deletions src/createAsyncThunk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -667,3 +667,49 @@ describe('unwrapResult', () => {
await expect(unwrapPromise).rejects.toBe('rejectWithValue!')
})
})

describe('`unwrap` option behaviour', () => {
const getState = jest.fn(() => ({}))
const dispatch = jest.fn((x: any) => x)
const extra = {}
test('fulfilled case', async () => {
const asyncThunk = createAsyncThunk('test', () => {
return 'fulfilled!'
})

const promise = asyncThunk(undefined, { unwrap: true })(
dispatch,
getState,
extra
)

await expect(promise).resolves.toBe('fulfilled!')
})
test('error case', async () => {
const error = new Error('Panic!')
const asyncThunk = createAsyncThunk('test', () => {
throw error
})

const promise = asyncThunk(undefined, { unwrap: true })(
dispatch,
getState,
extra
)

await expect(promise).rejects.toEqual(miniSerializeError(error))
})
test('rejectWithValue case', async () => {
const asyncThunk = createAsyncThunk('test', (_, { rejectWithValue }) => {
return rejectWithValue('rejectWithValue!')
})

const promise = asyncThunk(undefined, { unwrap: true })(
dispatch,
getState,
extra
)

await expect(promise).rejects.toBe('rejectWithValue!')
})
})
93 changes: 81 additions & 12 deletions src/createAsyncThunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,31 +166,86 @@ export type AsyncThunkAction<
arg: ThunkArg
}

type AsyncThunkUnwrappedAction<Returned, ThunkArg, ThunkApiConfig> = (
dispatch: GetDispatch<ThunkApiConfig>,
getState: () => GetState<ThunkApiConfig>,
extra: GetExtra<ThunkApiConfig>
) => Promise<Returned> & {
abort(reason?: string): void
requestId: string
arg: ThunkArg
}

type AsyncThunkActionCreator<
Returned,
ThunkArg,
ThunkApiConfig extends AsyncThunkConfig
> = IsAny<
ThunkArg,
// any handling
(arg: ThunkArg) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>,
{
(arg: ThunkArg, opts: { unwrap: true }): AsyncThunkUnwrappedAction<
Returned,
ThunkArg,
ThunkApiConfig
>
(arg: ThunkArg): AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>
},
// unknown handling
unknown extends ThunkArg
? (arg: ThunkArg) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig> // argument not specified or specified as void or undefined
? {
(arg: ThunkArg, opts: { unwrap: true }): AsyncThunkUnwrappedAction<
Returned,
ThunkArg,
ThunkApiConfig
>
(arg: ThunkArg): AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>
} // argument not specified or specified as void or undefined
: [ThunkArg] extends [void] | [undefined]
? () => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig> // argument contains void
? {
(arg: ThunkArg, opts: { unwrap: true }): AsyncThunkUnwrappedAction<
Returned,
ThunkArg,
ThunkApiConfig
>
(): AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>
} // argument contains void
: [void] extends [ThunkArg] // make optional
? (arg?: ThunkArg) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig> // argument contains undefined
? {
(
arg: ThunkArg | undefined,
opts: { unwrap: true }
): AsyncThunkUnwrappedAction<Returned, ThunkArg, ThunkApiConfig>
(arg?: ThunkArg): AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>
} // argument contains undefined
: [undefined] extends [ThunkArg]
? WithStrictNullChecks<
// with strict nullChecks: make optional
(
arg?: ThunkArg
) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>,
{
(
arg: ThunkArg | undefined,
opts: { unwrap: true }
): AsyncThunkUnwrappedAction<Returned, ThunkArg, ThunkApiConfig>
(arg?: ThunkArg): AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>
},
// without strict null checks this will match everything, so don't make it optional
(arg: ThunkArg) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>
{
(arg: ThunkArg, opts: { unwrap: true }): AsyncThunkUnwrappedAction<
Returned,
ThunkArg,
ThunkApiConfig
>
(arg: ThunkArg): AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>
}
> // default case: normal argument
: (arg: ThunkArg) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>
: {
(arg: ThunkArg, opts: { unwrap: true }): AsyncThunkUnwrappedAction<
Returned,
ThunkArg,
ThunkApiConfig
>
(arg: ThunkArg): AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>
}
>

interface AsyncThunkOptions<
Expand Down Expand Up @@ -328,7 +383,7 @@ export function createAsyncThunk<

return {
payload: error instanceof RejectWithValue ? error.payload : undefined,
error: (options?.serializeError || miniSerializeError)(
error: ((options && options.serializeError) || miniSerializeError)(
error || 'Rejected'
) as GetSerializedErrorType<ThunkApiConfig>,
meta: {
Expand Down Expand Up @@ -371,8 +426,11 @@ If you want to use the AbortController to react to \`abort\` events, please cons
}

function actionCreator(
arg: ThunkArg
): AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig> {
arg: ThunkArg,
opts?: { unwrap: true }
):
| AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>
| AsyncThunkUnwrappedAction<Returned, ThunkArg, ThunkApiConfig> {
return (dispatch, getState, extra) => {
const requestId = nanoid()

Expand Down Expand Up @@ -446,6 +504,17 @@ If you want to use the AbortController to react to \`abort\` events, please cons
if (!skipDispatch) {
dispatch(finalAction)
}

if (opts && opts.unwrap) {
if (rejected.match(finalAction)) {
if (finalAction.meta.rejectedWithValue) {
throw finalAction.payload
}
throw (finalAction as any).error
}
return finalAction.payload as any
}

return finalAction
})()
return Object.assign(promise, { abort, requestId, arg })
Expand Down
Loading

0 comments on commit 94c67a2

Please sign in to comment.