Advanced Reducers in Redux
Tags: redux, explainlikeimfive, statemanagement, tutorial
Published: 2019-08-08 08:50:53

*Nothing is mandatory, however, advised surely.*

I outline best practices, common usage patterns and app-scaling in state management architecture, powered by Redux.

With growing functionality, reducers would scale horizontally adding up to action types with linear rate. To acquire additional scalability, reducer is split up into smaller reducers, each concerned of specific domain (semantic) problem. Thus, I'd split my humongous reducer on:

1. Window reducer (that controls IoT window frames in a Smart House)
2. Thermostat reducer

Pattern here is extrapolation of responsibilities on domain-dedicated reducers - **combined reducers**. Redux gives us `combineReducer()` helper to produce `rootReducer`, being later initialized in a state. This function takes an object as input that has a state as property name and a reducer as value

    const rootReducer = combineReducers({
    	windowState: window

# Initial State

`const store = createStore(reducer, []);`

Empty list `[]` is the initial state adopted by Redux Store on initialization, afterwards the **Store** will run through each reducer once. 

Initializing action, that is received in the reducer, can also be accompanied by the initial state, following that leaving out initial state in the store initialization will turn it `undefined`.

`const store = createStore(reducer);`

Initial state can be specified with ES6 default parameter

    function reducer(state = [], action) {
    	...
    }

# Nested Data Structures

If initial state is an empty window list, empty list is a fine initial value, however, regard properties like: `auth_token`, `reason`, `toggle`, `errorStatus`, `currentTemperature` to appear instantaneously after first development iteration.

To define initial state beyond reducer's default parametrized one

    const initialStore = {
    	auth_token: null,
    	thermostat: {
    		currentTemperature: 0.0
    	},
    	windows: [
    	{id: 0, toggle: false},
    	{id: 1, toggle: false}
    	],
    };
    const store = createStore(reducer, initialState);

Two reducers operate on `windows` list, and on `thermostat` object

    function applyShutWindows(state, action) {
        return state.windows.map(window => action.windows.find(action.id)
            ? Object.assign({}, window, {toggle: false})
            : window);
    }
    function applySetTemperature(state, action) {
        return Object.assign({}, state.thermostat, { currentTemperature: action.temp });
    }

Nested data structures are reasonable, and are generally fine in Redux, but try to avoid deeply nested structures, since, as you may regard, complexity grows - readability falls.

Though a neat `normalizr` library available on npm, solves **deep nesting** concern
https://www.npmjs.com/package/normalizr

# Combined Reducer

Scaling state by using substates in Redux.

Probably you've read about multiple reducers from official [Documentation]([https://redux.js.org/basics/reducers#designing-the-state-shape](https://redux.js.org/basics/reducers#designing-the-state-shape))

Reducer `switch` can grow horizontally with addition of action types, but it's *linear scaling*. If to split reducers up on semantic (domain-related) categories as follows: thermostat, windows control - scaling is now feasible. This approach is guaranteed to scale well beyond *linear* growth.

Pattern here is extrapolation of responsibilities on domain-dedicated reducers - **combined reducers**. Redux gives us `combineReducer` helper to produce `rootReducer`, being later initialized in a state. This function takes an object as input that has a state as property name and a reducer as value

### Combine Reducers

Redux library provides a helper function `combineReducers` that takes object as input that describes **substate** as property name and **reducer** as value

    const rootReducer = combineReducers({
    	windowsState: windowsReducer,
    	thermostatState: thermostatReducer,
    });
    const store = createStore(rootReducer);

Individual reducers for windows and thermostat

    function windowsReducer(state = [
    		{id: 0, toggle: true}, 
    		{id: 1, toggle: false}
    		], action) {
    	switch(action.type) {
    		case SHUT_WINDOWS: {
    	            return applyShutWindows(state, action);
    	  }
    		default: return state;	
    }
    function thermostatReducer(state  = { currentTemperature: 0.0 }, action) {
    	switch(action.type) {
    		case SET_TEMP: {
                return applySetTemp(state, action);
        }
    		default: return state;
    }

`combineReducers` introduces an *intermediate state layer* (**substate**) for the global state object. The global state object, when using combined reducers, will look according to the object passed to `combineReducers`

    {
    	windowsState: ...,
    	thermostatState: ...,
    }

The property keys for the *intermediate layer* are those keys defined in the object passed to `combineReducers()`. Mind that default parameter in reducer no longer affects this global state, only it's own **substate,** furthermore, **substates** are isolated and have no idea about each other, nor of the global state. Individual reducers can now operate on root `state` variable since they are inside their own **substate**

    function applyShutWindows(state, action) {
        return state.map(window => action.windows.find(action.id)
            ? Object.assign({}, window, {toggle: false})
            : window);
    }
    function applySetTemperature(state, action) {
        const thermostatUpdate = Object.assign({}, state, { currentTemperature: action.temp });
        return Object.assign({}, state, thermostatUpdate);
    }

It seems like an unnecessary hassle, but it's critical to enable *non-linear* state scaling, substates empower **codebase** maintainability

# Clarification for Initial State

The initial state in `createStore` prevails reducer's default parameter

→ In case of **one plain reducer,** the initial state in the reducer only works when the incoming initial state is `undefined` because only then can it apply a default state. 

Note: if initial state is already defined in `createStore()` - reducer will not compete against it.

→ On the other hand, when using **combined reducers**: it's better to embrace default parametrized usage of the **substate** initialization. 

Note: initial state passed in `createStore()` doesn’t have to include all **substates** that are introduced by `combineReducers()` Therefore, when a
substate is `undefined`, the reducer can define it's own default substate. Otherwise the default object hierarchy from the `createStore()` is utilized..

# Nested Reducers

Scaling reducers horizontally, although with an additional vertial level in combined one, boils down to:

1. A reducer can care about different action types
2. A reducer can be split up into multiple reducers yet be combinet as one root reducer for the store initialization

Sometimes, in case of complex domain problem under trial, it's reasonable to introduce a new vertical layer of reducers. Take '[IoT window](https://en.wikipedia.org/wiki/Smart_glass) frames' example, and imagine those windows to also feature an electrically controlled tint layer.

    function windowsReducer(state = [
    		{id: 0, toggle: true, tint: { degree: 0.0 }}, 
    		{id: 1, toggle: false, tint: { degree: 0.0 }}
    		], action) {
    	switch(action.type) {
    		case SHUT_WINDOWS: {
    	            return applyShutWindows(state, action);
    	  }
    		case TINT_WINDOWS: {
    		//Vertical expansion
    		//tintReducer will tap into it's boundaries of substate.
    							return tintReducer(undefined, action);
    		}
    		default: return state;	
    }
    function tintReducer(state, action) {
    	switch(action.type) {
    		case TINT_WINDOWS: {
    			return applyTintWindows(state, action);
    		}
    	default : return state;
    	}
    }

# Conclusion

That's all about usage of plain Redux. It helps to manage a predictable singleton state object. The principle of Flux behind Reducing the Store is universal and hence may be deployed to any SPA (React, Vue, Angular). Surely, in simple React applications, local state may suffice the needs of the service, however, with releasing a cross-platform version, with bunch of API to be fetched and analytics to be triangulated - it's a safe bet to go with Redux to organize all the spaghetti into ranks and files.