createAsyncThunk

概览

一个函数,其接受一个 Redux action type 字符串和一个应当返回 promise 对象的回调函数。根据传入的 action type 的前缀,它会生成关于 promise 生命周期的 action types,并且返回一个会运行 promise 回调函数、且根据返回的 promise 派发生命周期 actions 的 thunk action creator。

这么做把处理异步请求生命周期的标准推荐做法抽象出来了。

该函数不会生成任何 reducer 函数,因为它并不知道它接受的数据是什么、如何追踪加载状态、或者返回的数据需要被如何处理。你应当编写你自己的 reducer 逻辑来处理这些 actions,不管是用何种适合你应用的加载状态和处理逻辑。

示例用法:

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'
// 首先, 创建 thunk
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
}
)
// 接着, 在你的 reducers 中处理这些 actions:
const usersSlice = createSlice({
name: 'users',
initialState: { entities: [], loading: 'idle' },
reducers: {
// 标准 reducer 逻辑, 带有每个 reducer 自动生成的 action types
},
extraReducers: {
// 在这里添加处理额外 action types 的 reducers, 并且如果有需要的话,也在此处理加载状态
[fetchUserById.fulfilled]: (state, action) => {
// 把用户添加到 state 数组中
state.entities.push(action.payload)
}
}
})
// 稍后, 在应用中需要用到的地方派发这个 thunk
dispatch(fetchUserById(123))

参数

createAsyncThunk 接收三个参数:一个字符串 action type 值、一个 payloadCreator 回调,和一个 options 对象。

type

一个用于生成额外 Redux action type 常数、代表着一个异步请求生命周期的字符串。

例如,'users/requestStatus'type 参数会生成如下的 action types:

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

payloadCreator

一个回调函数,起应当返回一个包含异步逻辑处理结果的 promise 对象。它也有可能同步地返回一个值。如果产生了错误,它要么应当返回一个被 rejected 的带有 Error 实例的 promise,要么返回一个比如像描述错误的信息的普通值,亦或者返回一个 resolved 的 promise,其带有一个 RejectWithValue 参数,如 thunkAPI.rejectWithValue 的返回值一样。

payloadCreator 可以包含任何需要计算一个合适结果的逻辑。这包括一个标准的 AJAX 数据请求、多个会把结果结合到一起的 AJAX 调用、与 React Native 的 AsyncStorage 进行互动等等。

payloadCreator 有两个参数:

  • arg: 一个单值,当派发 thunk action creator 时,该值包含传入到 payloadCreator 的第一个参数。对于传入像 ID 这样会被用作请求的一部分来的操作来说,是很有用的。如果你需要传入多个值,当你派发 thunk 的时候,把它们作为一个对象整体传入,例如 dispatch(fetchUsers({status: 'active', sortBy: 'name'}))

  • thunkAPI: 一个包含所有通常会被传入到 a Redux thunk 函数的参数的对象,其还有其他的选项:

    • dispatch: Redux store 的 dispatch 方法
    • getState: Redux store 的 getState 方法
    • extra: 被传入到设置阶段 thunk 中间件的 ”额外参数“,如果有的话
    • requestId: 一个被自动生成去识别请求顺序的唯一字符串 ID 值
    • signal: 一个 AbortController.signal 对象 ,其有可能被用于检查是否某部分应用逻辑已经把该请求标记为需要取消
    • rejectWithValue: rejectWithValue 是一个可以在你的 action creator return 以返回一个带有定义好 payload 的 rejected 响应的工具函数。它会把任何你给它的值进行传递,并且在 rejected 的 action payload 返回该值。

任何上述表示的值,都可以在有需要的时候被用到 payloadCreator 的逻辑中,进行结果的计算。

选项

一个带有如下可选字段的逻辑:

  • condition: 一个可以用于跳过 payload creator 执行和所有 action 派发的回调。查看Canceling Before Execution 获取完整的描述。
  • dispatchConditionRejection: 如果 condition() 返回 false,默认的行为是没有任何的 actions 会被派发。如果当 thunk 被取消时,你仍然想派发一个 "rejected" action 的话,把这个值设置成 true

返回值

createAsyncThunk 返回一个标准的 Redux thunk action creator。这个 thunk action creator 函数包含有处理 pending, fulfilled 以及 rejected 情况的普通 action creators,作为其嵌套字段。

把上面例子中的 fetchUserById 当作例子,createAsyncThunk 会生成四个函数:

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

当被派发时,thunk 会:

  • 派发 pending action
  • 调用 payloadCreator 回调函数和等待返回的 promise 结果
  • 当 promise 有结果时:
    • 如果 promise 成功了, 则带着该 promise 值作为 action.payload 派发 fulfilled action
    • 如果 promise 是带着 rejectWithValue(value) 的返回值被 resolved,则带着该传入到 action.payload 的值派发 rejected action,并且 'Rejected' 作为 action.error.message
    • 如果 promise 失败了并且没有被 rejectWithValue 处理,则带着一个作为 action.error 的序列化版本的错误值,派发 rejected action
  • 返回一个 fulfilled 的 promise 对象,包含最终被派发的 action ( fulfilled 或者rejected action 对象)

Promise 生命周期 Actions

createAsyncThunk 会使用 createAction 生成三个 Redux action creators:pending, fulfilledrejected。每一个生命周期 action creator 会被附加到返回的 thunk action creator 上,因此你的 reducer 逻辑可以引用这些 action types 且当 action 被派发时能对其作出响应。每一个 action 对象在 action.meta 字段中,都会含有当前唯一的 requestId 以及 args 值。

action creators 有如下这些签名:

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 方法是,在 createReducer 或者 createSlice 中,使用对象键表示法或者 "builder 回调" 表示法引用这些 action creators。(注意,如果你使用 TypeScript,你应该使用 "builder 回调" 表示法类型能被正确推断

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

处理 Thunk 结果

暴露 Result Actions

当被派发时,thunks 有可能返回一个值。常见的使用案例是,从 thunk 返回一个 promise 对象,从组件中派发这个 thunk,并且在做额外的工作之前等待这个 promise 被 resolve:

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

createAsyncThunk 生成的 thunks 永远返回一个被 resolved的 promise 对象,其值为一个 fulfilled 的 action 对象或 rejected 的 action 对象。

调用的逻辑期望是把这些 actions 当成是原始的 promise 内容。Redux工具包 导出一个用于从 action 中抽取 payloaderrorunwrapResult 函数,并且在适当的情况下返回或者抛出结果:

import { unwrapResult } from '@reduxjs/toolkit'
// in the component
const onClick = () => {
dispatch(fetchUserById(userId))
.then(unwrapResult)
.then(originalPromiseResult => {})
.catch(serializedError => {})
}

派发后检查错误

注意,这意味着 一个失败的请求或者 thunk 里面的错误 永远不会 返回 rejected 的 promise。在此,我们假定任何的失败都是一个被处理过的错误,而非未被处理的异常。这是因为我们想为那些不使用 dispatch 结果的使用者,去避免未被捕获的 promise rejections。

如果你的组件需要知道请求是否失败,请按照响应的情况使用 unwrapResult 处理被重新抛出的错误。

处理 Thunk 错误

payloadCreator 返回一个 rejected 的 promise 时 (例如一个在 async 函数中被抛出的错误),thunk 会派发一个 rejected action,包含有一个自动被序列化的错误,作为 action.error。然而,为了保证序列化,所有不满足 SerializedError 接口要求的事物都会被移除掉:

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

如果你需要自定义 rejected action 的内容,你应该自己捕获这些错误,之后使用 thunkAPI.rejectWithValue 工具函数 返回 一个新的值。使用 return rejectWithValue(errorPayload) 会让 rejected action 使用该值作为 action.payload

如果你的 API 响应 "成功",rejectWithValue 也应当被使用,但是应该含有某些额外能让 reducer 知道的错误细节。这种做法在这预期 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) {
// 通过 `rejectWithValue()` 工具函数,显式返回 `err.response.data`,
// 把其作为 `rejected` action 的 `action.payload`,
return rejectWithValue(err.response.data)
}
}
)

取消操作

执行前取消

如果你需要在 payload creator 被调用前取消 thunk,你可以在 payload creator 后面提供一个 condition 回调函数作为一个选项。这个回调会接收 thunk 参数和一个 {getState, extra} 对象,以供决定是否继续执行使用。如果执行需要被取消,condition 回调会返回一个字面量 false 值:

const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId, 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,默认行为是没有 actions 会被派发。如果当 thunk 被取消时,你仍然需派发一个 "rejected" action,传入 {condition, dispatchConditionRejection: true}

运行期间取消

如果你需要在 thunk 执行完成之前取消,你可以使用 dispatch(fetchUserById(userId)) 返回的 promise 里面的 abort 方法。

一个现实案例长这样:

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 以这种方式被取消之后,它会派发(以及返回)一个 "thunkName/rejected" action,其中 error 属性会有一个 AbortError 的类型。这个 thunk 不会再继续派发任何后续的 actions。

另外,你的 payloadCreator 可以使用 AbortSignal,它通过 thunkAPI.signal 被传递出来以取消一个昂贵的异步 action。

现代浏览器的 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 值

你可以使用 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) 去编写当 promise.abort() 被调用时 thunk 里面的逻辑能接收到通知。这种做法,比如说,能和 axios 的 CancelToken 结合使用:

import { createAsyncThunk } from '@reduxjs/toolkit'
import axios from 'axios'
const fetchUserById = createAsyncThunk(
'users/fetchById',
async (userId, { 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
}
)

示例

  • 利用加载状态,通过用户ID获取用户信息,以及一次只发一个请求:
import { createAsyncThunk, createSlice, unwrapResult } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId, { 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: {
[fetchUserById.pending]: (state, action) => {
if (state.loading === 'idle') {
state.loading = 'pending'
state.currentRequestId = action.meta.requestId
}
},
[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
}
},
[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 { users, loading, error } = useSelector(state => state.users)
const dispatch = useDispatch()
const fetchOneUser = async userId => {
try {
const resultAction = await dispatch(fetchUserById(userId))
const user = unwrapResult(resultAction)
showToast('success', `Fetched ${user.name}`)
} catch (err) {
showToast('error', `Fetch failed: ${err.message}`)
}
}
// 在这里渲染 UI
}
  • 在组件中使用 rejectWithValue 获取自定义 rejected 的 payload

    注意: 这是假定我们的 userAPI 只抛出合法性方面错误的情况下的一个人为的例子

// file: user/slice.ts
import { createAsyncThunk, createSlice, unwrapResult } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'
import { 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 // cast the error for access
if (!error.response) {
throw err
}
// 我们遇到了合法性的错误,让我们把这些错误返回以便我们能在组件中引用它们并且设置表单错误
return rejectWithValue(error.response.data)
}
})
interface UsersState {
error: string | null | undefined
entities: Record<string, User>
}
const initialState: UsersState = {
entities: {},
error: null,
}
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) {
// 由于我们把 ValidationErrors 传给了 `createAsyncThunk` 的 rejectType,
// payload 能从这里获得
state.error = action.payload.errorMessage
} else {
state.error = action.error.message
}
})
},
})
export default usersSlice.reducer
// file: user/UsersComponent.ts
import React from 'react'
import { useAppDispatch, RootState } from '../store'
import { useSelector } from 'react-redux'
import { User, updateUser } from './slice'
import { 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 处理函数的例子,其目的是展示如何获取 rejected action 里面的 payload
const handleUpdateUser = async (
values: FormValues,
formikHelpers: FormikHelpers<FormValues>
) => {
const resultAction = await dispatch(updateUser({ id: props.id, ...values }))
if (updateUser.fulfilled.match(resultAction)) {
// 当我们传递user时,它会有一个 User 的类型签名,作为 createAsyncThunk 的返回值参数
const user = resultAction.payload
showToast('success', `Updated ${user.first_name} ${user.last_name}`)
} else {
if (resultAction.payload) {
// 由于我们把 ValidationErrors 传给了 `createAsyncThunk` 的 rejectType,
// payload 能从这里获得
formikHelpers.setErrors(resultAction.payload.field_errors)
} else {
showToast('error', `Update failed: ${resultAction.error}`)
}
}
}
// 在这里渲染 UI
}