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 函数将具有用于 pending
、fulfilled
和 rejected
情况的普通 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
。
- 如果 promise 成功解决,则派发
- 返回一个包含最终派发 action(
fulfilled
或rejected
action 对象)的已解决 promise。
Promise 生命周期 Actions
createAsyncThunk
将使用 createAction
生成三个 Redux action creator:pending
、fulfilled
和 rejected
。每个生命周期 action creator 将附加到返回的 thunk action creator,以便你的 reducer 逻辑可以引用 action 类型并在派发时响应 action。每个 action 对象都将在 action.meta
下包含当前唯一的 requestId
和 arg
值。
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,请使用“构建器回调”符号在 createReducer
或 createSlice
中引用 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 拒绝。
如果您的组件需要知道请求是否失败,请使用 .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) {
// 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.aborted
和meta.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
}