65.9K
CodeProject 正在变化。 阅读更多。
Home

Redux Reducer 数组

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2017 年 3 月 31 日

MIT

5分钟阅读

viewsIcon

11731

一种简化的方法,可在具有现有 reducer 和 selector 模式的 React/Redux Web 应用程序中启用 reducer 数组。

引言

ReactRedux 为现代 Web 应用程序提供了强大而灵活的架构。它强制执行 单向数据流 来发起、管理和响应应用程序状态的更改,还提供了 工具实用程序 来构建大型复杂应用程序。Redux 的构造,如 reducerscombined reducers,允许将状态划分为逻辑分区进行管理,就像为不同状态设置不同的 Flux stores 一样。然而,没有标准的模式来处理同一 reducer 的多个实例。当需要打开多个域对象的实例时,我们需要一种实用且侵入性较小的方法。

例如,您已经拥有一个带有 combined reducers 的 Redux store,它可以处理单个 item (domain object),如何同时处理多个实例,例如条目或项的数组?标准的 splitting / combining reducers 在这里无能为力,因为状态会作为 props 数据通过变异传递给组件,而 reducers 本质上是处理 actions 来管理和变异状态的函数。

此外,由于当前的 combined reducersactions 和组件已经为单个实例工作并经过测试,因此我们在将它们放入集合时最好重用现有代码。

我们通过两个步骤解决了这个问题。第一步,将所有 React 组件与 store 的形状解耦,使它们仅在“组件 props”的逻辑边界内运行。第二步,在数组中管理状态的集合,并在更高级别的 reducer 中应用相同的 combined reducer。

解耦步骤有助于在重新架构 store 形状时保持组件的完整性,而集合化步骤则是将现有的 reducers 和 actions 应用于特定选定实例的状态,而无需进行重大更改。

这种两步方法的优点是更改的范围被包含在更高级别的 reducers 中,所有组件都对 store 的形状变得无关紧要。在演进一个相当复杂和大型的应用程序架构时,这两个优点对于灵活性和可维护性至关重要,最终会影响项目进度和质量。

背景

对多个实例的需求出现在我们项目的相当晚的阶段。该应用程序在没有同时打开/编辑多个域对象的概念的情况下构建。所有组件、actions、reducers 都已为单个域对象开发,并且对象状态由 combined reducer 管理。现在我们需要在一个单独的标签页中打开不同的域对象,以便用户可以轻松地在它们之间切换。

我们发现 Redux Selector 模式 是解耦步骤的一个很好的助手,它将关于 state 形状的知识封装在与 reducer 放在一起的 selectors 中,这样组件就不依赖于 state 结构,当重塑数据存储时,组件代码无需更改。

当当前选定的实例更改时,actions 和组件将自动切换到选定的状态,无需运行重复且不断变化的逻辑来获取要渲染的状态,因为 selectors 实际上消除了对 store 形状的依赖。

有了 selectors,我们可以简单地将单个实例的 combined reducer 从根状态中移除。当打开一个新的域对象后,创建了一个选项卡,combined reducer 及其初始状态将与该选项卡的状态相关联。当 action 被 dispatch 时,我们可以手动使用当前选定的选项卡状态来调用相同的 combined reducer。然后组件渲染,React 虚拟 DOM 使用选项卡数组中选定对象的数据更新 DOM。

让我们看一些代码。

使用 state selectors

假设 AEditor 是我们的域对象的组件,一个单独的编辑器组件在没有 selectors 的情况下代码如下:

// Domain object component without selector
// 
const AEditor = React.createClass({
    componentWillMount() {
        ...
    },
    componentWillReceiveProps(nextProps) {
        ...
    },
    render() {
        return ( <lowerlevelcomponent {...this.props.editorstate} />);
    }
});

export default connect(
	state => ( {editorState: state.editorContainer.editorState} ),
	dispatch => { (actions: bindActionCreators(actions, dispatch)} )
)(AEditor);
    

请注意,editorState 是直接从根状态中检索的,但组件只需要 editorState 来渲染。即使不需要多个 AEditor 实例,最好还是消除对 state 形状的依赖,这就是 selector 的作用。

这是使用 selectors 的相同组件代码:

// Domain object component with selector 
// 
const AEditor = ... ; //same as above 

export default connect( 
    state => ( {editorState: getEditorState(state) } ), 
    dispatch => { (actions: bindActionCreators(actions, dispatch)} ) 
)(AEditor);

getEditorState 就是 selector,它所做的就是接收根状态作为输入,并返回 editorState。只有 selector 知道 store 的形状,当当前选定的编辑器更改时,它会返回选定的编辑器状态。这就是当它有一个对象数组时,我们可以保持 AEditor 组件和相关 actions 不变的方法。

这是 getEditorState 在根 reducer 中的实现:

// selectors for single instance, editorContainer is a combined reducer.
export const getEditorContainer = (state) => state.editorContainer;
export const getEditorState = (state) => fromEditor.getEditor(getEditorContainer(state));

这里没什么特别的,这些 selector 函数的职责是“选择”正确的 state 到组件。对于多个状态实例,它们对于使当前 Actions 与选定的对象状态一起工作至关重要。

现在组件已经从 store 的形状中解耦了,我们可以继续创建编辑器状态的数组。

创建初始状态

editorContainer 实际上是一个 combined reducer:

export const editorContainer = combineReducers({
   editorState,
   propertiesState,
   outlineState,
   categoryState,
   dataPaneState
});

其中每个 xxxState 都是一个 reducer。例如,这是实现 editorStatereducer.editor.state.js 文件:

// sub-reducer that each has its own initialState
const initialState = {
   actId: '',
   actProps: undefined,
   actSteps: undefined,   
   deleted: false
};

export const editorState = (state = initialState, action = {}) => {
   switch (action.type) {
      case types.NAVIGATE_WITH_ACTION_ID: return onNavigateWithActionId(state, action);
      ...
      default: return state;
   }
};

在将 combined reducer editorContainer 应用于集合之前,我们需要一个初始状态的 selector:

// initial state selector for editorContainer
export const getInitialEditorContainerState = () => ({
   editorState: initialEditor,
   propertiesState: initialProperties,
   outlineState: initialOutline,
   categoryState: initialCategory,
   dataPaneState: initialDataPane
});

我们所需要做的就是从子 reducers 中导出每个 initialState,然后为初始状态 selector 导入它们。

// updated reducer.editor.state.js with exported initialState

export const initialState = { 
    actId: '', 
    ... // other initial props
};
...

// seletor.initialStaet.js:  import initialState from each reducer file:

import { initialState as initialEditor } from "../editor/reducer.editor.state";

export const getInitialEditorContainerState = () => ({
   editorState: initialEditor,
   ... // other imported initialStaet
});

一旦我们有了 combined reducer 的初始状态,其余的都很简单。

应用 combined reducer

我们的目标是使用户能够在自己的选项卡中打开/编辑多个 AEditor 实例。由于 tabsState 已经有一个 tab 数组,所以在创建新选项卡时,我们可以将 initialEditorContainerState 设置为该选项卡的 contentState,并将 editorContainer combined reducer 设置为它的 contentReducer

这是在 tabsState reducer 中创建 newTab 之后调用的代码:

// setting contentState and contentReducer for newly created tab 
function _setTabContent(newTab) {
   if (newTab.tabType === AppConst.AEDITOR) {
      if (!newTab.contentState || typeof(newTab.contentState) !== "object") {
         newTab.contentState = getInitialEditorContainerState();
      }
      if (!newTab.contentReducer || typeof(newTab.contentReducer) !== "function") {
         newTab.contentReducer = editorContainer;
      }
   }
}

接下来,我们需要在 tabsState reducer 中调用 contentReducer 来更新 contentState

// for all non-tab related actions, relay to contentReducer with contentState, then update the tab
function onContentActions(state, action) {
   let updatedState = state;

   let curTab = state.tabs[state.selectedIndex];
   if (curTab && curTab.contentReducer) {
      let updatedTabs = state.tabs.slice();
      updatedTabs.splice(state.selectedIndex, 1,  {...curTab, contentState: curTab.contentReducer(curTab.contentState, action)});
      updatedState = {...state, tabs: updatedTabs};
   }

   return updatedState;
}

export default function tabsState(state = _initialTabsState, action) {
   switch (action.type) {
      case AppConst.OPEN_OR_CREATE_TAB: return onOpenOrCreateTab(state, action);
      case AppConst.CLOSE_ONE_TAB: return onCloseOneTab(state, action);
      case AppConst.UPDATE_ONE_TAB: return onUpdateOneTab(state, action);
      default: return onContentActions(state, action);
   }
}

请注意,onContentActions 通过调用 curTab.contentReducer(curTab.contentState, action) 来更新当前选项卡,这是确保所有当前 Actions 和 Components 与当前选定选项卡内容一起工作是关键。

将选项卡状态与 contentStatecontentReducer 关联,为我们提供了当选项卡扩展到托管不同类型的组件时的灵活性。当您的用例不是在选项卡中时,相同的想法和技术仍然适用。

总结

尽管没有标准的模式来将 reducers 应用于集合,但 selector 模式以及使用数组中选定的状态调用单个实例的 reducer 是一种实用且高效的解决方案。

© . All rights reserved.