Go to file
Yanick Champoux c83443c95f add dist directory 2019-10-22 18:15:40 -04:00
dist add dist directory 2019-10-22 18:15:40 -04:00
src add actions 2019-10-22 18:11:14 -04:00
.gitignore housecleaning 2019-10-22 10:26:04 -04:00
Changes add actions 2019-10-22 18:11:14 -04:00
README.md add actions 2019-10-22 18:11:14 -04:00
babel.config.js remove the pipeline operator 2019-10-21 11:40:08 -04:00
package.json add actions 2019-10-22 18:11:14 -04:00
tsconfig.json tyding up 2019-10-21 23:13:40 -04:00
typedoc.json all tests pass as ts 2019-10-21 14:16:47 -04:00

README.md

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


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

....



[Redux]: https://redux.js.org
[Updeep]: https://github.com/substantial/updeep
[VueX]: https://vuex.vuejs.org/