UNPKG

@reduxjs/toolkit

Version:

The official, opinionated, batteries-included toolset for efficient Redux development

286 lines (258 loc) 7.53 kB
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 }