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 创建器函数将会有 pending
、fulfilled
和 rejected
的普通 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
- 如果 promise 成功解决,使用 promise 的值作为
- 返回一个包含最终派发的 action(
fulfilled
或rejected
action 对象)的已解决 promise
Promise 生命周期 Actions
createAsyncThunk
将使用 createAction
生成三个 Redux action 创建器:pending
、fulfilled
和 rejected
。每个生命周期 action 创建器将被附加到返回的 thunk action 创建器上,以便你的 reducer 逻辑可以引用 action 类型并在派发时响应 actions。每个 action 对象将包含当前唯一的 requestId
和 arg
值在 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" 符号在 createReducer
或 createSlice
中引用 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 拒绝。
如果你的组件需要知道请求是否失败,使用 .unwrap
或 unwrapResult
并相应地处理重新抛出的错误。
处理 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 后,它将派发(并返回)一个带有 AbortError
在 error
属性上的 "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.aborted
和meta.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
}