Redux 常见问题解答:代码结构
目录
- 我的文件结构应该是什么样的?我应该如何在我的项目中对我的 action creators 和 reducers 进行分组?我的 selectors 应该放在哪里?
- 我应该如何在 reducers 和 action creators 之间拆分我的逻辑?我的“业务逻辑”应该放在哪里?
- 为什么我应该使用 action creators?
- WebSockets 和其他持久连接应该放在哪里?
- 如何在非组件文件中使用 Redux store?
我的文件结构应该是什么样的?我应该如何在我的项目中对我的 action creators 和 reducers 进行分组?我的 selectors 应该放在哪里?
由于 Redux 只是一个数据存储库,它对项目的结构没有直接的意见。但是,大多数 Redux 开发人员倾向于使用一些常见的模式
- Rails 风格:为“actions”、“constants”、“reducers”、“containers”和“components”分别创建文件夹。
- “功能文件夹”/“领域”风格:为每个功能或领域创建单独的文件夹,可能包含按文件类型划分的子文件夹。
- “Ducks/Slices”:类似于领域风格,但明确地将 actions 和 reducers 绑定在一起,通常在同一个文件中定义它们。
通常建议将 selectors 与 reducers 一起定义并导出,然后在其他地方重用(例如在 mapStateToProps
函数、异步 action creators 或 sagas 中),以便将所有了解状态树实际形状的代码集中在 reducer 文件中。
我们特别建议将您的逻辑组织成“功能文件夹”,将给定功能的所有 Redux 逻辑放在一个“切片/ducks”文件中。.
请参阅本节以获取示例。
详细解释:示例文件夹结构
示例文件夹结构可能如下所示/src
index.tsx
:渲染 React 组件树的入口文件。/app
store.ts
:存储设置。rootReducer.ts
:根 reducer(可选)。App.tsx
:根 React 组件。
/common
:hooks、通用组件、utils 等。/features
:包含所有“功能文件夹”。/todos
:一个单独的功能文件夹。todosSlice.ts
:Redux reducer 逻辑和相关 actions。Todos.tsx
:一个 React 组件。
/app
包含依赖于所有其他文件夹的应用程序范围的设置和布局。
/common
包含真正通用且可重用的实用程序和组件。
/features
包含与特定功能相关的文件夹。在本例中,todosSlice.ts
是一个“duck”风格的文件,它包含对 RTK 的 createSlice()
函数的调用,并导出切片 reducer 和 action creators。
虽然最终如何将代码布局在磁盘上并不重要,但重要的是要记住,actions 和 reducers 不应该被孤立地考虑。一个文件夹中定义的 reducer 完全有可能(也鼓励)响应另一个文件夹中定义的 action。
更多信息
文档
文章
- 如何扩展 React 应用程序(配套演讲:扩展 React 应用程序)
- Redux 最佳实践
- 构建(Redux)应用程序的规则
- React/Redux 应用程序的更好文件结构
- 组织代码的四种策略
- 封装 Redux 状态树
- Redux Reducer/Selector 差异
- 模块化 Reducer 和 Selector
- 我构建可维护的 React/Redux 项目结构的旅程
- React/Redux 链接:架构 - 项目文件结构
讨论
- #839:强调在 reducer 旁边定义 selector
- #943:Reducer 查询
- React Boilerplate #27:应用程序结构
- Stack Overflow:如何构建 Redux 组件/容器
- Twitter:Redux 没有终极文件结构
我应该如何在 reducer 和 action creator 之间拆分我的逻辑?我的“业务逻辑”应该放在哪里?
对于哪些逻辑片段应该放在 reducer 或 action creator 中,没有一个明确的答案。一些开发人员更喜欢使用“胖” action creator,以及“瘦” reducer,这些 reducer 只接收 action 中的数据并将其盲目合并到相应的 state 中。其他人则试图强调保持 action 尽可能小,并最大限度地减少在 action creator 中使用 getState()
。 (就这个问题而言,其他异步方法(如 saga 和 observable)属于“action creator”类别。)
将更多逻辑放入 reducer 中有几个潜在的好处。action 类型可能更有语义和更有意义(例如 "USER_UPDATED"
而不是 "SET_STATE"
)。此外,在 reducer 中拥有更多逻辑意味着更多功能将受到时间旅行调试的影响。
此评论很好地总结了这种二分法
现在,问题是将什么放入动作创建器中,将什么放入 reducer 中,即在胖动作对象和瘦动作对象之间做出选择。如果你将所有逻辑都放在动作创建器中,你最终会得到胖动作对象,这些对象基本上声明了对状态的更新。reducer 变得纯粹、愚蠢,添加这个、删除那个、更新这些函数。它们将很容易组合。但你的大部分业务逻辑都不会在那里。如果你在 reducer 中放置更多逻辑,你最终会得到漂亮、瘦的动作对象,大部分数据逻辑都在一个地方,但你的 reducer 难以组合,因为你可能需要来自其他分支的信息。你最终会得到大型 reducer 或需要从状态中更高层接收额外参数的 reducer。
我们建议将尽可能多的逻辑放入 reducer 中。有时你可能需要一些逻辑来帮助准备放入动作的内容,但 reducer 应该完成大部分工作。
更多信息
文档
文章
讨论
- 将太多逻辑放入动作创建器中如何影响调试
- #384:reducer 中的内容越多,你就可以通过时间旅行重放的内容就越多
- #1165:将业务逻辑/验证放在哪里?
- #1171:关于动作创建器、reducer 和选择器的最佳实践建议
- Stack Overflow:在动作创建器中访问 Redux 状态?
- #2796:澄清“业务逻辑”
- Twitter:远离不明确的术语...
为什么要使用动作创建器?
Redux 不需要动作创建器。你可以自由地以任何对你来说最合适的方式创建动作,包括简单地将一个对象字面量传递给 dispatch
。动作创建器源于 Flux 架构,并已被 Redux 社区采用,因为它们提供了几个好处。
动作创建器更易于维护。对动作的更新可以在一个地方进行,并应用于所有地方。所有动作实例都保证具有相同的形状和相同的默认值。
动作创建器是可测试的。内联操作的正确性必须手动验证。与任何函数一样,动作创建器的测试可以编写一次并自动运行。
动作创建器更容易记录。动作创建器的参数枚举了操作的依赖项。动作定义的集中化提供了一个方便的地方来放置文档注释。当操作内联编写时,这些信息更难捕获和传达。
动作创建器是一种更强大的抽象。创建操作通常涉及转换数据或进行 AJAX 请求。动作创建器为这种变化的逻辑提供了一个统一的接口。这种抽象使组件能够分派操作,而不会被该操作创建的细节所复杂化。
更多信息
文章
讨论
WebSockets 和其他持久连接应该放在哪里?
由于以下几个原因,中间件是 Redux 应用程序中持久连接(如 WebSockets)的正确位置。
- 中间件在应用程序的生命周期内存在。
- 与存储本身一样,您可能只需要一个给定连接的单个实例,整个应用程序都可以使用它。
- 中间件可以查看所有分派的 action 并分派 action 本身。这意味着中间件可以接收分派的 action 并将其转换为通过 WebSocket 发送的消息,并在通过 WebSocket 接收消息时分派新的 action。
- WebSocket 连接实例不可序列化,因此它不属于存储状态本身
查看此示例,它展示了套接字中间件如何分派和响应 Redux action。
有很多现有的中间件用于 WebSockets 和其他类似连接 - 请参阅下面的链接。
库
如何在非组件文件中使用 Redux 存储?
每个应用程序应该只有一个 Redux 存储。这使其在应用程序架构方面实际上是一个单例。当与 React 一起使用时,存储在运行时通过在根 <App>
组件周围渲染 <Provider store={store}>
来注入到组件中,因此只有应用程序设置逻辑需要直接导入存储。
但是,代码库的其他部分可能也需要与存储交互。
您应该避免直接将存储导入到其他代码库文件中。虽然它在某些情况下可能有效,但这通常会导致循环导入依赖错误。
一些可能的解决方案是
- 将依赖于存储的逻辑编写为 thunk,然后从组件中分派该 thunk
- 将对
dispatch
的引用作为参数从组件传递到相关函数 - 将逻辑编写为中间件并在设置时将其添加到存储中
- 在创建应用程序时将存储实例注入相关文件。
一个常见的用例是从 Redux 状态中读取 API 授权信息(例如令牌),例如在 Axios 拦截器中。拦截器文件需要引用 store.getState()
,但也需要导入到 API 层文件,这会导致循环导入。
您可以从拦截器文件公开一个 injectStore
函数
let store
export const injectStore = _store => {
store = _store
}
axiosInstance.interceptors.request.use(config => {
config.headers.authorization = store.getState().auth.token
return config
})
然后,在您的入口点文件中,将存储注入到 API 设置文件中
import store from './app/store'
import { injectStore } from './common/api'
injectStore(store)
这样,应用程序设置是唯一需要导入存储的代码,并且文件依赖关系图避免了循环依赖。