Skip to content

Commit

Permalink
tweak createAsyncThunkCreator and allow overriding idGenerator
Browse files Browse the repository at this point in the history
  • Loading branch information
EskiMojo14 committed Sep 13, 2024
1 parent 871b671 commit adc78ed
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 68 deletions.
112 changes: 47 additions & 65 deletions packages/toolkit/src/createAsyncThunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,35 +487,62 @@ type CreateAsyncThunk<CurriedThunkApiConfig extends AsyncThunkConfig> = {
>
}

type InternalCreateAsyncThunkCreatorOptions<
/**
* @public
*/
export type CreateAsyncThunkCreatorOptions<
ThunkApiConfig extends AsyncThunkConfig,
> = {
serializeError?: ErrorSerializer<ThunkApiConfig>
}
> = Pick<
AsyncThunkOptions<unknown, ThunkApiConfig>,
'serializeError' | 'idGenerator'
>

function internalCreateAsyncThunkCreator<
export function createAsyncThunkCreator<
CreatorThunkApiConfig extends AsyncThunkConfig = {},
>(
creatorOptions?: InternalCreateAsyncThunkCreatorOptions<CreatorThunkApiConfig>,
creatorOptions?: CreateAsyncThunkCreatorOptions<CreatorThunkApiConfig>,
): CreateAsyncThunk<CreatorThunkApiConfig> {
function createAsyncThunk<
Returned,
ThunkArg,
ThunkApiConfig extends CreatorThunkApiConfig,
CallThunkApiConfig extends AsyncThunkConfig,
>(
typePrefix: string,
payloadCreator: AsyncThunkPayloadCreator<
Returned,
ThunkArg,
ThunkApiConfig
OverrideThunkApiConfigs<CreatorThunkApiConfig, CallThunkApiConfig>
>,
options?: AsyncThunkOptions<
ThunkArg,
OverrideThunkApiConfigs<CreatorThunkApiConfig, CallThunkApiConfig>
>,
options?: AsyncThunkOptions<ThunkArg, ThunkApiConfig>,
): AsyncThunk<Returned, ThunkArg, ThunkApiConfig> {
): AsyncThunk<
Returned,
ThunkArg,
OverrideThunkApiConfigs<CreatorThunkApiConfig, CallThunkApiConfig>
> {
type ThunkApiConfig = OverrideThunkApiConfigs<
CreatorThunkApiConfig,
CallThunkApiConfig
>
type RejectedValue = GetRejectValue<ThunkApiConfig>
type PendingMeta = GetPendingMeta<ThunkApiConfig>
type FulfilledMeta = GetFulfilledMeta<ThunkApiConfig>
type RejectedMeta = GetRejectedMeta<ThunkApiConfig>

const {
serializeError = miniSerializeError,
// nanoid needs to be wrapped because it accepts a size argument
idGenerator = () => nanoid(),
getPendingMeta,
condition,
dispatchConditionRejection,
} = {
...creatorOptions,
...options,
}

const fulfilled: AsyncThunkFulfilledActionCreator<
Returned,
ThunkArg,
Expand Down Expand Up @@ -552,18 +579,6 @@ function internalCreateAsyncThunkCreator<
}),
)

function getError(x: unknown): GetSerializedErrorType<ThunkApiConfig> {
if (options && options.serializeError) {
return options.serializeError(x)
}

if (creatorOptions && creatorOptions.serializeError) {
return creatorOptions.serializeError(x, miniSerializeError)
}

return miniSerializeError(x) as GetSerializedErrorType<ThunkApiConfig>
}

const rejected: AsyncThunkRejectedActionCreator<ThunkArg, ThunkApiConfig> =
createAction(
typePrefix + '/rejected',
Expand All @@ -575,7 +590,9 @@ function internalCreateAsyncThunkCreator<
meta?: RejectedMeta,
) => ({
payload,
error: getError(error || 'Rejected'),
error: serializeError(
error || 'Rejected',
) as GetSerializedErrorType<ThunkApiConfig>,
meta: {
...((meta as any) || {}),
arg,
Expand All @@ -592,9 +609,7 @@ function internalCreateAsyncThunkCreator<
arg: ThunkArg,
): AsyncThunkAction<Returned, ThunkArg, Required<ThunkApiConfig>> {
return (dispatch, getState, extra) => {
const requestId = options?.idGenerator
? options.idGenerator(arg)
: nanoid()
const requestId = idGenerator(arg)

const abortController = new AbortController()
let abortHandler: (() => void) | undefined
Expand All @@ -608,7 +623,7 @@ function internalCreateAsyncThunkCreator<
const promise = (async function () {
let finalAction: ReturnType<typeof fulfilled | typeof rejected>
try {
let conditionResult = options?.condition?.(arg, {
let conditionResult = condition?.(arg, {
getState,
extra,
})
Expand Down Expand Up @@ -637,10 +652,7 @@ function internalCreateAsyncThunkCreator<
pending(
requestId,
arg,
options?.getPendingMeta?.(
{ requestId, arg },
{ getState, extra },
),
getPendingMeta?.({ requestId, arg }, { getState, extra }),
) as any,
)
finalAction = await Promise.race([
Expand Down Expand Up @@ -689,8 +701,7 @@ function internalCreateAsyncThunkCreator<
// and https://github.com/reduxjs/redux-toolkit/blob/e85eb17b39a2118d859f7b7746e0f3fee523e089/docs/tutorials/advanced-tutorial.md#async-error-handling-logic-in-thunks

const skipDispatch =
options &&
!options.dispatchConditionRejection &&
!dispatchConditionRejection &&
rejected.match(finalAction) &&
(finalAction as any).meta.condition

Expand Down Expand Up @@ -728,12 +739,11 @@ function internalCreateAsyncThunkCreator<

createAsyncThunk.withTypes = () => createAsyncThunk

return createAsyncThunk as CreateAsyncThunk<AsyncThunkConfig>
return createAsyncThunk as CreateAsyncThunk<CreatorThunkApiConfig>
}

export const createAsyncThunk = /* @__PURE__ */ (() => {
return internalCreateAsyncThunkCreator() as CreateAsyncThunk<AsyncThunkConfig>
})()
export const createAsyncThunk =
/* @__PURE__ */ createAsyncThunkCreator<AsyncThunkConfig>()

interface UnwrappableAction {
payload: any
Expand Down Expand Up @@ -772,31 +782,3 @@ function isThenable(value: any): value is PromiseLike<any> {
typeof value.then === 'function'
)
}

/**
* An error serializer function that can be used to serialize errors into plain objects.
*
* @param error - The error to serialize
* @param defaultSerializer - The original default serializer `miniSerializeError` https://redux-toolkit.js.org/api/other-exports/#miniserializeerror
*
* @public
*/
type ErrorSerializer<ThunkApiConfig extends AsyncThunkConfig> = (
error: any,
defaultSerializer: (error: any) => SerializedError,
) => GetSerializedErrorType<ThunkApiConfig>

/**
* @public
*/
type CreateAsyncThunkCreatorOptions<ThunkApiConfig extends AsyncThunkConfig> = {
serializeError?: ErrorSerializer<ThunkApiConfig>
}

export const createAsyncThunkCreator = /* @__PURE__ */ (() => {
return <ThunkApiConfig extends AsyncThunkConfig = {}>(
options: CreateAsyncThunkCreatorOptions<ThunkApiConfig>,
): CreateAsyncThunk<ThunkApiConfig> => {
return internalCreateAsyncThunkCreator(options)
}
})()
47 changes: 44 additions & 3 deletions packages/toolkit/src/tests/createAsyncThunk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1035,7 +1035,7 @@ describe('createAsyncThunkCreator', () => {
return 'serialized by default serializer!'
}
function thunkSerializeError() {
return 'serialized by thunk serializer!'
return { message: 'serialized by thunk serializer!' }
}
const errorObject = 'something else!'

Expand All @@ -1052,7 +1052,11 @@ describe('createAsyncThunkCreator', () => {
const thunk = createAsyncThunk<
unknown,
void,
{ serializedErrorType: string }
{
serializedErrorType: {
message: string
}
}
>('test', () => Promise.reject(errorObject), {
serializeError: thunkSerializeError,
})
Expand All @@ -1064,7 +1068,7 @@ describe('createAsyncThunkCreator', () => {
const thunkLevelExpectation = {
type: 'test/rejected',
payload: undefined,
error: 'serialized by thunk serializer!',
error: { message: 'serialized by thunk serializer!' },
meta: expect.any(Object),
}

Expand All @@ -1073,4 +1077,41 @@ describe('createAsyncThunkCreator', () => {
expect(rejected.error).not.toEqual(miniSerializeError(errorObject))
expect(rejected.error).not.toEqual('serialized by default serializer!')
})
test('custom default idGenerator only', async () => {
function idGenerator(arg: unknown) {
return `${arg}`
}
const createAsyncThunk = createAsyncThunkCreator({
idGenerator,
})
const asyncThunk = createAsyncThunk<number, string>('test', async () => 1)
const store = configureStore({
reducer: (state = [], action) => [...state, action],
})
const promise = store.dispatch(asyncThunk('testArg'))
expect(promise.requestId).toBe('testArg')
const result = await promise
expect(result.meta.requestId).toBe('testArg')
})
test('custom default idGenerator with thunk-level override', async () => {
function defaultIdGenerator(arg: unknown) {
return `default-${arg}`
}
function thunkIdGenerator(arg: unknown) {
return `thunk-${arg}`
}
const createAsyncThunk = createAsyncThunkCreator({
idGenerator: defaultIdGenerator,
})
const thunk = createAsyncThunk<number, string>('test', async () => 1, {
idGenerator: thunkIdGenerator,
})
const store = configureStore({
reducer: (state = [], action) => [...state, action],
})
const promise = store.dispatch(thunk('testArg'))
expect(promise.requestId).toBe('thunk-testArg')
const result = await promise
expect(result.meta.requestId).toBe('thunk-testArg')
})
})

0 comments on commit adc78ed

Please sign in to comment.