Reduce and Conquer

« Return to Our Notebook

Reduce and Conquer

Bulbophyllum falcatum
reducin' all the way

In an ecosystem riddled with large, portentous frameworks, Redux is a refreshingly ascetic little store management system. Driven more by its functional programming-inspired tenets than supporting code, it offers — and needs — only a few helper functions to manage its stores.

Minimalism is good. It's also a good idea to abstract oft-used patterns into more expressive forms. Ideally, code should be crafted such that its intent comes out on first read, while making deeper digs possible when required.

Happily enough, the judicious use of delightfully succinct higher-order functions is often all that's required to tailor-suit some ergonomics into the manipulation of middleware and reducers. This blog entry will showcase some of those helper functions that work for me.

This article assumes you're already familiar with Redux. If this isn't the case, you might want to check out first one of my previous articles, which provides a gentler, if a tad unconventional, introduction to the framework.

Action-specific middleware

This is a great example of a pervasive pattern that is already so easy to deal with that we don't always think about abstracting it away. You have a middleware, but you only want it to react to certain actions. Assuming you're using just plain Redux middleware and not sagas, you're probably doing something like:

const mwWatchingForFoo = store => next => action => {

    if ( action.type !== 'FOO' ) return next(action);

    // do  whatever needs to be done for FOO actions
};

For sure, it is boilerplate. But it's one itsy-bitsy line of boilerplate, so it's not so much of a big deal, right? Right. But still, we can make things better.

import { includes } from 'lodash/fp'; 

const mwFor = (...target) => inner => store => next => action => ( 
    includes(action.type)(target) ? next : inner(store)(next)
)(action);

That's a mouthful, but it boils down to "if the action interests us, calls the inner function. If not, just skip ahead to the next middleware". And now we have a generic middleware handler that allows us to bring the filtering at the front of the declaration (and supports both single types and arrays of them as a welcome bonus):

const mwWatchingForFoo = mwFor( 'FOO' )( 
    store => next => action => {
        // do whatever needs to be done for FOO actions
    }
) ;

const mwWatchingMore = mwFor( 'BAR', 'BAZ' )( 
    store => next => action => {
        // do whatever needs to be done for BAR and BAZ actions
    }
);

Decorators could even be used to tag the middleware with the actions they target, although I'm not sure the syntax ends up being any clearer.

import { includes } from 'lodash/fp'; 

const mwFor = (...targetTypes) => ( target, key, descriptor ) => {
    const original = descriptor.value;
    descriptor.value = store => next => action => 
        ( targetTypes.indexOf(action.type) === -1 
            ? next : original(store)(next)
        )(action);
};

// decorators only work within classes
class MyMiddleware {

    @mwFor('FOO')
    static watchingForFoo(store){ return next => action => { 
        // do  whatever needs to be done for FOO actions
    } }
}

const mwWatchingForFoo = MyMiddleware.watchingForFoo;

State selectors

For selectors returning pertinent bits of the store's state, it really pays off to study the many functions that lodash (and its functional programming variant lodash/fp) has to offer. They help slice what would be complexish filters into smaller components.

For example, I have a pet project where the Redux store represents the state of a spaceship game. To select all ships without orders belonging to active players, I could do:

const play_turn = mwFor( 'PLAY_TURN' )( 
    { getState } => next => action => {
        const state = getState();

        let active_players = state.game.players
            .filter( p => p.active )
            .map( p => p.name );

        let waiting_ships = state.ships
            .filter( s => !s.orders || !s.orders.done )
            .filter( s => 
                active_players.indexOf( s.player_id ) > -1 );

        if( waiting_ships.length > 0 ) {
            // do the thing
        }
    } 
);

It's not horrible, but there's a lot going on, and what could be clean chains are dirtied a little bit by checks that must be done to ensure all data structures are present. All of that could be rewritten as:

import _  from 'lodash';
import fp from 'lodash/fp';

const gamePlayers   = state => _.get( state, 'game.players', [] );
const activePlayers = fp.filter('active')
const allShips      = state => _.get( state, 'ships', [] );
const withoutOrder  = fp.negate( fp.get('orders') );
const controlled    = players => fp.includes(players);

const play_turn = mwFor( 'PLAY_TURN' )( 
    { getState } => next => action => {
        const state = getState();

        let active = fp.pipe([
            gamePlayers,
            activePlayers,
            fp.map('name')
        ])(state);

        let waitingShips = fp.pipe([
            allShips,
            withoutOrder,
            controlled(active),
        ])(state);

        if( _.isEmpty(waitingShips) ) {
            // do the thing
        }
    }
);

This way, the mechanics are removed from the middleware proper, which makes it easier to read, and it provides us with a slew of small functions that we can use and test in isolation.

And for people who love to live on the bleeding edge, there is also a :: binding operator that is a stage-0 ECMAScript proposal. With a little Babel help, one would use it to give the chaining a different look.

import _  from 'lodash';
import fp from 'lodash/fp';

const gamePlayers   = () => _.get( this, 'game.players', [] );
const activePlayers = () => _.filter(this,'active')
const allShips      = () => _.get( this, 'ships', [] );
const withoutOrder  = () => fp.negate( _.get(this, 'orders') );
const controlled    = players => _.includes(this, players);

const play_turn = mwFor( 'PLAY_TURN' )(
    { getState } => next => action => {
        const state = getState();

        let active = 
            state::gamePlayers()::activePlayers().map( p => p.name );

        let waitingShips =
            state::allShips()::withoutOrder()::controlled(players);

        if( _.isEmpty(waitingShips) ) {
            // do the thing
        }
    }
);

Reducers

Per-action reducer

The basic way to create a reducer in Redux is to have a function that is a giant switch:

const shipReducer = function(state={},action) {
    switch( action.type ) {
        case 'MOVE': 
            return { ...state, navigation: action.navigation };

        case 'DAMAGE':
            return { ...state, hull: state.hull - action.damage };

        default: return state;
    }
}

In the spirit of our 'reduce and conquer' approach, let's break that switch and compose a reducer out of many per-action functions:

function actionsReducer( redactions, initial_state = {} ) {
    return function( state = initial_state, action ) {
        let red = redactions[action.type] || redactions['*'];
        return red ? red(state,action) : state;
    }
}

let redactions = {};

redactions.MOVE = (state,{navigation}) => { 
    ...state, 
    navigation 
};

redactions.DAMAGE = (state,{damage}) => {
    ...state, 
    hull: state.hull - damage
};

const shipReducer = actionsReducer(redactions);

Much shorter, isn't? We can even do one better. We can change the signature of the functions to be curriable:

function actionsReducer( redactions, initial_state = {} ) {
    return function( state = initial_state, action ) {
        let red = redactions[action.type] || redactions['*'];
        return red ? red(action)(state) : state;
    }
}

let redactions = {};

redactions.MOVE = ({navigation}) => state => { 
    ...state, 
    navigation 
};

redactions.DAMAGE = ({damage}) => state => { 
    ...state, 
    hull: state.hull - damage
};

const shipReducer = actionsReducer(redactions);

Okay, that doesn't look much different. But wait! If we slip in the use of updeep in there...

import u  from 'updeep';
import fp from 'lodash/fp';

function actionsReducer( redactions, initial_state = {} ) {
    return function( state = initial_state, action ) {
        let red = redactions[action.type] || redactions['*'];
        return red ? red(action)(state) : state;
    }
}

let redactions = {};

redactions.MOVE = ({navigation}) => u({navigation});

redactions.DAMAGE = ({damage}) => u({ hull: fp.add(-damage) });

const shipReducer = actionsReducer(redactions);

Asserting the existence of actions

Since action types are often simple strings, it's not unusual that peeps want to double-check that no typo slipped anywhere.

As one might expect, that can be done many ways. Assuming we have an object like in the previous section, we can do:

import _ from 'lodash';
import { actions as officialActions } from './actions';

let actions = new Set( _.keys( redactions ) );
[ ...officialActions, '*' ].forEach( actions.delete );

for ( unknownAction of actions.values() ) {
    console.error( 'unknown action used: ', unknownAction );
}

We could also be proactive and check that the types are legit as they are added to the object:

const assertAction = {
    set( object, prop ) {
        if( prop !== '*' && ! officialActions[prop] )
            throw new Error( `${prop} is not a known action` );

        return Reflect.set(...arguments);
    }
};

let redaction = new Proxy({}, assertAction );

redaction.MOVE = ...;  // totally fine

redaction.BAD = ...;   // insta-boom

Chaining reducers

If a reducer gets too big, its actions could be split by topic. In which case we might want to have the main state groomed by all sub-reducers, which is pretty much a clear case of, well, reducing:

const chainReducers = reducers => reducers.reduceRight(
    (accum,reducer) => (state,action) => accum(
        reducer(state,action), action 
    )
);

let shipReducer = chainReducers( 
    movementReducer, 
    damageReducer
);

Reducto absurdum

This one is as trivial as they come. But if one is to use the chainReducers shown in the previous section, it might be good to have a reducer explicitly devoted to assign a default state if there is none:

import u from 'updeep';
import fp from 'lodash/fp';

const initReducer = default => u.if( fp.isNil, default );

let shipReducer = chainReducers( 
    movementReducer, 
    damageReducer, 
    initReducer({ coords: [0,0] })
);

Mapping reducer

When the main state of the store is an array, we typically have to resort to some mapping. For example, the reducer for all ships in my pet project could look like:

import u from 'updeep';
import _ from 'lodash';

redactions.UPDATE_STATUS = () => u.map( ship => ({ 
    destroyed: ship.hull <=0
}));

redactions.MOVE = ({ ship_id, coords }) => u.map( 
    u.if( .matches({id: ship_id}), { coords } 
));

As you probably expect by now, we can extract the mapping into its own little function:

const mappingReducer = reducer => condOrAction => 
    _.isFunction(condOrAction ) 
        ? action => u.map( 
            u.if( condOrAction(action), reducer(action) ) 
        ) : u.map( reducer(condOrAction) );

const shipReducer = actionsReducer({
    UPDATE_STATUS: () => u( ship => ({
        destroyed: ship.hull <=0 
    })),
    MOVE: ({coords}) => u({coords}),
});

const mappedShipReducer = mappingReducer(shipReducer);

const singledShip = ({ ship_id }) => _.matches({id: ship_id});

const shipsReducer = actionsReducer({
    UPDATE_STATUS: mappedShipReducer,
    MOVE: mappedShipReducer(singledShip),
});

The function here is written so that it works well with my actionsReducer, but making it play nice with basic reducers is only a question of rejuggling arguments.

A more flexible combineReducers

The combineReducers that comes with Redux only considers the direct keys of the provided object, and expect them all to be reducers themselves. But it's possible that, after acquiring a taste for the flexibility of updeep regarding merges, we want to change that:

import u  from 'updeep';
import _  from 'lodash';
import fp from 'lodash/fp';

function combineReducers( reducers ) {
    // first let's get the recursivity out of the way
    reducers = u.map(
        u.if( _.isObjectLike, combineReducers )
    )(reducers);

    return action => u(
        u.map( reducer => reducer(action) )(reducers)
    );
}

const shipReducer = actionsReducer({
    DAMAGE: ({damage}) => combineReducers({
        hit_taken: fp.add(1),
        structure: { hull: { integrity: integrityReducer } }
    })
});

Have your curry and eat it too

In the previous sections, the signatures of my reducers have alterned between the classic state and action duo, and an inverted, curried version that plays better with updeep:

import u from 'updeep';

// signature is: (state,action) => new_state
const old_skool_reducer = function(state,action) {
    return u({ counter: c => c + action.increment })(state);
}

// signature is: action => state => new_state
const curry_friendly_reducer = action => 
    u({ counter: c => c + action.increment });

I'd be remiss if I was not to point our that, thanks to some lodash magic, it's possible to merge those two variations into a single function that will do the right thing based on how your invoke it.

import u from 'updeep';
import _ from 'lodash';

// signature is: (state,action) => new_state
const all_in_one_reducer = _.curryRight( function(state,action) {
    return u({ counter: c => c + action.increment })(state);
});

// 2 arguments? Works old skool
let new_state = all_in_one_reducer( previous_state, action );

// otherwise, assume you are hungry for some currying
let new_state = all_in_one_reducer(action)(previous_state);

In conclusion

It is my hope that this blog entry illustrated how easy it is to enhance the basic mechanisms that come with Redux, and how the interface doesn't bind to a specific style, but rather give plenty of room to define the set of helper functions that make sense to the project at hand. I also hope that it provided compelling examples on how that kind of helper functions can abstract implementation details out of the higher strata of code, and make code more readable by moving the focus away from the how and unto the what.

We solve problems with technology. What can we solve for you?

Reach Out

t: 800.646.0188