/canonical-reducer-composition

Spec for Canonical Reducer Composition design pattern.

Canonical Reducer Composition

Spec

Canonical Reducer Composition pattern requires that:

Reducer Definition

  • Reducer definition must register at least one domain.
  • Action name must correspond to the action name property value.
  • Action name must be unique in the entire reducer definition object.

Domain

  • Domain must own only sub-domains or action handlers.
  • Domain can own another domain.
  • Domain can own action handlers.
  • Domain can own CONSTRUCT action handler.

Action Handler

  • Action handler must be a function.
  • Action handler must not mutate its arguments.
  • Action handler must return domain state.

Action

  • Action must be a plain object.
  • Action must define name property.
    • Action name property value must be a string.
    • Action name property value must consist only of uppercase latin characters and one or more underscore characters (/^[A-Z\_]+$/).
  • Action can define data property. When defined,
    • data property value must be a plain object.
  • Action can define metadata property. When defined,
    • metadata property value must be a plain object.
  • Action can define error property. When defined,
    • It must be an object.
    • It can be an instance of Error.
    • It must have message property.
    • message property value must be a string.

Flux Standard Action

Flux Standard Action (FSA) is a competing standard. If you are implementing software using Canonical Reducer Composition and have dependencies that use FSA convention, you can use redux-convention middleware to convert between the two standards.

CONSTRUCT Action Handler

  • A domain can register CONSTRUCT action handler.
  • CONSTRUCT can be used to construct the initial domain state.

The application must send {name: 'CONSTRUCT'} action to initialise the domain state, e.g.

import {
    createStore
} from 'redux';

import {
     combineReducers
} from 'redux-immutable';

import * as reducers from './reducers';

import Immutable from 'immutable';

let reducer,
    state,
    store;

reducer = combineReducers(reducers);

state = Immutable.Map({});

// Invoking CONSTRUCT to build the initial state.
state = reducer(state, {
    name: 'CONSTRUCT'
});

store = createStore(reducer, state);

Schema

Reducer definition with a single domain:

{
    <domain>: {
        <action handler> (domain, action) {

        }
    }
}

In addition, domain can define a sub-domain:

{
    <domain>: {
        <domain>: {
            /**
             * Constructs the initial domain state.
             * 
             * @param {Object} domain
             * @return {Object}
             */
            CONSTRUCT (domain) {
                
            },
            /**
             * @typedef Action
             * @see {@link https://github.com/gajus/canonical-reducer-composition#action}
             * @property {String} name
             */
            
            /**
             * @param {Object} domain
             * @param {Action} action
             */
            <action handler> (domain, action) {

            },
            <action handler> (domain, action) {

            }
        },
        <domain>: {
            <action handler> (domain, action) {

            }
        }
    }
}

Benefits

Canonical Reducer Composition has the following benefits:

  • Introduces reducer declaration convention.
  • Domain reducer function is called only if it registers an action.
    • Enables logging of unhandled actions.
  • Enables intuitive nesting of the domain model.

Implementation Example

import {
    createStore,
} from 'redux';

import {
    combineReducers
} from 'redux-immutable';

import Immutable from 'immutable';

let reducer,
    state,
    store;

state = {
    // <domain>
    countries: [
        'IT',
        'JP',
        'DE'
    ],
    // <domain>
    cities: [],
    // <domain>
    user: {
        // <domain>
        names: [
            'Gajus',
            'Kuizinas'
        ]
    }
}

reducer = {
    countries: {
        ADD_COUNTRY: (domain, action) {
            return domain.push(action.country);
        },
        REMOVE_COUNTRY: (domain, action) {
            return domain.delete(domain.indexOf(action.country));
        }
    },
    cities: {
        // Using a constructor.
        CONSTRUCT () {
            return [
                'Rome',
                'Tokyo',
                'Berlin'
            ];
        },
        ADD_CITY (domain, action) {
            return domain.push(action.city);
        },
        REMOVE_CITY (domain, action) {
            return domain.delete(domain.indexOf(action.city));
        }
    },
    // Implement a sub-domain reducer map.
    user: {
        names: {
            ADD_NAME (domain, action) {
                return domain.push(action.name);
            },
            REMOVE_NAME (domain, action) {
                return domain.delete(domain.indexOf(action.name));
            }
        }
    }
};

reducer = combineReducers(reducer);

state = Immutable.fromJS(state);
// Invoking CONSTRUCT to build the initial state.
state = reducer(state, {
    name: 'CONSTRUCT'
});

store = createStore(reducer, state);

Redux Reducer Composition

Redux utilizes the concept of reducer composition.

let reducer = (state = {}, action) => ({
    // <domain>: <domainReducer> (<domain data>, <action>)
    countries: countryReducer(state.countries, action),
    cities: cityReducer(state.cities, action)
});

The benefit of this pattern is that domain reducers do not need to know the complete state; domain reducers receive only part of the state for their domain. This enables better code separation.

Redux combineReducers is a helper that turns an object whose values are different reducing functions into a single reducing function.

let reducer = combineReducers({
    countries: countryReducer,
    cities: cityReducer
});

However, Redux combineReducers does not dictate what should be the implementation of the domain reducer. Regardless of what is the implementation of the domain reducer, it does the same thing: listens to actions and when it recognizes an action, it create a new state, e.g.

export function countries (state = [], action) {
    switch (action.type) {
        case 'ADD_COUNTRY':
            // state =
            return state;

        case 'REMOVE_COUNTRY':
            // state =
            return state;

        default:
            return state;
    }
}

There are several problems with this:

  • This makes the code base less standardized (across different projects and different developers).
  • Domain reducer function is called regardless of whether it can handle the action.
  • The overhead of maintaining the boilerplate.

Validator Library

Libraries that implement Canonical Reducer Composition pattern validation:

Libraries

Libraries that implement Canonical Reducer Composition: