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

387 lines
8.5 KiB
Markdown

# What's Updux?
So, I'm a fan of [Redux](https://redux.js.org). Two days ago I discovered
[rematch](https://rematch.github.io/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](https://github.com/substantial/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](https://vuex.vuejs.org/) 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();
....
```