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