Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML5

Array of Redux Reducers

5.00/5 (4 votes)
31 Mar 2017MIT5 min read 11.3K  
A simplified approach to enable an array of reducers in a React/Redux web application with existing reducers and selector pattern.

Introduction

React and Redux offers a powerful and flexible architecture for modern web applications. It enforces unidirectional data flow to initiate, manage and react to application state mutations, also provides tools and utilities to build large scale complex applications. Redux constructs, like reducers and combined reducers, enables managing state in a logical divisions as if different Flux stores for different states. However, there is no standard pattern to handle multiple instances of the same reducer, when opening up multiple instances of domain objects, we need a practical and less intrusive solution.

For example, you've already got Redux store with combined reducers work with one item (domain object), how to handle multiple instances at the same time, like an array of entries or items? The standard splitting / combining reducers doesn't help here, because state will pass to components as props data with mutations, while reducers are essentially a function that handles actions to manage and mutate state.

Additionally, since current combined reducers, actions and components have been working and tested for single instance, we'd better to reuse existing code when putting them into a collection.

We've solved the problem with two steps. First, decoupling all react components from store shape, make them only operates within a logical boundary of "component props". Second, managing a collection of states in an array and apply the same combined reducer in a higher level reducer. 

The decoupling step helps to keep components intact while re-architecture the store shape, and the collection-ization step is to utilize existing reducers and actions to a specific selected instance's state without substantial changes.

The benefit of this two steps approach is the scope of changes are contained in higher level reducers, all components become agnostic to store shapes. When evolving a fairly complex and large application architecture, these two benefits are essential to flexibility and maintainability, ultimately project schedules and qualities.

Background

The requirement for multiple instances comes in a fairly late in our project. The application has been built without the notion of multiple domain objects opened / edited at the same time. All components, actions, reducers have been developed for single domain object, and object state is managed by a combined reducer. Now we need to open different domain objects in its own tab, so that user can easily switch between them.

We found Redux Selector Pattern is a great helper to the decoupling step, it encapsulates the knowledge about state shape in selectors that are collocated with reducer, so that component doesn't depend upon state structure, component code doesn't need to change when re-shaping the data store.

When current selected instance is changed, actions and components will automatically switch to selected state, no need to run repetitive and ever-changing logic to get the state for rendering, since the selectors essentially eliminate the dependencies on store shape.

With selectors in place, we can simply remove the single instance's combined reducer from root state. When a new domain object is opened, after a tab created, the combined reducer and its initial state will be associated with the tab state. And when action is dispatched, we can manually invoke the same combined reducer with current selected tab state. Then components render and React virtual DOM updates the DOM with the selected object data in the tab array.

Let's see some code.

Using state selectors

Assuming AEditor is the component for our domain object, a single editor component code looks like this without selectors:

JavaScript
// 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);

Notice the editorState is retrieved directly from root state, but the component only need editorState to render. Even without the need for multiple AEditor instances, it'd be better to eliminate the dependency on state shape, this is where selector comes to help.

Here is the same component code with selectors:

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

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

getEditorState is the selector, all it does is to take the root state as input, and returns the editorState. Only the selector knows the store shape, when current selected editor changes, it'll return the selected editor state back. This is how we can keep AEditor component and related actions as is when it has an array of objects.

Here is the implementation of getEditorState in root reducer:

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

Nothing really special here, these selector functions job is to "select" the correct state to the components. For multiple instances of states, they're critical to make current Actions work with selected object state.

Now that component is decoupled from store shape, we can move on to creating array of editor states.

Creating initial states

The editorContainer is actually a combined reducer:

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

where each xxxState is a reducer. For example, here is the reducer.editor.state.js file that implements editorState:

// 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;
   }
};

Before we can apply the combined reducer editorContainer to a collection, we need a selector of initial state:

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

All we need to do is to export each initialState from the sub reducers, then import them for the initial state selector:

JavaScript
// 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
});

Once we have the initial state for the combined reducer, all the rest is pretty straight forward.

Applying combined reducer

Our goal is to enable user opening / editing multiple AEditor instances in its own tab. Since tabsState already has an array of tab, when creating a new tab, we can set the initialEditorContainerState as the tab's contentState, also set the editorContainer combined reducer as its contentReducer

Here is the code invoked after newTab is created in tabsState reducer: 

JavaScript
// 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;
      }
   }
}

Next, we need to invoke contentReducer to update contentState in tabsState reducer: 

// 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);
   }
}

Notice onContentActions update the current tab by calling curTab.contentReducer(curTab.contentState, action), this is the key to make sure all current Actions and Components work with current selected tab content.

Associating tab state with contentState and contentReducer gives us flexibility when tabs extended to host different type of components. When your use case is not in tabs, same idea and technique still applies.

Wrap up

Although no standard pattern to apply reducers to a collection, the selector pattern and invoking single instance's reducer with selected state from an array is a practical and efficient solution. 

License

This article, along with any associated source code and files, is licensed under The MIT License