@reduxjs/toolkit
Version:
The official, opinionated, batteries-included toolset for efficient Redux development
286 lines (258 loc) • 7.53 kB
text/typescript
import { Dispatch, AnyAction } from 'redux'
import {
createAction,
PayloadAction,
ActionCreatorWithPreparedPayload
} from './createAction'
import { ThunkDispatch } from 'redux-thunk'
import { FallbackIfUnknown } from './tsHelpers'
import { nanoid } from './nanoid'
// @ts-ignore we need the import of these types due to a bundling issue.
type _Keep = PayloadAction | ActionCreatorWithPreparedPayload<any, unknown>
export type BaseThunkAPI<
S,
E,
D extends Dispatch = Dispatch,
RejectedValue = undefined
> = {
dispatch: D
getState: () => S
extra: E
requestId: string
signal: AbortSignal
rejectWithValue(value: RejectedValue): RejectWithValue<RejectedValue>
}
/**
* @alpha
*/
export interface SerializedError {
name?: string
message?: string
stack?: string
code?: string
}
const commonProperties: Array<keyof SerializedError> = [
'name',
'message',
'stack',
'code'
]
class RejectWithValue<RejectValue> {
constructor(public readonly value: RejectValue) {}
}
// Reworked from https://github.com/sindresorhus/serialize-error
export const miniSerializeError = (value: any): SerializedError => {
if (typeof value === 'object' && value !== null) {
const simpleError: SerializedError = {}
for (const property of commonProperties) {
if (typeof value[property] === 'string') {
simpleError[property] = value[property]
}
}
return simpleError
}
return { message: String(value) }
}
type AsyncThunkConfig = {
state?: unknown
dispatch?: Dispatch
extra?: unknown
rejectValue?: unknown
}
type GetState<ThunkApiConfig> = ThunkApiConfig extends {
state: infer State
}
? State
: unknown
type GetExtra<ThunkApiConfig> = ThunkApiConfig extends { extra: infer Extra }
? Extra
: unknown
type GetDispatch<ThunkApiConfig> = ThunkApiConfig extends {
dispatch: infer Dispatch
}
? FallbackIfUnknown<
Dispatch,
ThunkDispatch<
GetState<ThunkApiConfig>,
GetExtra<ThunkApiConfig>,
AnyAction
>
>
: ThunkDispatch<GetState<ThunkApiConfig>, GetExtra<ThunkApiConfig>, AnyAction>
type GetThunkAPI<ThunkApiConfig> = BaseThunkAPI<
GetState<ThunkApiConfig>,
GetExtra<ThunkApiConfig>,
GetDispatch<ThunkApiConfig>,
GetRejectValue<ThunkApiConfig>
>
type GetRejectValue<ThunkApiConfig> = ThunkApiConfig extends {
rejectValue: infer RejectValue
}
? RejectValue
: unknown
/**
*
* @param type
* @param payloadCreator
*
* @alpha
*/
export function createAsyncThunk<
Returned,
ThunkArg = void,
ThunkApiConfig extends AsyncThunkConfig = {}
>(
type: string,
payloadCreator: (
arg: ThunkArg,
thunkAPI: GetThunkAPI<ThunkApiConfig>
) =>
| Promise<Returned | RejectWithValue<GetRejectValue<ThunkApiConfig>>>
| Returned
| RejectWithValue<GetRejectValue<ThunkApiConfig>>
) {
type RejectedValue = GetRejectValue<ThunkApiConfig>
const fulfilled = createAction(
type + '/fulfilled',
(result: Returned, requestId: string, arg: ThunkArg) => {
return {
payload: result,
meta: { arg, requestId }
}
}
)
const pending = createAction(
type + '/pending',
(requestId: string, arg: ThunkArg) => {
return {
payload: undefined,
meta: { arg, requestId }
}
}
)
const rejected = createAction(
type + '/rejected',
(
error: Error | null,
requestId: string,
arg: ThunkArg,
payload?: RejectedValue
) => {
const aborted = !!error && error.name === 'AbortError'
return {
payload,
error: miniSerializeError(error || 'Rejected'),
meta: {
arg,
requestId,
aborted
}
}
}
)
let displayedWarning = false
const AC =
typeof AbortController !== 'undefined'
? AbortController
: class implements AbortController {
signal: AbortSignal = {
aborted: false,
addEventListener() {},
dispatchEvent() {
return false
},
onabort() {},
removeEventListener() {}
}
abort() {
if (process.env.NODE_ENV !== 'production') {
if (!displayedWarning) {
displayedWarning = true
console.info(
`This platform does not implement AbortController.
If you want to use the AbortController to react to \`abort\` events, please consider importing a polyfill like 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'.`
)
}
}
}
}
function actionCreator(arg: ThunkArg) {
return (
dispatch: GetDispatch<ThunkApiConfig>,
getState: () => GetState<ThunkApiConfig>,
extra: GetExtra<ThunkApiConfig>
) => {
const requestId = nanoid()
const abortController = new AC()
let abortReason: string | undefined
const abortedPromise = new Promise<never>((_, reject) =>
abortController.signal.addEventListener('abort', () =>
reject({ name: 'AbortError', message: abortReason || 'Aborted' })
)
)
function abort(reason?: string) {
abortReason = reason
abortController.abort()
}
const promise = (async function() {
let finalAction: ReturnType<typeof fulfilled | typeof rejected>
try {
dispatch(pending(requestId, arg))
finalAction = await Promise.race([
abortedPromise,
Promise.resolve(
payloadCreator(arg, {
dispatch,
getState,
extra,
requestId,
signal: abortController.signal,
rejectWithValue(value: RejectedValue) {
return new RejectWithValue(value)
}
})
).then(result => {
if (result instanceof RejectWithValue) {
return rejected(null, requestId, arg, result.value)
}
return fulfilled(result, requestId, arg)
})
])
} catch (err) {
finalAction = rejected(err, requestId, arg)
}
// We dispatch the result action _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
dispatch(finalAction)
return finalAction
})()
return Object.assign(promise, { abort })
}
}
return Object.assign(actionCreator, {
pending,
rejected,
fulfilled
})
}
type ActionTypesWithOptionalErrorAction =
| { error: any }
| { error?: never; payload: any }
type PayloadForActionTypesExcludingErrorActions<T> = T extends { error: any }
? never
: T extends { payload: infer P }
? P
: never
/**
* @alpha
*/
export function unwrapResult<R extends ActionTypesWithOptionalErrorAction>(
returned: R
): PayloadForActionTypesExcludingErrorActions<R> {
if ('error' in returned) {
throw returned.error
}
return (returned as any).payload
}