Redux:将选择器与减速器并置
Redux: Colocating Selectors with Reducers
在此 Redux: Colocating Selectors with Reducers Egghead tutorial, Dan Abramov 中建议使用接受 完整 状态树而不是状态切片的选择器来封装组件之外的状态知识。他认为这使得更改状态结构变得更容易,因为组件不知道它,我完全同意。
但是,他建议的方法是,对于对应于特定状态切片的每个选择器,我们将其与根缩减器一起再次定义,以便它可以接受完整状态。当然,这种实现开销破坏了他试图实现的目标......简化了未来改变状态结构的过程。
在一个有很多 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}.`);
}
};
这样做有什么坏处吗?
我自己犯了这个错误(不是使用 Redux,而是使用类似的 in-house 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 有一个简单的实现 - 我怀疑 reselect 库已经有合适的功能)。
这也为您提供 name-spacing - 待办事项选择器在 'todo' 导出下全部可用。现在,如果您有两个待办事项列表,您可以轻松导出 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) {...}
然后你的根选择器文件可以导入它,re-export 将 'todos' 和 'user' 连接到状态树的适当部分的版本。
因此,对于小型的一次性应用程序,它可能不是很有用,只是添加了样板文件(特别是在 JavaScript 中,这不是最简洁的函数式语言)。对于使用许多共享组件的大型应用程序套件,它将实现大量重用,并保持职责清晰。它还使 module-level 选择器更简单,因为它们不必先下降到适当的级别。此外,如果您添加 FlowType 或 TypeScript,您可以避免所有 sub-modules 必须依赖于您的根状态类型的真正糟糕的问题(基本上,我提到的隐式依赖变得显式)。
在此 Redux: Colocating Selectors with Reducers Egghead tutorial, Dan Abramov 中建议使用接受 完整 状态树而不是状态切片的选择器来封装组件之外的状态知识。他认为这使得更改状态结构变得更容易,因为组件不知道它,我完全同意。
但是,他建议的方法是,对于对应于特定状态切片的每个选择器,我们将其与根缩减器一起再次定义,以便它可以接受完整状态。当然,这种实现开销破坏了他试图实现的目标......简化了未来改变状态结构的过程。
在一个有很多 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}.`);
}
};
这样做有什么坏处吗?
我自己犯了这个错误(不是使用 Redux,而是使用类似的 in-house 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 有一个简单的实现 - 我怀疑 reselect 库已经有合适的功能)。
这也为您提供 name-spacing - 待办事项选择器在 'todo' 导出下全部可用。现在,如果您有两个待办事项列表,您可以轻松导出 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) {...}
然后你的根选择器文件可以导入它,re-export 将 'todos' 和 'user' 连接到状态树的适当部分的版本。
因此,对于小型的一次性应用程序,它可能不是很有用,只是添加了样板文件(特别是在 JavaScript 中,这不是最简洁的函数式语言)。对于使用许多共享组件的大型应用程序套件,它将实现大量重用,并保持职责清晰。它还使 module-level 选择器更简单,因为它们不必先下降到适当的级别。此外,如果您添加 FlowType 或 TypeScript,您可以避免所有 sub-modules 必须依赖于您的根状态类型的真正糟糕的问题(基本上,我提到的隐式依赖变得显式)。