跳至主要內容

createAsyncThunk

概觀

一個函式,接受 Redux action 類型字串和一個應該回傳 Promise 的回呼函式。它會根據您傳入的 action 類型字首產生 Promise 生命週期 action 類型,並回傳一個 thunk action creator,它會執行 Promise 回呼函式,並根據回傳的 Promise 發送生命週期 action。

這抽象化了處理非同步請求生命週期的標準建議方法。

它不會產生任何 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,或傳回純值,例如描述性錯誤訊息,或以 thunkAPI.rejectWithValue 函數傳回的 RejectWithValue 參數為基礎的已解決 Promise。

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 值,用於識別此要求序列
    • signalAbortController.signal 物件,可用於查看應用程式邏輯的另一部分是否已將此要求標記為需要取消。
    • rejectWithValue(value, [meta]):rejectWithValue 是實用程式函數,你可以在動作建立器中 return(或 throw)以傳回具有已定義酬載和 meta 的已拒絕回應。它將傳遞你給予它的任何值,並在已拒絕動作的酬載中傳回它。如果你也傳遞 meta,它將與現有的 rejectedAction.meta 合併。
    • fulfillWithValue(value, meta):fulfillWithValue 是實用程式函數,你可以在動作建立器中 returnfulfill 值,同時具有新增至 fulfilledAction.meta 的能力。

payloadCreator 函數中的邏輯可以使用這些值中的任何一個,視需要計算結果。

選項

具有以下選用欄位的物件

  • condition(arg, { getState, extra } ): boolean | Promise<boolean>:一個可用於跳過酬載建立器執行和所有動作分派(如果需要)的回呼。有關完整說明,請參閱 在執行前取消
  • dispatchConditionRejection:如果 condition() 傳回 false,預設行為是完全不會分派任何動作。如果你仍希望在 thunk 被取消時分派「已拒絕」動作,請將此旗標設為 true
  • idGenerator(arg): string:在產生請求序列的 requestId 時使用之函式。預設使用 nanoid,但你可以實作自己的 ID 產生邏輯。
  • serializeError(error: unknown) => any 以你自己的序列化邏輯取代內部的 miniSerializeError 方法。
  • getPendingMeta({ arg, requestId }, { getState, extra }): any:一個用於建立物件的函式,該物件會合併至 pendingAction.meta 欄位。

傳回值

createAsyncThunk 傳回標準 Redux thunk 動作建立器。thunk 動作建立器函式會將 pendingfulfilledrejected 案例的純動作建立器附加為巢狀欄位。

使用上述 fetchUserById 範例,createAsyncThunk 會產生四個函式

  • fetchUserById,thunk 動作建立器,用於啟動你撰寫的非同步 payload 回呼。
    • fetchUserById.pending,一個動作建立器,用於派送 'users/fetchByIdStatus/pending' 動作。
    • fetchUserById.fulfilled,一個動作建立器,用於派送 'users/fetchByIdStatus/fulfilled' 動作。
    • fetchUserById.rejected,一個動作建立器,用於派送 'users/fetchByIdStatus/rejected' 動作。

派送時,thunk 會

  • 派送 pending 動作
  • 呼叫 payloadCreator 回呼,並等待傳回的 Promise 解決
  • 當 Promise 解決時
    • 如果 Promise 成功解決,則派送 fulfilled 動作,其中 Promise 值為 action.payload
    • 如果 Promise 以 rejectWithValue(value) 傳回值解決,則派送 rejected 動作,其中傳入 action.payload 的值和「Rejected」為 action.error.message
    • 如果 Promise 失敗且未以 rejectWithValue 處理,則派送 rejected 動作,其中錯誤值的序列化版本為 action.error
  • 傳回包含最終派送動作(fulfilledrejected 動作物件)的已完成 Promise

Promise 生命週期動作

createAsyncThunk 將使用 createAction 產生三個 Redux 動作建立器:pendingfulfilledrejected。每個生命週期動作建立器都將附加到回傳的 thunk 動作建立器,以便您的 reducer 邏輯可以參照動作類型並在調度時回應動作。每個動作物件都將包含 action.meta 下的目前唯一 requestIdarg 值。

動作建立器將具有這些簽章

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 中處理這些動作,請使用「建構器回呼」符號在 createReducercreateSlice 中參照 createReducercreateSlice 中的動作建立器。

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 比對器,用於比對已完成和已拒絕的動作。概念上這類似於 finally 區塊。

請務必使用 addMatcher 而不是 addCase,因為 settled 是比對器,而不是動作建立器。

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 回傳一個承諾,從組件調度 thunk,然後在執行其他工作之前等待承諾解析。

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

createAsyncThunk 產生的 thunk 總是會回傳一個已解析的承諾,裡面包含 fulfilled 動作物件或 rejected 動作物件(視情況而定)。

呼叫邏輯可能會希望將這些動作視為原始承諾內容。由調度 thunk 回傳的承諾有一個 unwrap 屬性,可以呼叫它來擷取 fulfilled 動作的 payload 或拋出 rejected 動作中由 rejectWithValue 建立的 error 或(如果有的話)payload

// in the component

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

或使用非同步/等待語法

// 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
})
}

或使用非同步/等待語法

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 中的失敗請求或錯誤永遠不會回傳已拒絕的承諾。我們假設任何失敗在這個時候更像是已處理的錯誤,而不是未處理的例外。這是因為我們希望防止那些不使用 dispatch 結果的人發生未捕捉到的承諾拒絕。

如果您的元件需要知道要求是否失敗,請使用 .unwrapunwrapResult 並適當地處理重新拋出的錯誤。

處理 Thunk 錯誤

當您的 payloadCreator 傳回一個已拒絕的承諾(例如 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 回應「成功」,但包含還原器應該知道的某些額外錯誤詳細資料,也應該使用 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)
}
},
)

取消

在執行前取消

如果您需要在呼叫 payload 建構函式之前取消 thunk,您可以在 payload 建構函式之後提供一個 condition 回呼作為選項。回呼會收到 thunk 參數和一個包含 {getState, extra} 作為參數的物件,並使用它們來決定是否繼續。如果應該取消執行,condition 回呼應該傳回一個字面 false 值或一個應該解析為 false 的承諾。如果傳回一個承諾,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 被取消時發送一個「rejected」動作,請傳入 {condition, dispatchConditionRejection: true}

在執行中取消

如果您想在執行完成前取消正在執行的 thunk,您可以使用 dispatch(fetchUserById(userId)) 傳回的承諾的 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 動作(無論該動作是否實際已發送到儲存)。

  • 如果在執行前取消,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 存取自訂拒絕的 payload

    注意:這是一個假設性的範例,假設我們的 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
}