Redux:将 Selector 与 Reducers 共存

Posted

技术标签:

【中文标题】Redux:将 Selector 与 Reducers 共存【英文标题】:Redux: Colocating Selectors with Reducers 【发布时间】:2017-05-07 13:34:07 【问题描述】:

在这个Redux: Colocating Selectors with Reducers Egghead 教程中,Dan Abramov 建议使用接受完整 状态树而不是状态切片的选择器来封装远离组件的状态知识。他认为这使得更改状态结构变得更容易,因为组件不知道它,我完全同意。

但是,他建议的方法是,对于对应于特定状态切片的每个选择器,我们在根 reducer 旁边再次定义它,以便它可以接受完整状态。当然,这种实现开销会破坏他试图实现的目标......简化未来更改状态结构的过程。

在一个有很多reducer 的大型应用程序中,每个reducer 都有很多选择器,如果我们在根reducer 文件中定义所有选择器,我们是否会不可避免地遇到命名冲突?直接从其相关的 reducer 导入选择器并传入全局状态而不是相应的状态切片有什么问题?例如

const todos = (state = [], action) => 
  switch (action.type) 
    case 'ADD_TODO':
      return [...state, todo(undefined, action)];
    case 'TOGGLE_TODO':
      return state.map(t => todo(t, action));
    default:
      return state;
  
;

export default todos;

export const getVisibleTodos = (globalState, filter) => 
  switch (filter) 
    case 'all':
      return globalState.todos;
    case 'completed':
      return globalState.todos.filter(t => t.completed);
    case 'active':
      return globalState.todos.filter(t => !t.completed);
    default:
      throw new Error(`Unknown filter: $filter.`);
  
;

这样做有什么缺点吗?

【问题讨论】:

是的,我刚刚观看了那个视频,我可以看到一旦应用程序开始增长,一个动作就拥有 3 个真实来源,这看起来很糟糕 - 很糟糕。从 jsx 文件中的这一操作,我们调用 reducer/index 文件,该文件现在具有对保存状态的 reducer 文件的引用。我不知道,这对我来说 - 似乎有很多开销,并且要实例化一个需要数据片段的方法,我们现在必须在 3 个不同的文件中感受到它的存在。现在,将其乘以 50 或 100……也许在像“todos”这样的非常基础的应用程序中,它很好。但它不是现实世界。 Redux 中的很多东西都是建议的最佳实践,但这并不一定意味着它们最适合所有情况或您的用例。我做事的方式和你一样:将选择器与它们对应的减速器放在一起。我认为这更有意义,因为访问状态部分的知识位于定义状态部分的函数旁边。不过,这真的是关于什么对你最有效,而且 Redux 社区中的许多人都认为这就是你应该追求的。 在大型应用程序中,我认为教程中提出的结构不再有效。您需要根据它们所针对的特定域对象来拆分您的减速器/选择器/操作。要回答您的问题,除了您在组件和特定 reducer 之间引入了很多依赖关系之外,没有任何缺点 我在datchley.name/scoped-selectors-for-redux-modules找到了一篇关于这个问题的好帖子 【参考方案1】:

我自己犯了这个错误(不是使用 Redux,而是使用类似的内部 Flux 框架),问题是您建议的方法将选择器耦合到整个状态树中关联的 reducer 状态的位置。这在某些情况下会导致问题:

您希望减速器位于状态树的多个位置(例如,因为相关组件出现在屏幕的多个部分,或者被应用程序的多个独立屏幕使用)。 您想在另一个应用程序中重用reducer,而这个应用程序的状态结构与您原来的应用程序不同。

它还为每个模块的选择器添加了对根 reducer 的隐式依赖(因为它们必须知道它们所在的键,这实际上是根 reducer 的责任)。

如果一个选择器需要来自多个不同 reducer 的状态,问题可能会被放大。理想情况下,模块应该只导出一个将状态切片转换为所需值的纯函数,并由应用程序的根模块文件来连接它。

一个很好的技巧是有一个只导出选择器的文件,所有选择器都采用状态切片。这样就可以批量处理:

// in file rootselectors.js
import * as todoSelectors from 'todos/selectors';
//...
// something like this:
export const todo = shiftSelectors(state => state.todos, todoSelectors); 

(shiftSelectors 有一个简单的实现——我怀疑重新选择库已经有一个合适的功能)。

这也为您提供了名称间距 - 待办事项选择器都在“待办事项”导出下可用。现在,如果您有两个待办事项列表,您可以轻松导出 todo1 和 todo2,甚至可以通过导出一个记忆函数来为特定索引或 id 创建它们来提供对动态列表的访问。 (例如,如果您可以一次显示任意一组待办事项列表)。例如

export const todo = memoize(id => shiftSelectors(state => state.todos[id], todoSelectors)); 
// but be careful if there are lot of ids!

有时选择器需要来自应用程序多个部分的状态。同样,避免在根部以外的地方接线。在您的模块中,您将拥有:

export function selectSomeState(todos, user) ...

然后您的根选择器文件可以导入它,并将连接“待办事项”和“用户”的版本重新导出到状态树的适当部分。

因此,对于一个小型的一次性应用程序,它可能不是很有用,只是添加了样板文件(尤其是在 javascript 中,它不是最简洁的函数式语言)。对于使用许多共享组件的大型应用程序套件,它将实现大量重用,并保持职责清晰。它还使模块级选择器更简单,因为它们不必先降到适当的级别。此外,如果您添加 FlowType 或 TypeScript,您可以避免所有子模块必须依赖于您的根状态类型的真正糟糕问题(基本上,我提到的隐式依赖变得显式)。

【讨论】:

我一直在寻找解决这个确切问题的方法,而这个答案启发了一个很好的解决方案。对于正在寻找 shiftSelectors 可能是什么样子的示例的任何人,请查看以下要点中的 bindSelectors:gist.github.com/jslatts/1c5d4d46b6e5b0ac0e917fa3b6f7968f

以上是关于Redux:将 Selector 与 Reducers 共存的主要内容,如果未能解决你的问题,请参考以下文章

再次对redux进行研究

为啥这个 Redux Saga Selector 不工作?我似乎无法访问状态

Redux For SwiftUI

redux初识

总结下Redux

总结下Redux