updux/README.md
2019-10-24 11:39:35 -04:00

8.5 KiB

What's Updux?

So, I'm a fan of Redux. Two days ago I discovered rematch alonside a few other frameworks built atop Redux.

It has a couple of pretty good ideas that removes some of the boilerplate. Keeping mutations and asynchronous effects close to the reducer definition, à la [VueX][]? Nice. Automatically infering the actions from the said mutations and effects? Genius!

But it also enforces a flat hierarchy of reducers -- where is the fun in that? And I'm also having a strong love for Updeep, so I want reducer state updates to leverage the heck out of it.

All that to say, I had some fun yesterday and hacked a proto-lovechild of Rematch and Updeep, with a dash of VueX inspiration. I call it... Updux.

Synopsis

import updux from 'updux';

import otherUpdux from './otherUpdux';

const {
    initial,
    reducer,
    actions,
    middleware,
    createStore,
} = updux({ 
    initial: {
        counter: 0,
    },
    subduxes: {
        otherUpdux,
    },
    mutations: {
        inc: ( increment = 1 ) => u({counter: s => s + increment })
    },
    effects: {
        '*' => api => next => action => {
            console.log( "hey, look, an action zoomed by!", action );
            next(action);
        };
    },
    actions: {
        customAction: ( someArg ) => ({ someProp: someArg }),
    },

});

const store = createStore();

store.dispatch.inc(3);

Description

Updux exports one function, updux, both as a named export and as its default export.

helpers = updux(config);

updux is a way to minimize and simplify the boilerplate associated with the creation of a Redux store. It takes a shorthand configuration object, and generates the appropriate reducer, actions, middleware, etc. In true Redux-like fashion, just like reducers can be composed of sub-reducers, upduxs can be made of sub-upduxs.

config

The config object recognize following properties.

initial

The default initial state of the reducer. Can be anything your heart desires.

subduxes

Object mapping slices of the state to sub-upduxs.

For example, if in plain Redux you would do

import { combineReducers } from 'redux';
import todosReducer from './todos';
import statisticsReducer from './statistics';

const rootReducer = combineReducers({
    todos: todosReducer,
    stats: statisticsReducer,
});

then with Updux you'd do

import { updux } from 'updux';
import todos from './todos';
import statistics from './statistics';

const rootUpdux = updux({
    subduxes: {
        todos, statistics
    }
});

mutations

Object mapping actions to the associated state mutation.

For example, in Redux you'd do

function todosReducer(state=[],action) {

    switch(action.type) {
        case 'ADD':  return [ ...state, action.payload ];

        case 'DONE': return state.map( todo => todo.id === action.payload 
            ? { ...todo, done: true } : todo )

        default: return state;
    }
}

With Updux:

const todosUpdux = updux({
    mutations: {
        add: todo => state => [ ...state, todo ],
        done: done_id => u.map( u.if( ({id} => id === done_id), {done: true} ) )
    }
});

The signature of the mutations is (payload,action) => state => newState.
It is designed to play well with Updeep. This way, instead of doing

    mutation: {
        renameTodo: newName => state => { ...state, name: newName }
    }

we can do

    mutation: {
        renameTodo: newName => u({ name: newName })
    }

Also, the special key * can be used to match any action not explicitly matched by other mutations.

const todosUpdux = updux({
    mutations: {
        add: todo => state => [ ...state, todo ],
        done: done_id => u.map( u.if( ({id} => id === done_id), {done: true} ) ),
        '*' (payload,action) => state => {
            console.warn( "unexpected action ", action.type );
            return state;
        },
    }
});

effects

Plain object defining asynchronous actions and side-effects triggered by actions. The effects themselves are Redux middleware, expect with the dispatch property of the first argument augmented with all the available actions.

updux({
    effects: {
        fetch: ({dispatch}) => next => async (action) => {
            next(action);

            let result = await fetch(action.payload.url).then( result => result.json() );
            dispatch.fetchSuccess(result);
        }
    }
});

actions

Generic action creations are automatically created from the mutations and effects, but you can also define custom action creator here. The object's values are function that transform the arguments of the creator to the action's payload.

const { actions } = updox({
    mutations: {
        foo: () => state => state,
    }
    actions: {
        bar: (x,y) => ({x,y})
    }
});

actions.foo({ x: 1, y: 2 }); // => { type: foo, payload: { x:1, y:2  } }
actions.bar(1,2);            // => { type: bar, payload: { x:1, y:2  } }

return value

updux returns an object with the following properties:

initial

Default initial state of the reducer. If applicable, merge the initial states of config and subduxes, with config having precedence over subduxes.

If nothing was given, defaults to an empty object.

reducer

A Redux reducer generated using the computed initial state and mutations.

mutations

Merge of the config and subduxes mutations. If an action trigger mutations in both the main updux and its subduxes, the subduxes mutations will be performed first.

actions

Action creators for all actions defined or used in the actions, mutations, effects and subduxes of the updox config.

Non-custom action creators defined in actions have the signature (payload={},meta={}) => ({type, payload,meta}) (with the extra sugar that if meta or payload are not specified, the key is not present in the produced action).

If the same action appears in multiple locations, the precedence order determining which one will prevail is

actions generated from mutations/effects < non-custom subduxes actions <
custom subduxes actions < custom actions

middleware

A middleware aggregating all the effects defined in the updox and its subduxes. Effects of the updox itself are done before the subdoxes effects.

createStore

Same as doing

import { createStore, applyMiddleware } from 'redux';

const { initial, reducer, middleware, actions } = updox(...);

const store = createStore( initial, reducer, applyMiddleware(middleware) );

for ( let type in actions ) {
    store.dispatch[type] = (...args) => {
        store.dispatch(actions[type](...args))
    };
}

So that later on you can do

store.dispatch.addTodo(...);

// still work
store.dispatch( actions.addTodo(...) );

Example

battle.js

import { updux } from 'updux';

import game from './game';
import log from './log';
import bogeys from './bogeys';

const { createStore } = updux({
    subduxes: { game, log, bogeys }
})

export default createStore;

game.js

import { updux } from 'updux';
import _ from 'lodash';
import u from 'updeep';

import { calculateMovement } from 'game/rules';

export default updux({
    initial: { game: "", players: [], turn: 0, },
    mutations: {
        init_game: ({game: { name, players }}) => {name, players},
        play_turn: () => u({ turn: x => x+1 }),
    },
    effects: {
        play_turn: ({getState,dispatch}) => next => action => {

            const bogeys = api.getState().bogeys;

            // only allow the turn to be played if
            // all ships have their orders in
            if( bogeys.any( bogey => ! bogey.orders ) ) return;

            bogeys.forEach( bogey => {
                dispatch.move( calculateMovement(bogey) )
            } );

            next(action); 
        },
    }
});

log.js

import { updux } from 'updux';

export default updux({
    initial: [],
    mutations: {
        '*': (payload,action) => state => [ ...state, action ],
    },
});

bogeys.js

import { updux } from 'updux';
import _ from 'lodash';

export default updux({
    initial: [],
    mutations: {
        init_game: ({bogeys}) => () => _.keyBy( bogeys, 'id' ),
        move: ({position}) => u({ position }),
    },
});

myGame.js

import Battle from './battle';

const battle = Battle();

battle.dispatch.init_game({
    name: 'Gemini Prime',
    players: [ 'yenzie' ],
    bogeys: [ { id: 'Enkidu' } ]
});

battle.dispatch.play_game();

....