Actioner (Another Engine)

« Return to Our Notebook

Actioner (Another Engine)

I'm still playing around with Redux and, as usual, I'm always on the lookout for ways to optimize my laziness.

One thing that I found irks me just a little bit are the Redux actions. They are nothing but raw Javascript objects, meaning they are very easy to set up and manipulate. But since anything goes, they are also very easy to subtly get wrong. For example, I'm working on a spaceship game and I have an action called MOVE_SHIP. But what arguments was I using for that? Was it this:

{ type: 'MOVE_SHIP', ship: 'enkidu' }

or rather, that:

{ type: 'MOVE_SHIP', ship_id: 'enkidu' }

Sometimes, I remember to double check myself, but other times, I'll use the wrong property and set myself up for a long, protracted, somewhat less-than-joyful debugging session.

Ready? Set... (some expectations)

There is also the creation of the actions themselves. While it's not hard to do:

let action = { type: 'MOVE_SHIP', ship_id: the_ship };

it is more verbose than it needs to be. When we move a ship, we'll always need to have the ship id — or if we're fancy, we could also take a ship object and then extract the id from that. In a better world, I'd love to just do:

let action = action_move_ship( the_ship );
// action === { type: 'MOVE_SHIP', ship_id: the_ship }

(By the by, Redux-Actions already provides a mechanism to do just that. All in all, It's pretty close to what I want, but doesn't go far enough down the DWIM rabbit hole for my taste, nor does it allow for some more esoteric functionality that will pop up in a few paragraphs.)

Finally, there is the declaration of the action types, which is usually distinct from the creation of the actions themselves. The types are either used as ad-hoc strings, or as defined constants. Often like:

const MOVE_SHIP = 'MOVE_SHIP';
const ADD_SHIP = 'ADD_SHIP';

// later on...

function reducer( state, action ) {
    switch( action.type ) {
        case MOVE_SHIP: ...;
        case ADD_SHIP:  ...;
    }
}

It's not bad, but the neat freak in me wishes that those constants were gathered together, and somewhat tied to the action generators. Because, honestly, I don't want to type MOVE_SHIP more than once. (Yes, I am that lazy.)

So, to recap, I'd like to:

  • Have a unified, centralized way to declare both the action types and their generators.

  • Be able to customize the creators however I want (while having a useful default).

  • Having some type of validation on the payload of those actions would also be fantastic.

ACTIONS!

To scratch those itches, I'd like to introduce you to Actioner.

First, the ultra-basic way to use it with vanilla actions:

import Actioner from 'actioner';

let Actions = new Actioner();

Actions._add( 'move_ship' );
Actions._add( 'add_ship' );

console.log( Actions.MOVE_SHIP ); // prints 'MOVE_SHIP'

let action = Actions.add_ship( { id: 'enkidu', hull: 9 } );
// action === { type: 'MOVE_SHIP', id: 'enkidu', hull: 9 }

So, basically, _add()ing an action creates both the type name and the action creator, both accessible as keys from the main Actioner object. Simple enough. Oh, and don't worry, you can also define your action as the more Javascript-ish moveShip, and the type will still be expanded as MOVE_SHIP.

Next step: _add() also accepts a custom function that modifies input parameters in the resulting action object. We could, for example, rewrite MOVE_SHIP as:

// 'type' property is automatically added, natch
Actions._add( 'move_ship', ship => ({
    ( typeof ship === 'object' ) ? ship.id : ship
}) );

let action = Actions.move_ship( 'enkidu' );
// action === { type: 'MOVE_SHIP', ship_id: 'enkidu' }

let ship = { id: 'siduri', hull: 9 };
let action = Actions.move_ship( ship );
// action === { type: 'MOVE_SHIP', ship_id: 'siduri' }

What if you want that custom creator for most cases but still want the possibility to pass a straight-up object? Never fear, in addition to move_ship, the sibling function $move_ship, which always accepts raw objects, is also created:

// equivalent to the call to 'move_ship' above
let action = Actions.$move_ship( { ship_id: 'siduri' } );

And final feature: validation. For that, I went with my very own json-schema-shorthand library for json-schema.

new Actions = new Actioner({ schema_id: 'http://example.com/actions' });

Actions._add( 'add_ship', {
    ship_id: { type: 'string', required: true },
    hull:    { type: 'number', required: true },
    additional_properties: false,
});

Actions._validate(true);

Actions.add_ship({ ship_id: 'enkidu' }); // will throw

let schema = Actions._schema();
// schema === {
//     id: 'http://localhost/actions',
//     oneOf: [ { '$ref': '#/definitions/add_ship' } ]
//     definitions: {
//         add_ship: {
//             type: 'object',
//             properties: {
//                 type:    { enum: [ 'ADD_SHIP' ] },
//                 ship_id: { type: 'string' },
//                 hull:    { type: 'number' },
//             },
//             additional_properties: false,
//             required: [ 'ship_id', 'hull' ],
//         }
//     }

Did I say "final feature"? Well, that was the end of the hard requirements, but there are still a few goodies embedded in there. Like the security that comes with immutable objects? You'll love that you can have your actions be set as immutable via seamless-immutable, just by passing the immutable option to the Actioner constructor:

new Actions = new Actioner({ immutable: true });

Actions._add( 'FOO' );

let action = Actions.foo({ bar: 1 }); // action is immutable

And then there is the possibility to connect the object to a Redux store, and to be able to create and dispatch actions in one fell swoop.

Actions._store = aReduxStore;

Actions.dispatch_foo();  // equivalent to aReduxStore.dispatch( Actions.foo()  )
Actions.dispatch_$foo(); // equivalent to aReduxStore.dispatch( Actions.$foo() )

dispatch( 'FINAL_WORDS' )

So, all in all, this little helper library is nothing out of this world. But having those declarations centralized, and most importantly preemptively validated, is something that does wonders to keep me honest, or at least consistent, as I hack away. Even better, by using the JSON schema validation, I also plant the seeds of documentation, something that future-me will doubtlessly thanks present-me for.

As always, if you are intrigued, the code is on GitHub and available via npm.

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

Reach Out

t: 800.646.0188