Skip to main content

用 Thunk 编写逻辑

你将学到什么
  • "thunks" 是什么,以及为什么使用它们来编写 Redux 逻辑

    ¥What "thunks" are, and why they're used for writing Redux logic

  • thunk 中间件如何工作

    ¥How the thunk middleware works

  • 在 thunk 中编写同步和异步逻辑的技术

    ¥Techniques for writing sync and async logic in thunks

  • 常见的 thunk 使用模式

    ¥Common thunk usage patterns

块概述

¥Thunk Overview

什么是 "thunk"?

¥What is a "thunk"?

"thunk" 一词是一个编程术语,意思是 "一段执行一些延迟工作的代码"。我们可以编写一个可用于稍后执行工作的函数体或代码,而不是现在执行某些逻辑。

¥The word "thunk" is a programming term that means "a piece of code that does some delayed work". Rather than execute some logic now, we can write a function body or code that can be used to perform the work later.

具体来说,对于 Redux,"thunks" 是一种编写具有内部逻辑的函数的模式,可以与 Redux 存储的 dispatchgetState 方法进行交互。

¥For Redux specifically, "thunks" are a pattern of writing functions with logic inside that can interact with a Redux store's dispatch and getState methods.

使用 thunk 需要将 redux-thunk 中间件 添加到 Redux 存储作为其配置的一部分。

¥Using thunks requires the redux-thunk middleware to be added to the Redux store as part of its configuration.

thunk 是 在 Redux 应用中编写异步逻辑的标准方法,通常用于数据获取。但是,它们可用于各种任务,并且可以包含同步和异步逻辑。

¥Thunks are a standard approach for writing async logic in Redux apps, and are commonly used for data fetching. However, they can be used for a variety of tasks, and can contain both synchronous and asynchronous logic.

写感谢信

¥Writing Thunks

thunk 函数是一个接受两个参数的函数:Redux 存储 dispatch 方法和 Redux 存储 getState 方法。Thunk 函数不直接由应用代码调用。相反,它们被传递到 store.dispatch()

¥A thunk function is a function that accepts two arguments: the Redux store dispatch method, and the Redux store getState method. Thunk functions are not directly called by application code. Instead, they are passed to store.dispatch():

Dispatching thunk functions
const thunkFunction = (dispatch, getState) => {
// logic here that can dispatch actions or read state
}

store.dispatch(thunkFunction)

thunk 函数可以包含任意逻辑,同步或异步,并且可以随时调用 dispatchgetState

¥A thunk function may contain any arbitrary logic, sync or async, and can call dispatch or getState at any time.

就像 Redux 代码通常使用 动作创建者生成用于调度的动作对象 而不是手动编写操作对象一样,我们通常使用 thunk 操作创建器来生成分派的 thunk 函数。thunk 动作创建者是一个可能有一些参数的函数,并返回一个新的 thunk 函数。thunk 通常会关闭传递给操作创建者的任何参数,因此它们可以在逻辑中使用:

¥In the same way that Redux code normally uses action creators to generate action objects for dispatching instead of writing action objects by hand, we normally use thunk action creators to generate the thunk functions that are dispatched. A thunk action creator is a function that may have some arguments, and returns a new thunk function. The thunk typically closes over any arguments passed to the action creator, so they can be used in the logic:

Thunk action creators and thunk functions
// fetchTodoById is the "thunk action creator"
export function fetchTodoById(todoId) {
// fetchTodoByIdThunk is the "thunk function"
return async function fetchTodoByIdThunk(dispatch, getState) {
const response = await client.get(`/fakeApi/todo/${todoId}`)
dispatch(todosLoaded(response.todos))
}
}

Thunk 函数和动作创建器可以使用 function 关键字或箭头函数编写 - 这里没有有意义的区别。同样的 fetchTodoById thunk 也可以使用箭头函数编写,如下所示:

¥Thunk functions and action creators can be written using either the function keyword or arrow functions - there's no meaningful difference here. The same fetchTodoById thunk could also be written using arrow functions, like this:

Writing thunks using arrow functions
export const fetchTodoById = todoId => async dispatch => {
const response = await client.get(`/fakeApi/todo/${todoId}`)
dispatch(todosLoaded(response.todos))
}

无论哪种情况,thunk 都是通过调用操作创建者来分派的,就像你分派任何其他 Redux 操作一样:

¥In either case, the thunk is dispatched by calling the action creator, in the same way as you'd dispatch any other Redux action:

function TodoComponent({ todoId }) {
const dispatch = useDispatch()

const onFetchClicked = () => {
// Calls the thunk action creator, and passes the thunk function to dispatch
dispatch(fetchTodoById(todoId))
}
}

为什么要使用 thunk?

¥Why Use Thunks?

Thunk 允许我们独立于 UI 层编写额外的 Redux 相关逻辑。此逻辑可能包括副作用,例如异步请求或生成随机值,以及需要分派多个操作或访问 Redux 存储状态的逻辑。

¥Thunks allow us to write additional Redux-related logic separate from a UI layer. This logic can include side effects, such as async requests or generating random values, as well as logic that requires dispatching multiple actions or access to the Redux store state.

Redux reducer 不得含有副作用,但实际应用需要具有副作用的逻辑。其中一些可能存在于组件内部,但有些可能需要存在于 UI 层之外。Thunk(和其他 Redux 中间件)为我们提供了一个放置这些副作用的地方。

¥Redux reducers must not contain side effects, but real applications require logic that has side effects. Some of that may live inside components, but some may need to live outside the UI layer. Thunks (and other Redux middleware) give us a place to put those side effects.

直接在组件中包含逻辑是很常见的,例如在点击处理程序或 useEffect 钩子中发出异步请求,然后处理结果。然而,通常需要将尽可能多的逻辑移到 UI 层之外。这样做可能是为了提高逻辑的可测试性,使 UI 层尽可能精简和 "presentational",或者提高代码重用和共享。

¥It's common to have logic directly in components, such as making an async request in a click handler or a useEffect hook and then processing the results. However, it's often necessary to move as much of that logic as possible outside the UI layer. This may be done to improve testability of the logic, to keep the UI layer as thin and "presentational" as possible, or to improve code reuse and sharing.

从某种意义上说,thunk 是一个漏洞,你可以提前编写任何需要与 Redux 存储交互的代码,而无需知道将使用哪个 Redux 存储。这可以防止逻辑绑定到任何特定的 Redux 存储实例并保持其可重用性。

¥In a sense, a thunk is a loophole where you can write any code that needs to interact with the Redux store, ahead of time, without needing to know which Redux store will be used. This keeps the logic from being bound to any specific Redux store instance and keeps it reusable.

Detailed Explanation: Thunks, Connect, and "Container Components"

从历史上看,使用 thunk 的另一个原因是帮助保持 React 组件 "不知道 Redux"。connect API 允许传递动作创建者,而 "binding" 则允许他们在调用时自动分派动作。由于组件通常无法在内部访问 dispatch,因此将 thunk 传递给 connect 使组件可以仅调用 this.props.doSomething(),而无需知道它是否是来自父级的回调、分派普通 Redux 操作、分派执行同步或异步逻辑的 thunk 或测试中的模拟函数。

¥Historically, another reason to use thunks was to help keep React components "unaware of Redux". The connect API allowed passing action creators and "binding" them to automatically dispatch actions when called. Since components typically did not have access to dispatch internally, passing thunks to connect made it possible for components to just call this.props.doSomething(), without needing to know if it was a callback from a parent, dispatching a plain Redux action, dispatching a thunk performing sync or async logic, or a mock function in a test.

随着 React-Redux 钩子 API 的到来,这种情况发生了变化。社区总体上已经放弃了 "容器/展示" 模式,而 组件现在可以通过 useDispatch 钩子直接访问 dispatch 模式。这确实意味着可以在组件内部直接拥有更多逻辑,例如结果的异步获取+分派。然而,thunk 可以访问 getState,而组件则不能,并且将该逻辑移到组件之外仍然有价值。

¥With the arrival of the React-Redux hooks API, that situation has changed. The community has switched away from the "container/presentational" pattern in general, and components now have access to dispatch directly via the useDispatch hook. This does mean that it's possible to have more logic directly inside of a component, such as an async fetch + dispatch of the results. However, thunks have access to getState, which components do not, and there's still value in moving that logic outside of components.

块用例

¥Thunk Use Cases

由于 thunk 是一种通用工具,可以包含任意逻辑,因此它们可以用于多种目的。最常见的用例是:

¥Because thunks are a general-purpose tool that can contain arbitrary logic, they can be used for a wide variety of purposes. The most common use cases are:

  • 将复杂逻辑从组件中移出

    ¥Moving complex logic out of components

  • 发出异步请求或其他异步逻辑

    ¥Making async requests or other async logic

  • 编写需要连续或随时间分派多个操作的逻辑

    ¥Writing logic that needs to dispatch multiple actions in a row or over time

  • 编写需要访问 getState 才能做出决策或在操作中包含其他状态值的逻辑

    ¥Writing logic that needs access to getState to make decisions or include other state values in an action

Thunk 是 "one-shot" 函数,没有生命周期的意义。他们也看不到其他已调度的操作。因此,它们通常不应该用于初始化像 websocket 这样的持久连接,并且你不能使用它们来响应其他操作。

¥Thunks are "one-shot" functions, with no sense of a lifecycle. They also cannot see other dispatched actions. So, they should not generally be used for initializing persistent connections like websockets, and you can't use them to respond to other actions.

Thunk 最适合用于复杂的同步逻辑和简单到中等的异步逻辑,例如发出标准 AJAX 请求并根据请求结果分派操作。

¥Thunks are best used for complex synchronous logic, and simple to moderate async logic such as making a standard AJAX request and dispatching actions based on the request results.

Redux Thunk 中间件

¥Redux Thunk Middleware

调度 thunk 函数需要将 redux-thunk 中间件 添加到 Redux 存储作为其配置的一部分。

¥Dispatching thunk functions requires that the redux-thunk middleware has been added to the Redux store as part of its configuration.

添加中间件

¥Adding the Middleware

Redux Toolkit configureStore API 在存储创建期间自动添加 thunk 中间件,因此通常无需额外配置即可使用。

¥The Redux Toolkit configureStore API automatically adds the thunk middleware during store creation, so it should typically be available with no extra configuration needed.

如果你需要手动将 thunk 中间件添加到存储中,可以由 将 thunk 中间件传递给 applyMiddleware() 作为设置过程的一部分来完成。

¥If you need to add the thunk middleware to a store manually, that can be done by passing the thunk middleware to applyMiddleware() as part of the setup process.

中间件如何工作?

¥How Does the Middleware Work?

首先,让我们回顾一下 Redux 中间件的一般工作原理。

¥To start, let's review how Redux middleware work in general.

Redux 中间件全部编写为一系列 3 个嵌套函数

¥Redux middleware are all written as a series of 3 nested functions:

  • 外部函数接收一个 "存储 API" 对象和 {dispatch, getState}

    ¥The outer function receives a "store API" object with {dispatch, getState}

  • 中间函数接收链中的 next 中间件(或者实际的 store.dispatch 方法)

    ¥The middle function receives the next middleware in the chain (or the actual store.dispatch method)

  • 当每个 action 通过中间件链时,将调用内部函数

    ¥The inner function will be called with each action as it's passed through the middleware chain

需要注意的是,中间件可用于允许将不是操作对象的值传递到 store.dispatch() 中,只要中间件拦截这些值并且不让它们到达 reducer 即可。

¥It's important to note that middleware can be used to allow passing values that are not action objects into store.dispatch(), as long as the middleware intercepts those values and does not let them reach the reducers.

考虑到这一点,我们可以看看 thunk 中间件的细节。

¥With that in mind, we can look at the specifics of the thunk middleware.

thunk 中间件的实际实现很短 - 只有大约 10 行。这是来源,还有附加的评论:

¥The actual implementation of the thunk middleware is very short - only about 10 lines. Here's the source, with additional added comments:

Redux thunk middleware implementation, annotated
// standard middleware definition, with 3 nested functions:
// 1) Accepts `{dispatch, getState}`
// 2) Accepts `next`
// 3) Accepts `action`
const thunkMiddleware =
({ dispatch, getState }) =>
next =>
action => {
// If the "action" is actually a function instead...
if (typeof action === 'function') {
// then call the function and pass `dispatch` and `getState` as arguments
return action(dispatch, getState)
}

// Otherwise, it's a normal action - send it onwards
return next(action)
}

换句话说:

¥In other words:

  • 如果将函数传递给 dispatch,thunk 中间件会发现它是一个函数而不是操作对象,会拦截它,并以 (dispatch, getState) 作为参数调用该函数

    ¥If you pass a function into dispatch, the thunk middleware sees that it's a function instead of an action object, intercepts it, and calls that function with (dispatch, getState) as its arguments

  • 如果它是一个普通的操作对象(或其他任何对象),它将被转发到链中的下一个中间件

    ¥If it's a normal action object (or anything else), it's forwarded to the next middleware in the chain

将配置值注入到 thunk 中

¥Injecting Config Values Into Thunks

thunk 中间件确实有一个自定义选项。你可以在设置时创建 thunk 中间件的自定义实例,并将 "额外的参数" 注入到中间件中。然后,中间件将注入该额外值作为每个 thunk 函数的第三个参数。这最常用于将 API 服务层注入 thunk 函数,这样它们就不会对 API 方法有硬编码的依赖:

¥The thunk middleware does have one customization option. You can create a custom instance of the thunk middleware at setup time, and inject an "extra argument" into the middleware. The middleware will then inject that extra value as the third argument of every thunk function. This is most commonly used for injecting an API service layer into thunk functions, so that they don't have hardcoded dependencies on the API methods:

Thunk setup with an extra argument
import thunkMiddleware from 'redux-thunk'

const serviceApi = createServiceApi('/some/url')

const thunkMiddlewareWithArg = thunkMiddleware.withExtraArgument({ serviceApi })

Redux Toolkit 的 configureStore 支持 这是 getDefaultMiddleware 中中间件定制的一部分

¥Redux Toolkit's configureStore supports this as part of its middleware customization in getDefaultMiddleware:

Thunk extra argument with configureStore
const store = configureStore({
reducer: rootReducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware({
thunk: {
extraArgument: { serviceApi }
}
})
})

只能有一个额外的参数值。如果需要传递多个值,请传递包含这些值的对象。

¥There can only be one extra argument value. If you need to pass in multiple values, pass in an object containing those.

然后 thunk 函数将接收该额外值作为其第三个参数:

¥The thunk function will then receive that extra value as its third argument:

Thunk function with extra argument
export const fetchTodoById =
todoId => async (dispatch, getState, extraArgument) => {
// In this example, the extra arg is an object with an API service inside
const { serviceApi } = extraArgument
const response = await serviceApi.getTodo(todoId)
dispatch(todosLoaded(response.todos))
}

块使用模式

¥Thunk Usage Patterns

调度动作

¥Dispatching Actions

Thunk 可以访问 dispatch 方法。这可以用来调度动作,甚至其他重击。这对于连续分派多个操作(尽管 这是一种应该最小化的模式)或编排需要在流程中的多个点分派的复杂逻辑非常有用。

¥Thunks have access to the dispatch method. This can be used to dispatch actions, or even other thunks. This can be useful for dispatching multiple actions in a row (although this is a pattern that should be minimized), or orchestrating complex logic that needs to dispatch at multiple points in the process.

Example: thunks dispatching actions and thunks
// An example of a thunk dispatching other action creators,
// which may or may not be thunks themselves. No async code, just
// orchestration of higher-level synchronous logic.
function complexSynchronousThunk(someValue) {
return (dispatch, getState) => {
dispatch(someBasicActionCreator(someValue))
dispatch(someThunkActionCreator())
}
}

访问状态

¥Accessing State

与组件不同,thunk 也可以访问 getState。可以随时调用它来检索当前的根 Redux 状态值。这对于基于当前状态运行条件逻辑非常有用。这对于 读取 thunk 内部的状态时使用选择器函数 来说很常见,而不是直接访问嵌套状态字段,但任何一种方法都可以。

¥Unlike components, thunks also have access to getState. This can be called at any time to retrieve the current root Redux state value. This can be useful for running conditional logic based on the current state. It's common to use selector functions when reading state inside of thunks rather than accessing nested state fields directly, but either approach is fine.

Example: Conditional dispatching based on state
const MAX_TODOS = 5

function addTodosIfAllowed(todoText) {
return (dispatch, getState) => {
const state = getState()

// Could also check `state.todos.length < MAX_TODOS`
if (selectCanAddNewTodo(state, MAX_TODOS)) {
dispatch(todoAdded(todoText))
}
}
}

它比 在 reducer 中放置尽可能多的逻辑 更好,但是 thunk 内部也有额外的逻辑也很好。

¥It's preferable to put as much logic as possible in reducers, but it's fine for thunks to also have additional logic inside as well.

由于一旦 reducers 处理一个 action,状态就会同步更新,因此你可以在调度后调用 getState 来获取更新后的状态。

¥Since the state is synchronously updated as soon as the reducers process an action, you can call getState after a dispatch to get the updated state.

Example: checking state after dispatch
function checkStateAfterDispatch() {
return (dispatch, getState) => {
const firstState = getState()
dispatch(firstAction())

const secondState = getState()

if (secondState.someField != firstState.someField) {
dispatch(secondAction())
}
}
}

考虑在 thunk 中访问状态的另一个原因是用附加信息填写操作。有时,切片缩减器确实需要读取不在其自己的状态切片中的值。一个可能的解决方法是调度一个 thunk,从状态中提取所需的值,然后调度一个包含附加信息的普通操作。

¥One other reason to consider accessing state in a thunk is to fill out an action with additional info. Sometimes a slice reducer really needs to read a value that isn't in its own slice of state. A possible workaround to that is to dispatch a thunk, extract the needed values from state, and then dispatch a plain action containing the additional info.

Example: actions containing cross-slice data
// One solution to the "cross-slice state in reducers" problem:
// read the current state in a thunk, and include all the necessary
// data in the action
function crossSliceActionThunk() {
return (dispatch, getState) => {
const state = getState()
// Read both slices out of state
const { a, b } = state

// Include data from both slices in the action
dispatch(actionThatNeedsMoreData(a, b))
}
}

异步逻辑和副作用

¥Async Logic and Side Effects

Thunk 可能包含异步逻辑,以及更新 localStorage 等副作用。该逻辑可以使用 Promise 链接,例如 someResponsePromise.then(),尽管 async/await 语法通常更适合可读性。

¥Thunks may contain async logic, as well as side effects such as updating localStorage. That logic may use Promise chaining such as someResponsePromise.then(), although the async/await syntax is usually preferable for readability.

发出异步请求时,标准做法是在向 帮助跟踪加载状态 发出请求之前和之后分派操作。通常,请求之前的 "pending" 操作和加载状态枚举被标记为 "进行中"。如果请求成功,则调度 "fulfilled" 操作和结果数据,或者调度包含错误信息的 "rejected" 操作。

¥When making async requests, it's standard to dispatch actions before and after a request to help track loading state. Typically, a "pending" action before the request and a loading state enum is marked as "in progress". If the request succeeds, a "fulfilled" action is dispatched with the result data, or a "rejected" action is dispatched containing the error info.

这里的错误处理可能比大多数人想象的要棘手。如果将 resPromise.then(dispatchFulfilled).catch(dispatchRejected) 链接在一起,如果在处理 "fulfilled" 操作的过程中发生一些非网络错误,则最终可能会分派 "rejected" 操作。最好使用 .then() 的第二个参数来确保只处理与请求本身相关的错误:

¥Error handling here can be trickier than most people think. If you chain resPromise.then(dispatchFulfilled).catch(dispatchRejected) together, you may end up dispatching a "rejected" action if some non-network error occurs during the process of handling the "fulfilled" action. It's better to use the second argument of .then() to ensure you only handle errors related to the request itself:

Example: async request with promise chaining
function fetchData(someValue) {
return (dispatch, getState) => {
dispatch(requestStarted())

myAjaxLib.post('/someEndpoint', { data: someValue }).then(
response => dispatch(requestSucceeded(response.data)),
error => dispatch(requestFailed(error.message))
)
}
}

对于 async/await,这可能会更加棘手,因为 try/catch 逻辑通常是如何组织的。为了确保 catch 块仅处理来自网络级别的错误,可能需要重新组织逻辑,以便 thunk 在出现错误时提前返回,而 "fulfilled" 操作仅在最后发生:

¥With async/await, this can be even trickier, because of how try/catch logic is usually organized. In order to ensure that the catch block only handles errors from the network level, it may be necessary to reorganize the logic so that the thunk returns early if there's an error, and "fulfilled" action only happens at the end:

Example: error handling with async/await
function fetchData(someValue) {
return async (dispatch, getState) => {
dispatch(requestStarted())

// Have to declare the response variable outside the try block
let response

try {
response = await myAjaxLib.post('/someEndpoint', { data: someValue })
} catch (error) {
// Ensure we only catch network errors
dispatch(requestFailed(error.message))
// Bail out early on failure
return
}

// We now have the result and there's no error. Dispatch "fulfilled".
dispatch(requestSucceeded(response.data))
}
}

请注意,这个问题并不是 Redux 或 thunk 所独有的 - 即使你只使用 React 组件状态,或者任何其他需要对成功结果进行额外处理的逻辑,它也可以适用。

¥Note that this issue isn't exclusive to Redux or thunks - it can apply even if you're only working with React component state as well, or any other logic that requires additional processing of a successful result.

诚然,这种模式写起来和读起来都很尴尬。在大多数情况下,你可能可以采用更典型的 try/catch 模式,其中请求和 dispatch(requestSucceeded()) 是背对背的。仍然值得知道这可能是一个问题。

¥This pattern is admittedly awkward to write and read. In most cases you can probably get away with a more typical try/catch pattern where the request and the dispatch(requestSucceeded()) are back-to-back. It's still worth knowing that this can be an issue.

从 thunk 返回值

¥Returning Values from Thunks

默认情况下,store.dispatch(action) 返回实际的操作对象。中间件可以覆盖从 dispatch 传回的返回值,并替换它们想要返回的任何其他值。例如,中间件可以选择始终返回 42

¥By default, store.dispatch(action) returns the actual action object. Middleware can override the return value being passed back from dispatch, and substitute whatever other value they want to return. For example, a middleware could choose to always return 42 instead:

Middleware return values
const return42Middleware = storeAPI => next => action => {
const originalReturnValue = next(action)
return 42
}

// later
const result = dispatch(anyAction())
console.log(result) // 42

thunk 中间件通过返回被调用的 thunk 函数返回的任何内容来完成此操作。

¥The thunk middleware does this, by returning whatever the called thunk function returns.

最常见的用例是从 thunk 返回一个 promise。这允许调度 thunk 的代码等待 Promise 知道 thunk 的异步工作已完成。组件经常使用它来协调额外的工作:

¥The most common use case for this is returning a promise from a thunk. This allows the code that dispatched the thunk to wait on the promise to know that the thunk's async work is complete. This is often used by components to coordinate additional work:

Example: Awaiting a thunk result promise
const onAddTodoClicked = async () => {
await dispatch(saveTodo(todoText))
setTodoText('')
}

你还可以使用一个巧妙的技巧:当你只能访问 dispatch 时,你可以重新利用 thunk 作为从 Redux 状态进行一次性选择的一种方式。由于分派 thunk 返回 thunk 返回值,因此你可以编写一个接受选择器的 thunk,并立即使用状态调用选择器并返回结果。这在 React 组件中很有用,你可以访问 dispatch 但不能访问 getState

¥There's also a neat trick you can do with this: you can repurpose a thunk as a way to make a one-time selection from the Redux state when you only have access to dispatch. Since dispatching a thunk returns the thunk return value, you could write a thunk that accepts a selector, and immediately calls the selector with the state and returns the result. This can be useful in a React component, where you have access to dispatch but not getState.

Example: reusing thunks for selecting data
// In your Redux slices:
const getSelectedData = selector => (dispatch, getState) => {
return selector(getState())
}

// in a component
const onClick = () => {
const todos = dispatch(getSelectedData(selectTodos))
// do more logic with this data
}

这本身并不是推荐的做法,但它在语义上是合法的并且可以正常工作。

¥This is not a recommended practice per se, but it's semantically legal and will work fine.

使用 createAsyncThunk

¥Using createAsyncThunk

使用 thunk 编写异步逻辑可能有点乏味。每个 thunk 通常需要定义三种不同的动作类型 + "待处理/已完成/已拒绝" 的匹配动作创建者,加上实际的 thunk 动作创建者 + thunk 函数。还有需要处理错误处理的边缘情况。

¥Writing async logic with thunks can be somewhat tedious. Each thunk typically requires defining three different action types + matching action creators for "pending/fulfilled/rejected", plus the actual thunk action creator + thunk function. There's also the edge cases with error handling to deal with.

Redux Toolkit 的 createAsyncThunk API 抽象了生成这些操作的过程,根据 Promise 生命周期调度它们,并正确处理错误。它接受部分操作类型字符串(用于生成 pendingfulfilledrejected 的操作类型)和执行实际异步请求并返回 Promise 的 "有效负载创建回调"。然后,它会使用正确的参数自动分派请求之前和之后的操作。

¥Redux Toolkit has a createAsyncThunk API that abstracts the process of generating those actions, dispatching them based on a Promise lifecycle, and handling the errors correctly. It accepts a partial action type string (used to generate the action types for pending, fulfilled, and rejected), and a "payload creation callback" that does the actual async request and returns a Promise. It then automatically dispatches the actions before and after the request, with the right arguments.

由于这是异步请求特定用例的抽象,因此 createAsyncThunk 并未解决 thunk 的所有可能用例。如果你需要编写同步逻辑或其他自定义行为,你仍然应该自己手动编写 "normal" thunk。

¥Since this is an abstraction for the specific use case of async requests, createAsyncThunk does not address all possible use cases for thunks. If you need to write synchronous logic or other custom behavior, you should still write a "normal" thunk by hand yourself instead.

thunk 动作创建器附加了 pendingfulfilledrejected 的动作创建器。你可以使用 createSlice 中的 extraReducers 选项来监听这些操作类型并相应地更新切片的状态。

¥The thunk action creator has the action creators for pending, fulfilled, and rejected attached. You can use the extraReducers option in createSlice to listen for those action types and update the slice's state accordingly.

Example: createAsyncThunk
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'

// omit imports and state

export const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
const response = await client.get('/fakeApi/todos')
return response.todos
})

const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
// omit reducer cases
},
extraReducers: builder => {
builder
.addCase(fetchTodos.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchTodos.fulfilled, (state, action) => {
const newEntities = {}
action.payload.forEach(todo => {
newEntities[todo.id] = todo
})
state.entities = newEntities
state.status = 'idle'
})
}
})

使用 RTK 查询获取数据

¥Fetching Data with RTK Query

Redux 工具包有一个新的 RTK 查询数据获取 API。RTK Query 是专为 Redux 应用构建的数据获取和缓存解决方案,无需编写任何 thunk 或 reducers 来管理数据获取。

¥Redux Toolkit has a new RTK Query data fetching API. RTK Query is a purpose built data fetching and caching solution for Redux apps, and can eliminate the need to write any thunks or reducers to manage data fetching.

RTK 查询实际上在内部使用 createAsyncThunk 来处理所有请求,并使用自定义中间件来管理缓存数据生命周期。

¥RTK Query actually uses createAsyncThunk internally for all requests, along with a custom middleware to manage cache data lifetimes.

首先,创建一个 "API 切片",其中包含应用将与之通信的服务器端点的定义。每个端点将自动生成一个 React hook,其名称基于端点和请求类型,例如 useGetPokemonByNameQuery

¥First, create an "API slice" with definitions for the server endpoints your app will talk to. Each endpoint will auto-generate a React hook with a name based on the endpoint and request type, like useGetPokemonByNameQuery:

RTK Query: API slice (pokemonSlice.js)
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export const pokemonApi = createApi({
reducerPath: 'pokemonApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
endpoints: builder => ({
getPokemonByName: builder.query({
query: (name: string) => `pokemon/${name}`
})
})
})

export const { useGetPokemonByNameQuery } = pokemonApi

然后,将生成的 API 切片 reducer 和自定义中间件添加到 store 中:

¥Then, add the generated API slice reducer and custom middleware to the store:

RTK Query: store setup
import { configureStore } from '@reduxjs/toolkit'
// Or from '@reduxjs/toolkit/query/react'
import { setupListeners } from '@reduxjs/toolkit/query'
import { pokemonApi } from './services/pokemon'

export const store = configureStore({
reducer: {
// Add the generated reducer as a specific top-level slice
[pokemonApi.reducerPath]: pokemonApi.reducer
},
// Adding the api middleware enables caching, invalidation, polling,
// and other useful features of `rtk-query`.
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(pokemonApi.middleware)
})

最后,将自动生成的 React hook 导入到你的组件中并调用它。该钩子会在组件挂载时自动获取数据,如果多个组件使用相同的钩子和相同的参数,它们将共享缓存的结果:

¥Finally, import the auto-generated React hook into your component and call it. The hook will automatically fetch data when the component mounts, and if multiple components use the same hook with the same arguments, they will share the cached results:

RTK Query: using fetching hooks
import { useGetPokemonByNameQuery } from './services/pokemon'

export default function Pokemon() {
// Using a query hook automatically fetches data and returns query values
const { data, error, isLoading } = useGetPokemonByNameQuery('bulbasaur')

// rendering logic
}

我们鼓励你尝试 RTK 查询,看看它是否可以帮助简化你自己的应用中的数据获取代码。

¥We encourage you to try out RTK Query and see if it can help simplify the data fetching code in your own apps.

更多信息

¥Further Information