跳到主要内容

 

createAsyncThunk

概述

一个接受 Redux 动作类型字符串和应返回 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'

// 首先,创建 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

// 然后,在你的 reducers 中处理动作:
const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {
// 标准的 reducer 逻辑,每个 reducer 都有自动生成的动作类型
},
extraReducers: (builder) => {
// 在这里添加额外的动作类型的 reducers,并根据需要处理加载状态
builder.addCase(fetchUserById.fulfilled, (state, action) => {
// 将用户添加到 state 数组
state.entities.push(action.payload)
})
},
})

// 稍后,在应用中根据需要分派 thunk
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 实例的 rejected promise 或一个如描述性错误消息的普通值,或者返回一个包含 RejectWithValue 参数的 resolved 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 store 的 dispatch 方法
    • getState:Redux store 的 getState 方法
    • extra:在设置 thunk 中间件时给出的 "extra argument",如果有的话
    • requestId:一个自动生成的唯一字符串 ID 值,用于标识此请求序列
    • signal:一个 AbortController.signal 对象,可以用来查看应用逻辑的其他部分是否已将此请求标记为需要取消。
    • rejectWithValue(value, [meta]):rejectWithValue 是一个你可以在你的动作创建器中 return(或 throw)的实用函数,以返回一个带有定义的 payload 和 meta 的 rejected 响应。它将传递你给它的任何值,并在 rejected 动作的 payload 中返回它。如果你还传入了一个 meta,它将与现有的 rejectedAction.meta 合并。
    • fulfillWithValue(value, meta):fulfillWithValue 是一个你可以在你的动作创建器中 return 的实用函数,以 fulfill 一个值,同时有添加到 fulfilledAction.meta 的能力。

payloadCreator 函数中的逻辑可以根据需要使用这些值来计算结果。

选项

一个带有以下可选字段的对象:

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

返回值

createAsyncThunk 返回一个标准的 Redux thunk action 创建器。这个 thunk action 创建器函数将会有 pendingfulfilledrejected 的普通 action 创建器作为嵌套字段。

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

  • fetchUserById,启动你编写的异步 payload 回调的 thunk action 创建器
    • fetchUserById.pending,一个派发 'users/fetchByIdStatus/pending' action 的 action 创建器
    • fetchUserById.fulfilled,一个派发 'users/fetchByIdStatus/fulfilled' action 的 action 创建器
    • fetchUserById.rejected,一个派发 'users/fetchByIdStatus/rejected' action 的 action 创建器

当派发时,thunk 将:

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

Promise 生命周期 Actions

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

action 创建器将有这些签名:

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>

要在你的 reducers 中处理这些 actions,使用 "builder callback" 符号在 createReducercreateSlice 中引用 action 创建器。

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 匹配器,用于匹配已完成和被拒绝的 actions。从概念上讲,这类似于 finally 块。

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

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 结果

解包结果 Actions

Thunks 可能在派发时返回一个值。一个常见的用例是从 thunk 返回一个 promise,从组件中派发 thunk,然后等待 promise 解决后再进行额外的工作:

const onClick = () => {
dispatch(fetchUserById(userId)).then(() => {
// 执行额外的工作
})
}

createAsyncThunk 生成的 thunks 将始终返回一个已解决的 promise,其中包含适当的 fulfilled action 对象或 rejected action 对象。

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

// in the component

const onClick = () => {
dispatch(fetchUserById(userId))
.unwrap()
.then((originalPromiseResult) => {
// 在这里处理结果
})
.catch((rejectedValueOrSerializedError) => {
// 在这里处理错误
})
}

或者使用 async/await 语法:

// in the component

const onClick = async () => {
try {
const originalPromiseResult = await dispatch(fetchUserById(userId)).unwrap()
// 在这里处理结果
} catch (rejectedValueOrSerializedError) {
// 在这里处理错误
}
}

在大多数情况下,优先使用附加的 .unwrap() 属性,然而 Redux Toolkit 也导出了一个 unwrapResult 函数,可以用于类似的目的:

import { unwrapResult } from '@reduxjs/toolkit'

// 在组件中
const onClick = () => {
dispatch(fetchUserById(userId))
.then(unwrapResult)
.then((originalPromiseResult) => {
// 在这里处理结果
})
.catch((rejectedValueOrSerializedError) => {
// 在这里处理错误
})
}

或者使用 async/await 语法:

import { unwrapResult } from '@reduxjs/toolkit'

// 在组件中
const onClick = async () => {
try {
const resultAction = await dispatch(fetchUserById(userId))
const originalPromiseResult = unwrapResult(resultAction)
// 在这里处理结果
} catch (rejectedValueOrSerializedError) {
// 在这里处理错误
}
}

派发后检查错误

注意,这意味着失败的请求或 thunk 中的错误将 永远不会 返回一个 被拒绝的 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) {
// 使用 `err.response.data` 作为 `rejected` 动作的 `action.payload`,
// 通过显式地使用 `rejectWithValue()` 实用程序返回它
return rejectWithValue(err.response.data)
}
},
)

取消

执行前取消

如果你需要在调用 payload 创建器之前取消 thunk,你可以在 payload 创建器后的选项中提供一个 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') {
// 已经获取或正在进行中,不需要重新获取
return false
}
},
},
)

如果 condition() 返回 false,默认行为是根本不会派发任何动作。如果你仍然希望在 thunk 被取消时派发一个 "rejected" 动作,传入 {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(() => {
// 派发 thunk 返回一个 promise
const promise = dispatch(fetchUserById(props.userId))
return () => {
// `createAsyncThunk` 将一个 `abort()` 方法附加到 promise 上
promise.abort()
}
}, [props.userId])
}

在这种方式下取消 thunk 后,它将派发(并返回)一个带有 AbortErrorerror 属性上的 "thunkName/rejected" 动作。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('停止工作,这已经被取消了!')
}
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('这个 thunk 应该总是被跳过', async () => {
const thunk = createAsyncThunk(
'users/fetchById',
async () => throw new Error('这个 promise 永远不应该被进入'),
{
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', `获取 ${user.name}`)
} catch (err) {
showToast('error', `获取失败: ${err.message}`)
}
}

// 在这里渲染 UI
}
  • 使用 rejectWithValue 在组件中访问自定义拒绝的有效载荷

    注意:这是一个假设我们的 userAPI 只会抛出特定于验证的错误的人为示例

// 文件: 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>

// 文件: user/userAPI.ts noEmit

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

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

// 将要使用的样本类型
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 // 转换错误以便访问
if (!error.response) {
throw err
}
// 我们得到了验证错误,让我们返回这些错误,这样我们就可以在组件中引用并设置表单错误
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) => {
// 这里使用 `builder` 回调形式,因为它提供了从 action creators 中正确类型化的 reducers
builder.addCase(updateUser.fulfilled, (state, { payload }) => {
state.entities[payload.id] = payload
})
builder.addCase(updateUser.rejected, (state, action) => {
if (action.payload) {
// 由于我们在 `createAsyncThunk` 中传入了 ValidationErrors 到 rejectType,所以有效载荷将在这里可用。
state.error = action.payload.errorMessage
} else {
state.error = action.error.message
}
})
},
})

export default usersSlice.reducer

// 文件: externalModules.d.ts noEmit

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

// 文件: 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()

// 这是一个使用 Formik 的 onSubmit 处理器的示例,旨在演示如何访问被拒绝的 action 的有效载荷
const handleUpdateUser = async (
values: FormValues,
formikHelpers: FormikHelpers<FormValues>,
) => {
const resultAction = await dispatch(updateUser({ id: props.id, ...values }))
if (updateUser.fulfilled.match(resultAction)) {
// user 将具有 User 的类型签名,因为我们将其作为 Returned 参数传递给了 createAsyncThunk
const user = resultAction.payload
showToast('success', `更新了 ${user.first_name} ${user.last_name}`)
} else {
if (resultAction.payload) {
// 由于我们在 `createAsyncThunk` 中传入了 ValidationErrors 到 rejectType,所以这些类型将在这里可用。
formikHelpers.setErrors(resultAction.payload.field_errors)
} else {
showToast('error', `更新失败: ${resultAction.error}`)
}
}
}

// 在这里渲染 UI
}