Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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
51 changes: 51 additions & 0 deletions docs/api/createAsyncThunk.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -769,3 +769,54 @@ const UsersComponent = (props: { id: string }) => {
// render UI here
}
```

## `createAsyncThunkCreator`

### Options

An object with the following optional fields:

- `serializeError(error: any, defaultSerializer: (error: any) => SerializedError) => GetSerializedErrorType<ThunkApiConfig>` to replace or extend the default internal serializer method with your own serialization logic.

### Return Value

`createAsyncThunkCreator` returns a Redux thunk action creator with customized options. Currently, the only option is `serializeError`.

### Example

```ts no-transpile
import { createAsyncThunkCreator, SerializedError } from '@reduxjs/toolkit'

export interface AppSerializedError extends SerializedError {
isAxiosError?: boolean
}

type ThunkApiConfig = {
state: RootState
serializedErrorType: AppSerializedError
}

const createAppAsyncThunkCreator = createAsyncThunkCreator<ThunkApiConfig>({
serializeError(error, defaultSerializer) {
const serializedError = defaultSerializer(error) as AppSerializedError
serializedError.isAxiosError = error.isAxiosError
return serializedError
},
})

function createAppAsyncThunk<
Returned,
ThunkArg = void,
T extends ThunkApiConfig = ThunkApiConfig,
>(
typePrefix: string,
payloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg, T>,
options?: AsyncThunkOptions<ThunkArg, T>,
): AsyncThunk<Returned, ThunkArg, T> {
return createAppAsyncThunkCreator<Returned, ThunkArg, T>(
typePrefix,
payloadCreator,
options,
)
}
```
70 changes: 63 additions & 7 deletions packages/toolkit/src/createAsyncThunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class FulfillWithMeta<Payload, FulfilledMeta> {
*/
export const miniSerializeError = (value: any): SerializedError => {
if (typeof value === 'object' && value !== null) {
const simpleError: SerializedError = {}
const simpleError = {} as Record<string, string>
for (const property of commonProperties) {
if (typeof value[property] === 'string') {
simpleError[property] = value[property]
Expand Down Expand Up @@ -487,11 +487,21 @@ type CreateAsyncThunk<CurriedThunkApiConfig extends AsyncThunkConfig> = {
>
}

export const createAsyncThunk = /* @__PURE__ */ (() => {
type InternalCreateAsyncThunkCreatorOptions<
ThunkApiConfig extends AsyncThunkConfig,
> = {
serializeError?: ErrorSerializer<ThunkApiConfig>
}

function internalCreateAsyncThunkCreator<
CreatorThunkApiConfig extends AsyncThunkConfig = {},
>(
creatorOptions?: InternalCreateAsyncThunkCreatorOptions<CreatorThunkApiConfig>,
): CreateAsyncThunk<CreatorThunkApiConfig> {
function createAsyncThunk<
Returned,
ThunkArg,
ThunkApiConfig extends AsyncThunkConfig,
ThunkApiConfig extends CreatorThunkApiConfig,
>(
typePrefix: string,
payloadCreator: AsyncThunkPayloadCreator<
Expand Down Expand Up @@ -542,6 +552,18 @@ export const createAsyncThunk = /* @__PURE__ */ (() => {
}),
)

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 @@ -553,9 +575,7 @@ export const createAsyncThunk = /* @__PURE__ */ (() => {
meta?: RejectedMeta,
) => ({
payload,
error: ((options && options.serializeError) || miniSerializeError)(
error || 'Rejected',
) as GetSerializedErrorType<ThunkApiConfig>,
error: getError(error || 'Rejected'),
meta: {
...((meta as any) || {}),
arg,
Expand Down Expand Up @@ -588,7 +608,10 @@ export const createAsyncThunk = /* @__PURE__ */ (() => {
const promise = (async function () {
let finalAction: ReturnType<typeof fulfilled | typeof rejected>
try {
let conditionResult = options?.condition?.(arg, { getState, extra })
let conditionResult = options?.condition?.(arg, {
getState,
extra,
})
if (isThenable(conditionResult)) {
conditionResult = await conditionResult
}
Expand Down Expand Up @@ -702,9 +725,14 @@ export const createAsyncThunk = /* @__PURE__ */ (() => {
},
)
}

createAsyncThunk.withTypes = () => createAsyncThunk

return createAsyncThunk as CreateAsyncThunk<AsyncThunkConfig>
}

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

interface UnwrappableAction {
Expand Down Expand Up @@ -744,3 +772,31 @@ 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)
}
})()
1 change: 1 addition & 0 deletions packages/toolkit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export type {

export {
createAsyncThunk,
createAsyncThunkCreator,
unwrapResult,
miniSerializeError,
} from './createAsyncThunk'
Expand Down
83 changes: 83 additions & 0 deletions packages/toolkit/src/tests/createAsyncThunk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
createReducer,
unwrapResult,
miniSerializeError,
createAsyncThunkCreator,
} from '@reduxjs/toolkit'
import { vi } from 'vitest'

Expand Down Expand Up @@ -991,3 +992,85 @@ describe('meta', () => {
expect(thunk.fulfilled.type).toBe('a/fulfilled')
})
})
describe('createAsyncThunkCreator', () => {
test('custom default serializeError only', async () => {
function serializeError() {
return 'serialized!'
}
const errorObject = 'something else!'

const store = configureStore({
reducer: (state = [], action) => [...state, action],
})

const createAsyncThunk = createAsyncThunkCreator<{
serializedErrorType: string
}>({
serializeError,
})

const asyncThunk = createAsyncThunk<
unknown,
void,
{ serializedErrorType: string }
>('test', () => Promise.reject(errorObject), { serializeError })
const rejected = await store.dispatch(asyncThunk())
if (!asyncThunk.rejected.match(rejected)) {
throw new Error()
}

const expectation = {
type: 'test/rejected',
payload: undefined,
error: 'serialized!',
meta: expect.any(Object),
}
expect(rejected).toEqual(expectation)
expect(store.getState()[2]).toEqual(expectation)
expect(rejected.error).not.toEqual(miniSerializeError(errorObject))
})

test('custom default serializeError with thunk-level override', async () => {
function defaultSerializeError() {
return 'serialized by default serializer!'
}
function thunkSerializeError() {
return 'serialized by thunk serializer!'
}
const errorObject = 'something else!'

const store = configureStore({
reducer: (state = [], action) => [...state, action],
})

const createAsyncThunk = createAsyncThunkCreator<{
serializedErrorType: string
}>({
serializeError: defaultSerializeError,
})

const thunk = createAsyncThunk<
unknown,
void,
{ serializedErrorType: string }
>('test', () => Promise.reject(errorObject), {
serializeError: thunkSerializeError,
})
const rejected = await store.dispatch(thunk())
if (!thunk.rejected.match(rejected)) {
throw new Error()
}

const thunkLevelExpectation = {
type: 'test/rejected',
payload: undefined,
error: 'serialized by thunk serializer!',
meta: expect.any(Object),
}

expect(rejected).toEqual(thunkLevelExpectation)
expect(store.getState()[2]).toEqual(thunkLevelExpectation)
expect(rejected.error).not.toEqual(miniSerializeError(errorObject))
expect(rejected.error).not.toEqual('serialized by default serializer!')
})
})