Redux 常见问题解答:代码结构
¥Redux FAQ: Code Structure
目录
¥Table of Contents
¥Where should websockets and other persistent connections live?
我的文件结构应该是什么样的?我应该如何在项目中对动作创建者和减速者进行分组?我的选择器应该去哪里?
¥What should my file structure look like? How should I group my action creators and reducers in my project? Where should my selectors go?
由于 Redux 只是一个数据存储库,因此它对于如何构建你的项目没有直接的意见。然而,大多数 Redux 开发者倾向于使用一些常见模式:
¥Since Redux is just a data store library, it has no direct opinion on how your project should be structured. However, there are a few common patterns that most Redux developers tend to use:
导轨式:“actions”、“constants”、“reducers”、“containers”和“components”的单独文件夹
¥Rails-style: separate folders for “actions”, “constants”, “reducers”, “containers”, and “components”
"功能文件夹" / "域"-style :每个功能或域单独的文件夹,可能每个文件类型都有子文件夹
¥"Feature folders" / "Domain"-style : separate folders per feature or domain, possibly with sub-folders per file type
“动态/切片”:与字段风格类似,但通常通过在同一文件中定义它们来显式地将操作和化简器联系在一起
¥“Ducks/Slices”: similar to domain style, but explicitly tying together actions and reducers, often by defining them in the same file
通常建议选择器与 reducer 一起定义并导出,然后在其他地方重用(例如在 mapStateToProps
函数、异步操作创建器或 sagas 中),以将所有了解 reducer 文件中状态树实际形状的代码放在一起。
¥It's generally suggested that selectors are defined alongside reducers and exported, and then reused elsewhere (such as in mapStateToProps
functions, in async action creators, or sagas) to colocate all the code that knows about the actual shape of the state tree in the reducer files.
我们特别建议将你的逻辑组织到 "功能文件夹" 中,并将给定功能的所有 Redux 逻辑放在单个 "切片/动态" 文件中。
¥We specifically recommend organizing your logic into "feature folders", with all the Redux logic for a given feature in a single "slice/ducks" file.
请参阅本节的示例:
¥See this section for an example:
Detailed Explanation: Example Folder Structure
示例文件夹结构可能类似于:
¥An example folder structure might look something like:
/src
index.tsx
:渲染 React 组件树的入口点文件¥
index.tsx
: Entry point file that renders the React component tree/app
store.ts
:存储设置¥
store.ts
: store setuprootReducer.ts
:根部 reducer(可选)¥
rootReducer.ts
: root reducer (optional)App.tsx
:根反应组件¥
App.tsx
: root React component
/common
:钩子、通用组件、实用程序等¥
/common
: hooks, generic components, utils, etc/features
:包含全部 "功能文件夹"¥
/features
: contains all "feature folders"/todos
:单个功能文件夹¥
/todos
: a single feature foldertodosSlice.ts
:Redux reducer 逻辑和相关操作¥
todosSlice.ts
: Redux reducer logic and associated actionsTodos.tsx
:一个反应组件¥
Todos.tsx
: a React component
/app
包含依赖于所有其他文件夹的应用范围的设置和布局。
¥/app
contains app-wide setup and layout that depends on all the other folders.
/common
包含真正通用且可重用的实用程序和组件。
¥/common
contains truly generic and reusable utilities and components.
/features
的文件夹包含与特定功能相关的所有功能。在此示例中,todosSlice.ts
是 "duck" 样式的文件,其中包含对 RTK 的 createSlice()
函数的调用,并导出切片缩减器和动作创建器。
¥/features
has folders that contain all functionality related to a specific feature. In this example, todosSlice.ts
is a "duck"-style file that contains a call to RTK's createSlice()
function, and exports the slice reducer and action creators.
虽然最终如何在磁盘上布置代码并不重要,但重要的是要记住,不应孤立地考虑操作和化简器。一个文件夹中定义的 reducer 完全有可能(并且鼓励)响应另一个文件夹中定义的操作。
¥While it ultimately doesn't matter how you lay out your code on disk, it's important to remember that actions and reducers should not be considered in isolation. It's entirely possible (and encouraged) for a reducer defined in one folder to respond to an action defined in another folder.
更多信息
¥Further information
文档
¥Documentation
¥Style Guide: Structure Files as Feature Folders with Single-File Logic
文章
¥Articles
¥How to Scale React Applications (accompanying talk: Scaling React Applications)
¥My journey towards a maintainable project structure for React/Redux
讨论
¥Discussions
我应该如何在 reducer 和动作创建者之间划分逻辑?我的“业务逻辑”应该去哪里?
¥How should I split my logic between reducers and action creators? Where should my “business logic” go?
对于 reducer 或动作创建器中应该包含哪些逻辑部分,没有一个明确的答案。一些开发者更喜欢“胖”动作创建器,而“瘦”reducer 则简单地获取动作中的数据并盲目地将其合并到相应的状态中。其他人则试图强调保持动作尽可能小,并尽量减少动作创建器中 getState()
的使用。(就本问题而言,其他异步方法(例如 sagas 和 observables)属于 "动作创造者" 类别。)
¥There's no single clear answer to exactly what pieces of logic should go in a reducer or an action creator. Some developers prefer to have “fat” action creators, with “thin” reducers that simply take the data in an action and blindly merge it into the corresponding state. Others try to emphasize keeping actions as small as possible, and minimize the usage of getState()
in an action creator. (For purposes of this question, other async approaches such as sagas and observables fall in the "action creator" category.)
将更多逻辑放入 reducer 中有几个潜在的好处。操作类型可能会更具语义且更有意义(例如 "USER_UPDATED"
而不是 "SET_STATE"
)。此外,reducer 中拥有更多逻辑意味着更多功能将受到时间旅行调试的影响。
¥There are several potential benefits from putting more logic into your reducers. It's likely that the action types would be more semantic and more meaningful (such as "USER_UPDATED"
instead of "SET_STATE"
). In addition, having more logic in reducers means that more functionality will be affected by time travel debugging.
这条评论很好地总结了这种二分法:
¥This comment sums up the dichotomy nicely:
现在的问题是在动作创建器中放入什么,在 reducer 中放入什么,在胖动作对象和瘦动作对象之间进行选择。如果将所有逻辑放在操作创建器中,最终会得到胖操作对象,这些对象基本上声明了状态的更新。reducer 变得纯粹、愚蠢、添加这个、删除那个、更新这些函数。它们很容易创作。但那里不会有太多的业务逻辑。如果你在 reducer 中放入更多逻辑,你最终会得到漂亮、精简的操作对象,大部分数据逻辑都集中在一个地方,但是你的 reducer 更难组合,因为你可能需要来自其他分支的信息。你最终会得到大型 reducer 或 reducer,这些 reducer 从状态的更高层获取额外的参数。
¥Now, the problem is what to put in the action creator and what in the reducer, the choice between fat and thin action objects. If you put all the logic in the action creator, you end up with fat action objects that basically declare the updates to the state. Reducers become pure, dumb, add-this, remove that, update these functions. They will be easy to compose. But not much of your business logic will be there. If you put more logic in the reducer, you end up with nice, thin action objects, most of your data logic in one place, but your reducers are harder to compose since you might need info from other branches. You end up with large reducers or reducers that take additional arguments from higher up in the state.
我们建议将尽可能多的逻辑放入 reducer 中。有时你可能需要一些逻辑来帮助准备要执行的操作,但 reducer 应该完成大部分工作。
¥We recommend putting as much logic as possible into reducers. There are times when you may need some logic to help prepare what goes into the action, but reducers should do most of the work.
更多信息
¥Further information
文档
¥Documentation
文章
¥Articles
¥Where do I put my business logic in a React/Redux application?
Redux 之道,第 2 部分 - 实践与哲学。粗 reducer 和细 reducer。
¥The Tao of Redux, Part 2 - Practice and Philosophy. Thick and thin reducers.
讨论
¥Discussions
¥How putting too much logic in action creators could affect debugging
#384:reducer 中的内容越多,你可以通过时间旅行重播的内容就越多
¥#384: The more that's in a reducer, the more you can replay via time travel
#1171:有关操作创建者、reducer 和选择器的最佳实践建议
¥#1171: Recommendations for best practices regarding action-creators, reducers, and selectors
¥Stack Overflow: Accessing Redux state in an action creator?
为什么我应该使用动作创建器?
¥Why should I use action creators?
Redux 不需要操作创建者。你可以自由地以最适合你的任何方式创建操作,包括简单地将对象字面量传递给 dispatch
。Action Creator 诞生于 通量架构,并已被 Redux 社区采用,因为它们提供了多种好处。
¥Redux does not require action creators. You are free to create actions in any way that is best for you, including simply passing an object literal to dispatch
. Action creators emerged from the Flux architecture and have been adopted by the Redux community because they offer several benefits.
动作创建者更易于维护。对操作的更新可以在一处进行并应用到任何地方。保证动作的所有实例具有相同的形状和相同的默认值。
¥Action creators are more maintainable. Updates to an action can be made in one place and applied everywhere. All instances of an action are guaranteed to have the same shape and the same default values.
动作创建者是可测试的。内联操作的正确性必须手动验证。与任何函数一样,动作创建器的测试可以编写一次并自动运行。
¥Action creators are testable. The correctness of an inline action must be verified manually. Like any function, tests for an action creator can be written once and run automatically.
动作创建者更容易记录。操作创建者的参数枚举操作的依赖。操作定义的集中化为文档注释提供了方便的地方。当操作内联编写时,此信息更难以捕获和传达。
¥Action creators are easier to document. The action creator's parameters enumerate the action's dependencies. And centralization of the action definition provides a convenient place for documentation comments. When actions are written inline, this information is harder to capture and communicate.
动作创建者是一个更强大的抽象。创建操作通常涉及转换数据或发出 AJAX 请求。动作创建者为这种不同的逻辑提供了统一的接口。这种抽象可以释放组件来分派操作,而不会因为该操作的创建细节而变得复杂。
¥Action creators are a more powerful abstraction. Creating an action often involves transforming data or making AJAX requests. Action creators provide a uniform interface to this varied logic. This abstraction frees a component to dispatch an action without being complicated by the details of that action's creation.
更多信息
¥Further information
文章
¥Articles
讨论
¥Discussions
Websocket 和其他持久连接应该位于哪里?
¥Where should websockets and other persistent connections live?
中间件是 Redux 应用中 Websocket 等持久连接的正确选择,原因如下:
¥Middleware are the right place for persistent connections like websockets in a Redux app, 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
参见 此示例展示了套接字中间件如何调度和响应 Redux 操作。
¥See this example that shows how a socket middleware might dispatch and respond to Redux actions.
有许多用于 websocket 和其他类似连接的现有中间件 - 请参阅下面的链接。
¥There's many existing middleware for websockets and other similar connections - see the link below.
库
¥Libraries
如何在非组件文件中使用 Redux 存储?
¥How can I use the Redux store in non-component files?
每个应用应该只有一个 Redux 存储。这使得它在应用架构方面实际上成为单例。当与 React 一起使用时,存储会在运行时通过在根 <App>
组件周围渲染 <Provider store={store}>
来注入到组件中,因此只有应用设置逻辑需要直接导入存储。
¥There should only be a single Redux store per application. This makes it effectively a singleton in terms of the app architecture. When used with React, the store is injected into the components at runtime by rendering a <Provider store={store}>
around the root <App>
component, so only the application setup logic needs to import the store directly.
然而,有时代码库的其他部分也可能需要与存储交互。
¥However, there may be times when other parts of the codebase need to interact with the store as well.
你应该避免将存储直接导入到其他代码库文件中。虽然它在某些情况下可能有效,但这通常最终会导致循环导入依赖错误。
¥You should avoid importing the store directly into other codebase files. While it may work in some cases, that often ends up causing circular import dependency errors.
一些可能的解决方案是:
¥Some possible solutions are:
将依赖于存储的逻辑编写为 thunk,然后从组件分派该 thunk
¥Write your store-dependent logic as a thunk, and then dispatch that thunk from a component
将组件中对
dispatch
的引用作为参数传递给相关函数¥Pass along references to
dispatch
from components as arguments to the relevant functions将逻辑编写为中间件并在设置时将它们添加到存储中
¥Write the logic as middleware and add them to the store at setup time
创建应用时将存储实例注入相关文件中。
¥Inject the store instance into the relevant files as the app is being created.
一种常见的用例是从 Axios 拦截器内部的 Redux 状态读取 API 授权信息,例如令牌。拦截器文件需要引用 store.getState()
,同时还需要导入到 API 层文件中,这就导致了循环导入。
¥One common use case is reading API authorization information such as a token from the Redux state, inside of an Axios interceptor. The interceptor file needs to reference store.getState()
, but also needs to be imported into API layer files, and this leads to circular imports.
你可以从拦截器文件中公开 injectStore
函数:
¥You can expose an injectStore
function from the interceptor file instead:
let store
export const injectStore = _store => {
store = _store
}
axiosInstance.interceptors.request.use(config => {
config.headers.authorization = store.getState().auth.token
return config
})
然后,在入口点文件中,将存储注入 API 设置文件中:
¥Then, in your entry point file, inject the store into the API setup file:
import store from './app/store'
import { injectStore } from './common/api'
injectStore(store)
这样,应用设置是唯一必须导入存储的代码,并且文件依赖图避免了循环依赖。
¥This way, the application setup is the only code that has to import the store, and the file dependency graph avoids circular dependencies.