副作用方法
"副作用" 是什么以及它们如何融入 Redux
¥What "side effects" are and how they fit into Redux
使用 Redux 管理副作用的常用工具
¥Common tools for managing side effects with Redux
我们针对不同用例使用哪些工具的建议
¥Our recommendations for which tools to use for different use cases
Redux 和副作用
¥Redux and Side Effects
副作用概述
¥Side Effects Overview
Redux 存储本身并不了解异步逻辑。它只知道如何同步分派操作,通过调用根 reducer 函数更新状态,并通知 UI 某些内容发生了变化。任何异步性都必须发生在存储之外。
¥By itself, a Redux store doesn't know anything about async logic. It only knows how to synchronously dispatch actions, update the state by calling the root reducer function, and notify the UI that something has changed. Any asynchronicity has to happen outside the store.
Redux reducer 绝不能包含 "副作用"。"副作用" 是除了从函数返回值之外可以看到的状态或行为的任何更改。一些常见的副作用如下:
¥Redux reducers must never contain "side effects". A "side effect" is any change to state or behavior that can be seen outside of returning a value from a function. Some common kinds of side effects are things like:
将值记录到控制台
¥Logging a value to the console
保存文件
¥Saving a file
设置异步计时器
¥Setting an async timer
发出 AJAX HTTP 请求
¥Making an AJAX HTTP request
修改函数外部存在的某些状态,或改变函数的参数
¥Modifying some state that exists outside of a function, or mutating arguments to a function
生成随机数或唯一的随机 ID(例如
Math.random()
或Date.now()
)¥Generating random numbers or unique random IDs (such as
Math.random()
orDate.now()
)
然而,任何真正的应用都需要在某个地方做这些事情。那么,如果我们不能将副作用放入 reducer 中,我们可以将它们放在哪里呢?
¥However, any real app will need to do these kinds of things somewhere. So, if we can't put side effects in reducers, where can we put them?
中间件和副作用
¥Middleware and Side Effects
Redux 中间件旨在支持编写具有副作用的逻辑。
¥Redux middleware were designed to enable writing logic that has side effects.
Redux 中间件在看到已分派的操作时可以执行任何操作:记录某些内容、修改操作、延迟操作、进行异步调用等等。另外,由于中间件围绕真实的 store.dispatch
函数形成了一个管道,这也意味着我们实际上可以将一些不是普通操作对象的东西传递给 dispatch
,只要中间件拦截该值并且不让它到达 reducer。
¥A Redux middleware can do anything when it sees a dispatched action: log something, modify the action, delay the action, make an async call, and more. Also, since middleware form a pipeline around the real store.dispatch
function, this also means that we could actually pass something that isn't a plain action object to dispatch
, as long as a middleware intercepts that value and doesn't let it reach the reducers.
中间件还可以访问 dispatch
和 getState
。这意味着你可以在中间件中编写一些异步逻辑,并且仍然能够通过分派操作与 Redux 存储进行交互。
¥Middleware also have access to dispatch
and getState
. That means you could write some async logic in a middleware, and still have the ability to interact with the Redux store by dispatching actions.
因此,Redux 副作用和异步逻辑通常通过中间件实现。
¥Because of this, Redux side effects and async logic are normally implemented through middleware.
副作用用例
¥Side Effects Use Cases
实际上,典型 Redux 应用中最常见的副作用用例是从服务器获取和缓存数据。
¥In practice, the single most common use case for side effects in a typical Redux app is fetching and caching data from the server.
Redux 更具体的另一个用例是编写逻辑,通过执行附加逻辑(例如分派更多操作)来响应分派的操作或状态更改。
¥Another use case more specific to Redux is writing logic that responds to a dispatched action or state change by executing additional logic, such as dispatching more actions.
建议
¥Recommendations
我们建议使用最适合每个用例的工具(请参阅下文了解我们建议的原因以及每个工具的更多详细信息):
¥We recommend using the tools that best fit each use case (see below for the reasons for our recommendations, as well as further details on each tool):
数据获取
¥Data Fetching
使用 RTK 查询作为数据获取和缓存的默认方法
¥Use RTK Query as the default approach for data fetching and caching
如果 RTKQ 由于某种原因不完全适合,请使用
createAsyncThunk
¥If RTKQ doesn't fully fit for some reason, use
createAsyncThunk
如果没有其他办法的话,只能退回到手写的重击声
¥Only fall back to handwritten thunks if nothing else works
不要使用 sagas 或 observables 来获取数据!
¥Don't use sagas or observables for data fetching!
响应操作/状态更改、异步工作流程
¥Reacting to Actions / State Changes, Async Workflows
使用 RTK 监听器作为响应存储更新和编写长时间运行的异步工作流程的默认监听器
¥Use RTK listeners as the default for responding to store updates and writing long-running async workflows
仅当监听器不能很好地解决你的用例时才使用 sagas / observables
¥Only use sagas / observables if listeners don't solve your use case well enough
具有状态访问的逻辑
¥Logic with State Access
使用 thunk 实现复杂的同步和中等异步逻辑,包括访问
getState
和分派多个操作¥Use thunks for complex sync and moderate async logic, including access to
getState
and dispatching multiple actions
为什么要使用 RTK 查询来获取数据
¥Why RTK Query for Data Fetching
根据 "Effects 中数据获取的替代方案" 上的 React 文档部分,你应该使用内置于服务器端框架或客户端缓存中的数据获取方法。你不应该自己编写数据获取和缓存管理代码。
¥Per the React docs section on "alternatives for data fetching in Effects", you should use either data fetching approaches that are built into a server-side framework, or a client-side cache. You should not write data fetching and cache management code yourself.
RTK Query 专门设计为基于 Redux 的应用的完整数据获取和缓存层。它为你管理所有的获取、缓存和加载状态逻辑,并涵盖了许多边缘情况,如果你自己编写数据获取代码,这些情况通常会被遗忘或难以处理,并且内置了缓存生命周期管理。它还使得通过自动生成的 React hook 获取和使用数据变得简单。
¥RTK Query was specifically designed to be a complete data fetching and caching layer for Redux-based applications. It manages all the fetching, caching, and loading status logic for you, and covers many edge cases that are typically forgotten or hard to handle if you write data fetching code yourself, as well as having cache lifecycle management built-in. It also makes it simple to fetch and use data via the auto-generated React hooks.
我们特别建议不要使用 sagas 来获取数据,因为 sagas 的复杂性没有帮助,而且你仍然需要自己编写所有缓存 + 加载状态管理逻辑。
¥We specifically recommend against sagas for data fetching because the complexity of sagas is not helpful, and you would still have to write all of the caching + loading status management logic yourself.
为什么使用反应式逻辑监听器
¥Why Listeners for Reactive Logic
我们特意将 RTK 监听器中间件设计得易于使用。它使用标准 async/await
语法,涵盖最常见的反应式用例(响应操作或状态更改、去抖动、延迟),甚至几种高级用例(启动子任务)。它的包大小很小(~3K),包含在 Redux Toolkit 中,并且与 TypeScript 配合得很好。
¥We've intentionally designed the RTK listener middleware to be straightforward to use. It uses standard async/await
syntax, covers most common reactive use cases (responding to actions or state changes, debouncing, delays), and even several advanced cases (launching child tasks). It has a small bundle size (~3K), is included with Redux Toolkit, and works great with TypeScript.
出于多种原因,我们特别建议不要对大多数反应式逻辑使用 sagas 或 observable:
¥We specifically recommend against sagas or observables for most reactive logic for multiple reasons:
传奇:需要了解生成器函数语法以及 saga 效果行为;由于需要调度额外的操作,因此添加多个间接级别;TypeScript 支持较差;大多数 Redux 用例根本不需要这种功能和复杂性。
¥Sagas: require understanding generator function syntax as well as the saga effects behaviors; add multiple levels of indirection due to needing extra actions dispatched; have poor TypeScript support; and the power and complexity is simply not needed for most Redux use cases.
观察结果:需要了解 RxJS API 和心智模型;可能难以调试;可以显着增加打包包大小
¥Observables: require understanding the RxJS API and mental model; can be difficult to debug; can add significant bundle size
常见副作用的方法
¥Common Side Effects Approaches
使用 Redux 管理副作用的最底层的技术是编写你自己的自定义中间件来监听特定操作并运行逻辑。然而,这很少被使用。相反,大多数应用历史上都使用生态系统中可用的常见预构建 Redux 副作用中间件之一:thunks、sagas 或 observables。其中每一个都有其不同的用例和权衡。
¥The lowest-level technique for managing side effects with Redux is to write your own custom middleware that listens for specific actions and runs logic. However, that's rarely used. Instead, most apps have historically used one of the common pre-built Redux side effects middleware available in the ecosystem: thunks, sagas, or observables. Each of these has its own different use cases and tradeoffs.
最近,我们的官方 Redux Toolkit 包添加了两个新的 API 用于管理副作用:用于编写反应逻辑的 "listener" 中间件,以及用于获取和缓存服务器状态的 RTK 查询。
¥More recently, our official Redux Toolkit package has added two new APIs for managing side effects: the "listener" middleware for writing reactive logic, and RTK Query for fetching and caching server state.
块
¥Thunks
Redux "thunk" 中间件 传统上是最广泛使用的用于编写异步逻辑的中间件。
¥The Redux "thunk" middleware has traditionally been the most widely used middleware for writing async logic.
thunk 通过将函数传递到 dispatch
来工作。thunk 中间件拦截该函数,调用它,并传入 theThunkFunction(dispatch, getState)
。thunk 函数现在可以执行任何同步/异步逻辑并与存储交互。
¥Thunks work by passing a function into dispatch
. The thunk middleware intercepts the function, calls it, and passes in theThunkFunction(dispatch, getState)
. The thunk function can now do any sync/async logic and interact with the store.
块用例
¥Thunk Use Cases
Thunk 最适合用于需要访问 dispatch
和 getState
的复杂同步逻辑,或中等异步逻辑(例如一次性 "获取一些异步数据并使用结果调度一个操作" 请求)。
¥Thunks are best used for complex sync logic that needs access to dispatch
and getState
, or moderate async logic such as one-shot "fetch some async data and dispatch an action with the result" requests.
我们传统上建议将 thunk 作为默认方法,并且 Redux Toolkit 特别包含用于 "请求和发送" 用例的 createAsyncThunk
API。对于其他用例,你可以编写自己的 thunk 函数。
¥We have traditionally recommended thunks as the default approach, and Redux Toolkit specifically includes the createAsyncThunk
API for the "request and dispatch" use case. For other use cases, you can write your own thunk functions.
块权衡
¥Thunk Tradeoffs
👍 只需编写函数即可;可能包含任何逻辑
¥👍: Just write functions; may contain any logic
👎 无法响应调度的操作;至关重要的;无法取消
¥👎: Can't respond to dispatched actions; imperative; can't be cancelled
const thunkMiddleware =
({ dispatch, getState }) =>
next =>
action => {
if (typeof action === 'function') {
return action(dispatch, getState)
}
return next(action)
}
// Original "hand-written" thunk fetch request pattern
const fetchUserById = userId => {
return async (dispatch, getState) => {
// Dispatch "pending" action to help track loading state
dispatch(fetchUserStarted())
// Need to pull this out to have correct error handling
let lastAction
try {
const user = await userApi.getUserById(userId)
// Dispatch "fulfilled" action on success
lastAction = fetchUserSucceeded(user)
} catch (err) {
// Dispatch "rejected" action on failure
lastAction = fetchUserFailed(err.message)
}
dispatch(lastAction)
}
}
// Similar request with `createAsyncThunk`
const fetchUserById2 = createAsyncThunk('fetchUserById', async userId => {
const user = await userApi.getUserById(userId)
return user
})
传奇
¥Sagas
传统上,Redux-Saga 中间件 是继 thunks 之后第二个最常见的副作用工具。它的灵感来自后端 "saga" 模式,其中长时间运行的工作流可以响应整个系统触发的事件。
¥The Redux-Saga middleware has traditionally been the second most common tool for side effects, after thunks. It's inspired by the backend "saga" pattern, where long-running workflows can respond to events triggered throughout the system.
从概念上讲,你可以将 sagas 视为 Redux 应用内的 "后台线程",它能够监听分派的操作并运行额外的逻辑。
¥Conceptually, you can think of sagas as "background threads" inside the Redux app, which have the ability to listen to dispatched actions and run additional logic.
Sagas 是使用生成器函数编写的。Saga 函数返回副作用的描述并自行暂停,saga 中间件负责执行副作用并使用结果恢复 saga 函数。redux-saga
库包含各种效果定义,例如:
¥Sagas are written using generator functions. Saga functions return descriptions of side effects and pause themselves, and the saga middleware is responsible for executing the side effect and resuming the saga function with the result. The redux-saga
library includes a variety of effects definitions such as:
call
:执行异步函数并在 Promise 解析时返回结果:¥
call
: executes an async function and returns the result when the promise resolves:put
:调度 Redux 操作¥
put
: dispatches a Redux actionfork
:产生一个 "子 saga",就像一个可以做更多工作的附加线程¥
fork
: spawns a "child saga", like an additional thread that can do more worktakeLatest
:监听给定的 Redux 操作,触发 saga 函数执行,并在再次分派时取消之前运行的 saga 副本¥
takeLatest
: listens for a given Redux action, triggers a saga function to execute, and cancels previous running copies of the saga if it's dispatched again
佐贺用例
¥Saga Use Cases
Sagas 非常强大,最适合需要 "后台线程" 类型行为或去抖动/取消的高度复杂的异步工作流程。
¥Sagas are extremely powerful, and are best used for highly complex async workflows that require "background thread"-type behavior or debouncing/cancelling.
Saga 用户经常指出这样一个事实,即 saga 函数仅返回所需效果的描述,这是使它们更易于测试的一个主要优点。
¥Saga users have often pointed to the fact that saga functions only return descriptions of the desired effects as a major positive that makes them more testable.
传奇权衡
¥Saga Tradeoffs
👍 Sagas 是可测试的,因为它们只返回效果的描述;强大的效果模型;暂停/取消功能
¥👍: Sagas are testable because they only return descriptions of effects; powerful effects model; pause/cancel capabilities
👎 生成器函数很复杂;独特的传奇效果 API;saga 测试通常只测试实现结果,并且每次触及 saga 时都需要重写,这使得它们的价值大大降低;不能很好地与 TypeScript 配合使用;
¥👎: generator functions are complex; unique saga effects API; saga tests often only test implementation results and need to be rewritten every time the saga is touched, making them a lot less valuable; do not work well with TypeScript;
import { call, put, takeEvery } from 'redux-saga/effects'
// "Worker" saga: will be fired on USER_FETCH_REQUESTED actions
function* fetchUser(action) {
yield put(fetchUserStarted())
try {
const user = yield call(userApi.getUserById, action.payload.userId)
yield put(fetchUserSucceeded(user))
} catch (err) {
yield put(fetchUserFailed(err.message))
}
}
// "Watcher" saga: starts fetchUser on each `USER_FETCH_REQUESTED` action
function* fetchUserWatcher() {
yield takeEvery('USER_FETCH_REQUESTED', fetchUser)
}
// Can use also use sagas for complex async workflows with "child tasks":
function* fetchAll() {
const task1 = yield fork(fetchResource, 'users')
const task2 = yield fork(fetchResource, 'comments')
yield delay(1000)
}
function* fetchResource(resource) {
const { data } = yield call(api.fetch, resource)
yield put(receiveData(data))
}
观测值
¥Observables
Redux-Observable 中间件 允许你使用 RxJS observables 创建称为 "epics" 的处理管道。
¥The Redux-Observable middleware lets you use RxJS observables to create processing pipelines called "epics".
由于 RxJS 是一个与框架无关的库,可观察到的用户指出,你可以重用如何跨不同平台使用它的知识作为主要卖点。此外,RxJS 允许你构建声明性管道来处理取消或去抖动等计时情况。
¥Since RxJS is a framework-agnostic library, observable users point to the fact that you can reuse knowledge of how to use it across different platforms as a major selling point. In addition, RxJS lets you construct declarative pipelines that handle timing cases like cancellation or debouncing.
可观察的用例
¥Observable Use Cases
与 sagas 类似,可观察量功能强大,最适合需要 "后台线程" 类型行为或去抖动/取消的高度复杂的异步工作流程。
¥Similar to sagas, observables are powerful and best used for highly complex async workflows that require "background thread"-type behavior or debouncing/cancelling.
可观察到的权衡
¥Observable Tradeoffs
👍 Observables 是一个非常强大的数据流模型;RxJS 知识可以与 Redux 分开使用;声明性语法
¥👍: Observables are a highly powerful data flow model; RxJS knowledge can be used separate from Redux; declarative syntax
👎 RxJS API 很复杂;心理模型;可能很难调试;打包尺寸
¥👎: RxJS API is complex; mental model; can be hard to debug; bundle size
// Typical AJAX example:
const fetchUserEpic = action$ =>
action$.pipe(
filter(fetchUser.match),
mergeMap(action =>
ajax
.getJSON(`https://api.github.com/users/${action.payload}`)
.pipe(map(response => fetchUserFulfilled(response)))
)
)
// Can write highly complex async pipelines, including delays,
// cancellation, debouncing, and error handling:
const fetchReposEpic = action$ =>
action$.pipe(
filter(fetchReposInput.match),
debounceTime(300),
switchMap(action =>
of(fetchReposStart()).pipe(
concat(
searchRepos(action.payload).pipe(
map(payload => fetchReposSuccess(payload.items)),
catchError(error => of(fetchReposError(error)))
)
)
)
)
)
监听器
¥Listeners
Redux Toolkit 包括 createListenerMiddleware
API 来处理 "reactive" 逻辑。它专门旨在成为 sagas 和 observables 的轻量级替代品,可处理 90% 的相同用例,具有更小的包大小、更简单的 API 和更好的 TypeScript 支持。
¥Redux Toolkit includes the createListenerMiddleware
API to handle "reactive" logic. It's specifically intended to be a lighter-weight alternative to sagas and observables that handles 90% of the same use cases, with a smaller bundle size, simpler API, and better TypeScript support.
从概念上讲,这类似于 React 的 useEffect
hook,但用于 Redux 存储更新。
¥Conceptually, this is similar to React's useEffect
hook, but for Redux store updates.
监听器中间件允许你添加与操作匹配的条目,以确定何时运行 effect
回调。与 thunk 类似,effect
回调可以是同步或异步的,并且可以访问 dispatch
和 getState
。他们还收到一个 listenerApi
对象,其中包含用于构建异步工作流程的多个原语,例如:
¥The listener middleware lets you add entries that match against actions to determine when to run the effect
callback. Similar to thunks, an effect
callback can be sync or async, and have access to dispatch
and getState
. They also receive a listenerApi
object with several primitives for building async workflows, such as:
condition()
:暂停,直到调度某个操作或发生状态更改¥
condition()
: pauses until a certain action is dispatched or state change occurscancelActiveListeners()
:取消现有的正在进行的效果实例¥
cancelActiveListeners()
: cancel existing in-progress instances of the effectfork()
:创建一个可以做额外工作的 "子任务"¥
fork()
: creates a "child task" that can do additional work
这些原语允许监听器复制 Redux-Saga 中的几乎所有效果行为。
¥These primitives allow listeners to replicate almost all of the effects behaviors from Redux-Saga.
监听器用例
¥Listener Use Cases
监听器可用于各种任务,例如轻量级存储持久性、分派操作时触发附加逻辑、监视状态更改以及复杂的长时间运行的 "后台线程" 样式异步工作流程。
¥Listeners can be used for a wide variety of tasks, such as lightweight store persistence, triggering additional logic when an action is dispatched, watching for state changes, and complex long-running "background thread"-style async workflows.
此外,可以通过调度特殊的 add/removeListener
操作在运行时动态添加和删除监听器条目。这与 React 的 useEffect
钩子很好地集成,并且可用于添加与组件生命周期相对应的附加行为。
¥In addition, listener entries can be added and removed dynamically at runtime by dispatching special add/removeListener
actions. This integrates nicely with React's useEffect
hook, and can be used for adding additional behavior that corresponds to a component's lifetime.
监听器权衡
¥Listener Tradeoffs
👍 内置于 Redux 工具包中;
async/await
是更熟悉的语法;类似于 thunk;轻量化概念和尺寸;与 TypeScript 配合得很好¥👍: Built into Redux Toolkit;
async/await
is more familiar syntax; similar to thunks; lightweight concepts and size; works great with TypeScript👎 相对较新,还不如 "battle-tested";不像 sagas/observables 那样灵活
¥👎: Relatively new and not as "battle-tested" yet; not quite as flexible as sagas/observables
// Create the middleware instance and methods
const listenerMiddleware = createListenerMiddleware()
// Add one or more listener entries that look for specific actions.
// They may contain any sync or async logic, similar to thunks.
listenerMiddleware.startListening({
actionCreator: todoAdded,
effect: async (action, listenerApi) => {
// Run whatever additional side-effect-y logic you want here
console.log('Todo added: ', action.payload.text)
// Can cancel other running instances
listenerApi.cancelActiveListeners()
// Run async logic
const data = await fetchData()
// Use the listener API methods to dispatch, get state,
// unsubscribe the listener, start child tasks, and more
listenerApi.dispatch(todoAdded('Buy pet food'))
}
})
listenerMiddleware.startListening({
// Can match against actions _or_ state changes/contents
predicate: (action, currentState, previousState) => {
return currentState.counter.value !== previousState.counter.value
},
// Listeners can have long-running async workflows
effect: async (action, listenerApi) => {
// Pause until action dispatched or state changed
if (await listenerApi.condition(matchSomeAction)) {
// Spawn "child tasks" that can do more work and return results
const task = listenerApi.fork(async forkApi => {
// Can pause execution
await forkApi.delay(5)
// Complete the child by returning a value
return 42
})
// Unwrap the child result in the listener
const result = await task.result
if (result.status === 'ok') {
console.log('Child succeeded: ', result.value)
}
}
}
})
RTK 查询
¥RTK Query
Redux Toolkit 包括 RTK 查询,这是一个专门为 Redux 应用构建的数据获取和缓存解决方案。它旨在简化在 Web 应用中加载数据的常见情况,无需你自己手动编写数据获取和缓存逻辑。
¥Redux Toolkit includes RTK Query, a purpose-built data fetching and caching solution for Redux apps. It's designed to simplify common cases for loading data in a web application, eliminating the need to hand-write data fetching & caching logic yourself.
RTK 查询依赖于创建由许多 "endpoints" 组成的 API 定义。端点可以是用于获取数据的 "query",也可以是用于向服务器发送更新的 "mutation"。RTKQ 在内部管理数据的获取和缓存,包括跟踪每个缓存条目的使用情况以及删除不再需要的缓存数据。它具有独特的 "tag" 系统,用于在服务器上的突变更新状态时触发自动重新获取数据。
¥RTK Query relies on creating an API definition consisting of many "endpoints". An endpoint can be a "query" for fetching data, or a "mutation" for sending an update to the server. RTKQ manages fetching and caching data internally, including tracking usage of each cache entry and removing cached data that's no longer needed. It features a unique "tag" system for triggering automatic refetches of data as mutations update state on the server.
与 Redux 的其他部分一样,RTKQ 的核心与 UI 无关,并且可以与任何 UI 框架一起使用。然而,它还内置了 React 集成,并且可以自动为每个端点生成 React hooks。这提供了一个熟悉且简单的 API,用于从 React 组件获取和更新数据。
¥Like the rest of Redux, RTKQ is UI-agnostic at its core, and can be used with any UI framework. However, it also comes with React integration built in, and can automatically generate React hooks for each endpoint. This provides a familiar and simple API for fetching and updating data from React components.
RTKQ 提供了一个基于 fetch
的开箱即用实现,并且与 REST API 配合得很好。它还足够灵活,可以与 GraphQL API 一起使用,甚至可以配置为与任意异步函数一起使用,从而允许与 Firebase、Supabase 等外部 SDK 或你自己的异步逻辑集成。
¥RTKQ provides a fetch
-based implementation out of the box, and works great with REST APIs. It's also flexible enough to be used with GraphQL APIs, and can even be configured to work with arbitrary async functions, allowing integration with external SDKs such as Firebase, Supabase, or your own async logic.
RTKQ 还具有端点 "生命周期方法" 等强大功能,允许你在添加和删除缓存条目时运行逻辑。这可用于以下场景:获取聊天室的初始数据,然后订阅套接字以获取用于更新缓存的其他消息。
¥RTKQ also has powerful capabilities such as endpoint "lifecycle methods", allowing you to run logic as cache entries are added and removed. This can be used for scenarios like fetching initial data for a chat room, then subscribing to a socket for additional messages that are used to update the cache.
RTK 查询用例
¥RTK Query Use Cases
RTK 查询是专门为解决数据获取和服务器状态缓存的用例而构建的。
¥RTK Query is specifically built to solve the use case of data fetching and caching of server state.
RTK 查询权衡
¥RTK Query Tradeoffs
👍 内置 RTK;无需编写任何代码(thunk、选择器、效果、reducers)来管理数据获取和加载状态;与 TS 配合良好;集成到 Redux 存储的其余部分;内置 React 钩子
¥👍: Built into RTK; eliminates the need to write any code (thunks, selectors, effects, reducers) for managing data fetching and loading state; works great with TS; integrates into the rest of the Redux store; built-in React hooks
👎 故意使用 "document" 风格的缓存,而不是 "normalized";增加一次性额外打包尺寸成本
¥👎: Intentionally a "document"-style cache, rather than "normalized"; Adds a one-time additional bundle size cost
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Pokemon } from './types'
// Create an API definition using a base URL and expected endpoints
export const api = createApi({
reducerPath: 'pokemonApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
endpoints: builder => ({
getPokemonByName: builder.query<Pokemon, string>({
query: name => `pokemon/${name}`
}),
getPosts: builder.query<Post[], void>({
query: () => '/posts'
}),
addNewPost: builder.mutation<void, Post>({
query: initialPost => ({
url: '/posts',
method: 'POST',
// Include the entire post object as the body of the request
body: initialPost
})
})
})
})
// Export hooks for usage in functional components, which are
// auto-generated based on the defined endpoints
export const { useGetPokemonByNameQuery } = api
export default function App() {
// Using a query hook automatically fetches data and returns query values
const { data, error, isLoading } = useGetPokemonByNameQuery('bulbasaur')
// render UI based on data and loading state
}
其他方法
¥Other Approaches
定制中间件
¥Custom Middleware
鉴于 thunk、sagas、observables 和监听器都是 Redux 中间件的所有形式(RTK Query 包含其自己的自定义中间件),如果这些工具都不能充分处理你的用例,那么始终可以编写你自己的自定义中间件。
¥Given that thunks, sagas, observables, and listeners are all forms of Redux middleware (and RTK Query includes its own custom middleware), it's always possible to write your own custom middleware if none of these tools sufficiently handles your use cases.
请注意,我们特别建议不要尝试使用自定义中间件作为管理大部分应用逻辑的技术!一些用户尝试创建数十个自定义中间件,每个特定的应用功能一个。这会增加显着的开销,因为每个中间件都必须作为每次调用 dispatch
的一部分来运行。最好使用通用中间件,例如 thunk 或监听器,其中添加一个可以处理许多不同逻辑块的中间件实例。
¥Note that we specifically recommend against trying to use custom middleware as a technique for managing the bulk of your app's logic! Some users have tried creating dozens of custom middleware, one per specific app feature. This adds significant overhead, as each middleware has to run as part of each call to dispatch
. It's better to use a general-purpose middleware such as thunks or listeners instead, where there's a single middleware instance added that can handle many different chunks of logic.
const delayedActionMiddleware = storeAPI => next => action => {
if (action.type === 'todos/todoAdded') {
setTimeout(() => {
// Delay this action by one second
next(action)
}, 1000)
return
}
return next(action)
}
网络套接字
¥Websockets
许多应用使用 Websocket 或某种其他形式的持久连接,主要是为了从服务器接收流式更新。
¥Many apps use websockets or some other form of persistent connection, primarily to receive streaming updates from the server.
我们一般推荐 Redux 应用中的大多数 websocket 使用应该位于自定义中间件中,原因如下:
¥We generally recommend that most websocket usage in a Redux app should live inside a custom middleware, for several reasons:
中间件在应用的整个生命周期中存在
¥Middleware exist for the lifetime of the application
与存储本身一样,你可能只需要整个应用可以使用的给定连接的单个实例
¥Like with the store itself, you probably only need a single instance of a given connection that the whole app can use
中间件可以查看所有已分派的操作并自行分派操作。这意味着中间件可以采取分派的操作并将其转换为通过 websocket 发送的消息,并在通过 websocket 接收到消息时分派新的操作。
¥Middleware can see all dispatched actions and dispatch actions themselves. This means a middleware can take dispatched actions and turn those into messages sent over the websocket, and dispatch new actions when a message is received over the websocket.
Websocket 连接实例不可序列化,因此 它不属于存储状态本身
¥A websocket connection instance isn't serializable, so it doesn't belong in the store state itself
根据应用的需要,你可以将套接字创建为中间件初始化进程的一部分,通过分派初始化操作在中间件中按需创建套接字,或者在单独的模块文件中创建它,以便可以在其他地方访问它。
¥Depending on the needs of the application, you could create the socket as part of the middleware init process, create the socket on demand in the middleware by dispatching an initialization action, or create it in a separate module file so it can be accessed elsewhere.
Websockets 还可以在 RTK 查询生命周期回调中使用,它们可以通过对 RTKQ 缓存应用更新来响应消息。
¥Websockets can also be used in an RTK Query lifecycle callback, where they could respond to messages by applying updates to the RTKQ cache.
XState
状态机对于定义系统可能的已知状态以及每个状态之间可能的转换以及在转换发生时触发副作用非常有用。
¥State machines can be very useful for defining possible known states for a system and the possible transitions between each state, as well as triggering side effects when a transition occurs.
Redux reducer 可以编写为真正的有限状态机,但 RTK 不包含任何对此有帮助的内容。在实践中,它们往往是部分状态机,实际上只关心分派的操作来确定如何更新状态。监听器、传奇和可观察量可用于 "调度后运行副作用" 方面,但有时可能需要更多工作来确保副作用仅在特定时间运行。
¥Redux reducers can be written as true Finite State Machines, but RTK doesn't include anything to help with this. In practice, they tend to be partial state machines that really only care about the dispatched action to determine how to update the state. Listeners, sagas, and observables can be used for the "run side effects after dispatch" aspect, but can sometimes require more work to ensure a side effect only runs at a specific time.
XState 是一个功能强大的库,用于定义真正的状态机并执行它们,包括管理基于事件的状态转换和触发相关的副作用。它还具有用于通过图形编辑器创建状态机定义的相关工具,然后可以将其加载到 XState 逻辑中执行。
¥XState is a powerful library for defining true state machines and executing them, including managing state transitions based on events and triggering related side effects. It also has related tools for creating state machine definitions via a graphical editor, which can then be loaded into the XState logic for execution.
虽然目前 XState 和 Redux 之间没有官方集成,但可以使用 XState 机器作为 Redux reducer,并且 XState 开发者创建了一个有用的 POC 演示使用 XState 作为 Redux 副作用中间件:
¥While there currently is no official integration between XState and Redux, it is possible to use an XState machine as a Redux reducer, and the XState developers have created a useful POC demonstrating using XState as a Redux side effects middleware:
更多信息
¥Further Information
¥Presentation: The Evolution of Redux Async Logic
中间件的原因和副作用:
¥Reason for middleware and side effects:
文档和教程:
¥Docs and Tutorials:
文章和比较
¥Articles and comparisons