跳至主要内容

createAsyncThunk

概述

一个接受 Redux action 类型字符串和回调函数的函数,该回调函数应返回一个 promise。它根据您传入的动作类型前缀生成 promise 生命周期动作类型,并返回一个 thunk 动作创建者,该创建者将运行 promise 回调并根据返回的 promise 分派生命周期动作。

本节概述了处理异步请求生命周期的标准推荐方法。

它不会生成任何 reducer 函数,因为它不知道您要获取什么数据、如何跟踪加载状态,以及如何处理返回的数据。您应该编写自己的 reducer 逻辑来处理这些操作,并使用适合您应用程序的加载状态和处理逻辑。

提示

Redux Toolkit 的 RTK Query 数据获取 API 是一个专为 Redux 应用程序构建的数据获取和缓存解决方案,可以消除编写任何 thunk 或 reducer 来管理数据获取的需要。我们鼓励您尝试一下,看看它是否可以帮助简化您应用程序中的数据获取代码!

示例用法

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'

// First, create the thunk
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId: number, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
},
)

interface UsersState {
entities: User[]
loading: 'idle' | 'pending' | 'succeeded' | 'failed'
}

const initialState = {
entities: [],
loading: 'idle',
} satisfies UserState as UsersState

// Then, handle actions in your reducers:
const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {
// standard reducer logic, with auto-generated action types per reducer
},
extraReducers: (builder) => {
// Add reducers for additional action types here, and handle loading state as needed
builder.addCase(fetchUserById.fulfilled, (state, action) => {
// Add user to the state array
state.entities.push(action.payload)
})
},
})

// Later, dispatch the thunk as needed in the app
dispatch(fetchUserById(123))

参数

createAsyncThunk 接受三个参数:一个字符串操作 type 值、一个 payloadCreator 回调函数和一个 options 对象。

type

一个字符串,用于生成额外的 Redux 操作类型常量,表示异步请求的生命周期。

例如,type 参数为 'users/requestStatus' 将生成以下操作类型:

  • pending: 'users/requestStatus/pending'
  • fulfilled: 'users/requestStatus/fulfilled'
  • rejected: 'users/requestStatus/rejected'

payloadCreator

这是一个回调函数,应该返回一个包含异步逻辑结果的 Promise。它也可以同步返回一个值。如果出现错误,它应该返回一个包含 `Error` 实例的拒绝 Promise 或一个简单的值,例如描述性错误消息,或者返回一个包含 `RejectWithValue` 参数的已解决 Promise,该参数由 `thunkAPI.rejectWithValue` 函数返回。

payloadCreator 函数可以包含您需要计算适当结果的任何逻辑。这可能包括标准的 AJAX 数据获取请求、将结果组合成最终值的多个 AJAX 调用、与 React Native `AsyncStorage` 的交互等等。

payloadCreator 函数将使用两个参数调用

  • arg:一个单一的值,包含在分派 thunk 操作创建者时传递给它的第一个参数。这对于传递像项目 ID 这样的值很有用,这些值可能作为请求的一部分需要。如果您需要传递多个值,请在分派 thunk 时将它们一起传递到一个对象中,例如 `dispatch(fetchUsers({status: 'active', sortBy: 'name'}))`。
  • thunkAPI:一个包含通常传递给 Redux thunk 函数的所有参数以及其他选项的对象
    • dispatch:Redux 存储的 `dispatch` 方法
    • getState:Redux 存储的 `getState` 方法
    • extra:如果可用,在设置时传递给 thunk 中间件的“额外参数”
    • requestId:一个自动生成的唯一字符串 ID 值,用于标识此请求序列
    • signal:一个 AbortController.signal 对象,可用于查看应用程序逻辑的另一个部分是否已将此请求标记为需要取消。
    • rejectWithValue(value, [meta]): rejectWithValue 是一个实用函数,您可以在您的 action creator 中 return(或 throw)它,以返回一个带有定义的有效负载和元数据的拒绝响应。它将传递您给它的任何值,并在拒绝 action 的有效负载中返回它。如果您还传递了一个 meta,它将与现有的 rejectedAction.meta 合并。
    • fulfillWithValue(value, meta): fulfillWithValue 是一个实用函数,您可以在您的 action creator 中 return 它,以使用一个值 fulfill,同时能够添加到 fulfilledAction.meta 中。

payloadCreator 函数中的逻辑可以使用这些值中的任何一个来计算结果。

选项

具有以下可选字段的对象

  • condition(arg, { getState, extra } ): boolean | Promise<boolean>: 一个回调函数,可以用来跳过有效负载创建者和所有 action 分派的执行,如果需要。有关完整描述,请参见 执行前取消
  • dispatchConditionRejection: 如果 condition() 返回 false,则默认行为是不分派任何 action。如果您仍然希望在 thunk 被取消时分派一个“拒绝”action,请将此标志设置为 true
  • idGenerator(arg): string: 用于生成请求序列的 requestId 的函数。默认情况下使用 nanoid,但您可以实现自己的 ID 生成逻辑。
  • serializeError(error: unknown) => any 用于用您自己的序列化逻辑替换内部 miniSerializeError 方法。
  • getPendingMeta({ arg, requestId }, { getState, extra }): any: 用于创建一个将合并到 pendingAction.meta 字段中的对象的函数。

返回值

createAsyncThunk 返回一个标准的 Redux thunk action creator。thunk action creator 函数将具有用于 pendingfulfilledrejected 情况的普通 action creator,作为嵌套字段附加。

使用上面的 fetchUserById 示例,createAsyncThunk 将生成四个函数

  • fetchUserById 是一个 thunk action creator,它会启动你编写的异步 payload 回调函数。
    • fetchUserById.pending 是一个 action creator,它会派发一个 'users/fetchByIdStatus/pending' action。
    • fetchUserById.fulfilled 是一个 action creator,它会派发一个 'users/fetchByIdStatus/fulfilled' action。
    • fetchUserById.rejected 是一个 action creator,它会派发一个 'users/fetchByIdStatus/rejected' action。

当被派发时,thunk 会:

  • 派发 pending action。
  • 调用 payloadCreator 回调函数,并等待返回的 promise 解决。
  • 当 promise 解决时:
    • 如果 promise 成功解决,则派发 fulfilled action,并将 promise 的值作为 action.payload
    • 如果 promise 使用 rejectWithValue(value) 返回值解决,则派发 rejected action,并将传递给 action.payload 的值作为 action.payload,并将 'Rejected' 作为 action.error.message
    • 如果 promise 失败且未使用 rejectWithValue 处理,则派发 rejected action,并将错误值的序列化版本作为 action.error
  • 返回一个包含最终派发 action(fulfilledrejected action 对象)的已解决 promise。

Promise 生命周期 Actions

createAsyncThunk 将使用 createAction 生成三个 Redux action creator:pendingfulfilledrejected。每个生命周期 action creator 将附加到返回的 thunk action creator,以便你的 reducer 逻辑可以引用 action 类型并在派发时响应 action。每个 action 对象都将在 action.meta 下包含当前唯一的 requestIdarg 值。

action creator 将具有以下签名

interface SerializedError {
name?: string
message?: string
code?: string
stack?: string
}

interface PendingAction<ThunkArg> {
type: string
payload: undefined
meta: {
requestId: string
arg: ThunkArg
}
}

interface FulfilledAction<ThunkArg, PromiseResult> {
type: string
payload: PromiseResult
meta: {
requestId: string
arg: ThunkArg
}
}

interface RejectedAction<ThunkArg> {
type: string
payload: undefined
error: SerializedError | any
meta: {
requestId: string
arg: ThunkArg
aborted: boolean
condition: boolean
}
}

interface RejectedWithValueAction<ThunkArg, RejectedValue> {
type: string
payload: RejectedValue
error: { message: 'Rejected' }
meta: {
requestId: string
arg: ThunkArg
aborted: boolean
}
}

type Pending = <ThunkArg>(
requestId: string,
arg: ThunkArg,
) => PendingAction<ThunkArg>

type Fulfilled = <ThunkArg, PromiseResult>(
payload: PromiseResult,
requestId: string,
arg: ThunkArg,
) => FulfilledAction<ThunkArg, PromiseResult>

type Rejected = <ThunkArg>(
requestId: string,
arg: ThunkArg,
) => RejectedAction<ThunkArg>

type RejectedWithValue = <ThunkArg, RejectedValue>(
requestId: string,
arg: ThunkArg,
) => RejectedWithValueAction<ThunkArg, RejectedValue>

要在你的 reducer 中处理这些 action,请使用“构建器回调”符号在 createReducercreateSlice 中引用 action creator。

const reducer1 = createReducer(initialState, (builder) => {
builder.addCase(fetchUserById.fulfilled, (state, action) => {})
})

const reducer2 = createSlice({
name: 'users',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchUserById.fulfilled, (state, action) => {})
},
})

此外,还附加了一个 settled 匹配器,用于匹配已解决和已拒绝的 action。从概念上讲,这类似于 finally 块。

确保使用 addMatcher 而不是 addCase,因为 settled 是一个匹配器而不是一个 action creator。

const reducer1 = createReducer(initialState, (builder) => {
builder.addMatcher(fetchUserById.settled, (state, action) => {})
})

const reducer2 = createSlice({
name: 'users',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addMatcher(fetchUserById.settled, (state, action) => {})
},
})

处理 Thunk 结果

解包结果操作

Thunk 在调度时可能会返回值。一个常见的用例是从 thunk 返回一个 promise,从组件调度 thunk,然后等待 promise 解决,然后再进行其他工作。

const onClick = () => {
dispatch(fetchUserById(userId)).then(() => {
// do additional work
})
}

createAsyncThunk 生成的 thunk **将始终返回一个已解决的 promise**,其中包含 fulfilled 操作对象或 rejected 操作对象,具体取决于情况。

调用逻辑可能希望将这些操作视为原始 promise 内容。调度后的 thunk 返回的 promise 具有一个 unwrap 属性,可以调用它来提取 fulfilled 操作的 payload,或者从 rejected 操作中抛出 error 或(如果可用)由 rejectWithValue 创建的 payload

// in the component

const onClick = () => {
dispatch(fetchUserById(userId))
.unwrap()
.then((originalPromiseResult) => {
// handle result here
})
.catch((rejectedValueOrSerializedError) => {
// handle error here
})
}

或者使用 async/await 语法

// in the component

const onClick = async () => {
try {
const originalPromiseResult = await dispatch(fetchUserById(userId)).unwrap()
// handle result here
} catch (rejectedValueOrSerializedError) {
// handle error here
}
}

在大多数情况下,建议使用附加的 .unwrap() 属性,但是 Redux Toolkit 还导出一个 unwrapResult 函数,可以用于类似目的。

import { unwrapResult } from '@reduxjs/toolkit'

// in the component
const onClick = () => {
dispatch(fetchUserById(userId))
.then(unwrapResult)
.then((originalPromiseResult) => {
// handle result here
})
.catch((rejectedValueOrSerializedError) => {
// handle result here
})
}

或者使用 async/await 语法

import { unwrapResult } from '@reduxjs/toolkit'

// in the component
const onClick = async () => {
try {
const resultAction = await dispatch(fetchUserById(userId))
const originalPromiseResult = unwrapResult(resultAction)
// handle result here
} catch (rejectedValueOrSerializedError) {
// handle error here
}
}

调度后检查错误

请注意,这意味着 **thunk 中的失败请求或错误** **永远不会返回** **一个** **rejected** **promise**。我们假设任何失败都更多地是已处理的错误,而不是此时未处理的异常。这是因为我们希望防止那些不使用 dispatch 结果的人出现未捕获的 promise 拒绝。

如果您的组件需要知道请求是否失败,请使用 .unwrapunwrapResult 并相应地处理重新抛出的错误。

处理 Thunk 错误

当您的 payloadCreator 返回一个被拒绝的 promise(例如 async 函数中抛出的错误)时,thunk 将调度一个 rejected 操作,其中包含错误的自动序列化版本作为 action.error。但是,为了确保可序列化性,所有不匹配 SerializedError 接口的内容都将从其中删除。

export interface SerializedError {
name?: string
message?: string
stack?: string
code?: string
}

如果您需要自定义 rejected 操作的内容,您应该自己捕获任何错误,然后使用 thunkAPI.rejectWithValue 实用程序 **返回** 一个新值。执行 return rejectWithValue(errorPayload) 将导致 rejected 操作使用该值作为 action.payload

如果您的 API 响应“成功”,但包含某种附加的错误详细信息,reducer 应该知道,则也应该使用 rejectWithValue 方法。当预期从 API 获取字段级验证错误时,这尤其常见。

const updateUser = createAsyncThunk(
'users/update',
async (userData, { rejectWithValue }) => {
const { id, ...fields } = userData
try {
const response = await userAPI.updateById(id, fields)
return response.data.user
} catch (err) {
// Use `err.response.data` as `action.payload` for a `rejected` action,
// by explicitly returning it using the `rejectWithValue()` utility
return rejectWithValue(err.response.data)
}
},
)

取消

执行前取消

如果您需要在调用有效负载创建者之前取消 thunk,您可以在有效负载创建者之后提供一个 condition 回调作为选项。回调将接收 thunk 参数和一个包含 {getState, extra} 的对象作为参数,并使用它们来决定是否继续。如果应取消执行,condition 回调应返回一个文字 false 值或一个应解析为 false 的 promise。如果返回了一个 promise,thunk 将等待它完成,然后再调度 pending 操作,否则它将继续同步调度。

const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId: number, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
},
{
condition: (userId, { getState, extra }) => {
const { users } = getState()
const fetchStatus = users.requests[userId]
if (fetchStatus === 'fulfilled' || fetchStatus === 'loading') {
// Already fetched or in progress, don't need to re-fetch
return false
}
},
},
)

如果 condition() 返回 false,默认行为是根本不会调度任何操作。如果您仍然希望在 thunk 被取消时调度一个“拒绝”操作,请传入 {condition, dispatchConditionRejection: true}

运行时取消

如果您想在运行的 thunk 完成之前取消它,可以使用 dispatch(fetchUserById(userId)) 返回的 promise 的 abort 方法。

一个现实生活中的例子看起来像这样

// file: store.ts noEmit
import { configureStore } from '@reduxjs/toolkit'
import type { Reducer } from '@reduxjs/toolkit'
import { useDispatch } from 'react-redux'

declare const reducer: Reducer<{}>
const store = configureStore({ reducer })
export const useAppDispatch = () => useDispatch<typeof store.dispatch>()

// file: slice.ts noEmit
import { createAsyncThunk } from '@reduxjs/toolkit'
export const fetchUserById = createAsyncThunk(
'fetchUserById',
(userId: string) => {
/* ... */
},
)

// file: MyComponent.ts
import { fetchUserById } from './slice'
import { useAppDispatch } from './store'
import React from 'react'

function MyComponent(props: { userId: string }) {
const dispatch = useAppDispatch()
React.useEffect(() => {
// Dispatching the thunk returns a promise
const promise = dispatch(fetchUserById(props.userId))
return () => {
// `createAsyncThunk` attaches an `abort()` method to the promise
promise.abort()
}
}, [props.userId])
}

以这种方式取消 thunk 后,它将调度(并返回)一个 "thunkName/rejected" 操作,并在 error 属性上有一个 AbortError。thunk 不会再调度任何操作。

此外,您的 payloadCreator 可以使用通过 thunkAPI.signal 传递给它的 AbortSignal 来实际取消一个代价高昂的异步操作。

现代浏览器的 fetch api 已经支持 AbortSignal

import { createAsyncThunk } from '@reduxjs/toolkit'

const fetchUserById = createAsyncThunk(
'users/fetchById',
async (userId: string, thunkAPI) => {
const response = await fetch(`https://reqres.in/api/users/${userId}`, {
signal: thunkAPI.signal,
})
return await response.json()
},
)

检查取消状态

读取信号值

您可以使用 signal.aborted 属性定期检查 thunk 是否已被中止,如果是,则停止代价高昂的长时间运行的工作

import { createAsyncThunk } from '@reduxjs/toolkit'

const readStream = createAsyncThunk(
'readStream',
async (stream: ReadableStream, { signal }) => {
const reader = stream.getReader()

let done = false
let result = ''

while (!done) {
if (signal.aborted) {
throw new Error('stop the work, this has been aborted!')
}
const read = await reader.read()
result += read.value
done = read.done
}
return result
},
)

监听中止事件

您还可以调用 signal.addEventListener('abort', callback),以便 thunk 内部的逻辑在调用 promise.abort() 时收到通知。例如,这可以与 axios CancelToken 结合使用

import { createAsyncThunk } from '@reduxjs/toolkit'
import axios from 'axios'

const fetchUserById = createAsyncThunk(
'users/fetchById',
async (userId: string, { signal }) => {
const source = axios.CancelToken.source()
signal.addEventListener('abort', () => {
source.cancel()
})
const response = await axios.get(`https://reqres.in/api/users/${userId}`, {
cancelToken: source.token,
})
return response.data
},
)

检查 promise 拒绝是来自错误还是取消

为了调查 thunk 取消周围的行为,您可以检查分派操作的 meta 对象上的各种属性。如果 thunk 被取消,promise 的结果将是一个 rejected 操作(无论该操作是否实际分派到 store)。

  • 如果它在执行之前被取消,meta.condition 将为 true。
  • 如果它在运行时被中止,meta.aborted 将为 true。
  • 如果这两个都不为真,则 thunk 未被取消,它只是被拒绝,要么是 Promise 拒绝,要么是 rejectWithValue
  • 如果 thunk 未被拒绝,meta.abortedmeta.condition 都将为 undefined

因此,如果您想测试 thunk 是否在执行之前被取消,您可以执行以下操作

import { createAsyncThunk } from '@reduxjs/toolkit'

test('this thunk should always be skipped', async () => {
const thunk = createAsyncThunk(
'users/fetchById',
async () => throw new Error('This promise should never be entered'),
{
condition: () => false,
}
)
const result = await thunk()(dispatch, getState, null)

expect(result.meta.condition).toBe(true)
expect(result.meta.aborted).toBe(false)
})

示例

  • 通过 ID 请求用户,使用加载状态,并且一次只请求一个
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI, User } from './userAPI'

const fetchUserById = createAsyncThunk<
User,
string,
{
state: { users: { loading: string; currentRequestId: string } }
}
>('users/fetchByIdStatus', async (userId: string, { getState, requestId }) => {
const { currentRequestId, loading } = getState().users
if (loading !== 'pending' || requestId !== currentRequestId) {
return
}
const response = await userAPI.fetchById(userId)
return response.data
})

const usersSlice = createSlice({
name: 'users',
initialState: {
entities: [],
loading: 'idle',
currentRequestId: undefined,
error: null,
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUserById.pending, (state, action) => {
if (state.loading === 'idle') {
state.loading = 'pending'
state.currentRequestId = action.meta.requestId
}
})
.addCase(fetchUserById.fulfilled, (state, action) => {
const { requestId } = action.meta
if (
state.loading === 'pending' &&
state.currentRequestId === requestId
) {
state.loading = 'idle'
state.entities.push(action.payload)
state.currentRequestId = undefined
}
})
.addCase(fetchUserById.rejected, (state, action) => {
const { requestId } = action.meta
if (
state.loading === 'pending' &&
state.currentRequestId === requestId
) {
state.loading = 'idle'
state.error = action.error
state.currentRequestId = undefined
}
})
},
})

const UsersComponent = () => {
const { entities, loading, error } = useSelector((state) => state.users)
const dispatch = useDispatch()

const fetchOneUser = async (userId) => {
try {
const user = await dispatch(fetchUserById(userId)).unwrap()
showToast('success', `Fetched ${user.name}`)
} catch (err) {
showToast('error', `Fetch failed: ${err.message}`)
}
}

// render UI here
}
  • 使用 rejectWithValue 在组件中访问自定义拒绝的有效负载

    注意:这是一个人为的示例,假设我们的 userAPI 只会抛出与验证相关的错误

// file: store.ts noEmit
import { configureStore } from '@reduxjs/toolkit'
import type { Reducer } from '@reduxjs/toolkit'
import { useDispatch } from 'react-redux'
import usersReducer from './user/slice'

const store = configureStore({ reducer: { users: usersReducer } })
export const useAppDispatch = () => useDispatch<typeof store.dispatch>()
export type RootState = ReturnType<typeof store.getState>

// file: user/userAPI.ts noEmit

export declare const userAPI: {
updateById<Response>(id: string, fields: {}): { data: Response }
}

// file: user/slice.ts
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'
import type { AxiosError } from 'axios'

// Sample types that will be used
export interface User {
id: string
first_name: string
last_name: string
email: string
}

interface ValidationErrors {
errorMessage: string
field_errors: Record<string, string>
}

interface UpdateUserResponse {
user: User
success: boolean
}

export const updateUser = createAsyncThunk<
User,
{ id: string } & Partial<User>,
{
rejectValue: ValidationErrors
}
>('users/update', async (userData, { rejectWithValue }) => {
try {
const { id, ...fields } = userData
const response = await userAPI.updateById<UpdateUserResponse>(id, fields)
return response.data.user
} catch (err) {
let error: AxiosError<ValidationErrors> = err // cast the error for access
if (!error.response) {
throw err
}
// We got validation errors, let's return those so we can reference in our component and set form errors
return rejectWithValue(error.response.data)
}
})

interface UsersState {
error: string | null | undefined
entities: Record<string, User>
}

const initialState = {
entities: {},
error: null,
} satisfies UsersState as UsersState

const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {},
extraReducers: (builder) => {
// The `builder` callback form is used here because it provides correctly typed reducers from the action creators
builder.addCase(updateUser.fulfilled, (state, { payload }) => {
state.entities[payload.id] = payload
})
builder.addCase(updateUser.rejected, (state, action) => {
if (action.payload) {
// Being that we passed in ValidationErrors to rejectType in `createAsyncThunk`, the payload will be available here.
state.error = action.payload.errorMessage
} else {
state.error = action.error.message
}
})
},
})

export default usersSlice.reducer

// file: externalModules.d.ts noEmit

declare module 'some-toast-library' {
export function showToast(type: string, message: string)
}

// file: user/UsersComponent.ts

import React from 'react'
import { useAppDispatch } from '../store'
import type { RootState } from '../store'
import { useSelector } from 'react-redux'
import { updateUser } from './slice'
import type { User } from './slice'
import type { FormikHelpers } from 'formik'
import { showToast } from 'some-toast-library'

interface FormValues extends Omit<User, 'id'> {}

const UsersComponent = (props: { id: string }) => {
const { entities, error } = useSelector((state: RootState) => state.users)
const dispatch = useAppDispatch()

// This is an example of an onSubmit handler using Formik meant to demonstrate accessing the payload of the rejected action
const handleUpdateUser = async (
values: FormValues,
formikHelpers: FormikHelpers<FormValues>,
) => {
const resultAction = await dispatch(updateUser({ id: props.id, ...values }))
if (updateUser.fulfilled.match(resultAction)) {
// user will have a type signature of User as we passed that as the Returned parameter in createAsyncThunk
const user = resultAction.payload
showToast('success', `Updated ${user.first_name} ${user.last_name}`)
} else {
if (resultAction.payload) {
// Being that we passed in ValidationErrors to rejectType in `createAsyncThunk`, those types will be available here.
formikHelpers.setErrors(resultAction.payload.field_errors)
} else {
showToast('error', `Update failed: ${resultAction.error}`)
}
}
}

// render UI here
}