updux/README.md
Yanick Champoux f5c5728ede update README
2019-10-20 09:52:39 -04:00

7.7 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);
        };
    },

});

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);
        }
    }
});

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 used in the mutations, effects and subdox of the updox config.

The action creators 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).

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();

....