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 值,用於識別此要求序列signal
:AbortController.signal
物件,可用於查看應用程式邏輯的另一部分是否已將此要求標記為需要取消。rejectWithValue(value, [meta])
:rejectWithValue 是實用程式函數,你可以在動作建立器中return
(或throw
)以傳回具有已定義酬載和 meta 的已拒絕回應。它將傳遞你給予它的任何值,並在已拒絕動作的酬載中傳回它。如果你也傳遞meta
,它將與現有的rejectedAction.meta
合併。fulfillWithValue(value, meta)
:fulfillWithValue 是實用程式函數,你可以在動作建立器中return
以fulfill
值,同時具有新增至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 動作建立器函式會將 pending
、fulfilled
和 rejected
案例的純動作建立器附加為巢狀欄位。
使用上述 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
- 如果 Promise 成功解決,則派送
- 傳回包含最終派送動作(
fulfilled
或rejected
動作物件)的已完成 Promise
Promise 生命週期動作
createAsyncThunk
將使用 createAction
產生三個 Redux 動作建立器:pending
、fulfilled
和 rejected
。每個生命週期動作建立器都將附加到回傳的 thunk 動作建立器,以便您的 reducer 邏輯可以參照動作類型並在調度時回應動作。每個動作物件都將包含 action.meta
下的目前唯一 requestId
和 arg
值。
動作建立器將具有這些簽章
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 中處理這些動作,請使用「建構器回呼」符號在 createReducer
或 createSlice
中參照 createReducer
或 createSlice
中的動作建立器。
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
結果的人發生未捕捉到的承諾拒絕。
如果您的元件需要知道要求是否失敗,請使用 .unwrap
或 unwrapResult
並適當地處理重新拋出的錯誤。
處理 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.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 存取自訂拒絕的 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
}