updux/README.md

359 lines
7.4 KiB
Markdown
Raw Normal View History

2019-10-17 15:15:10 +00:00
# What's Updux?
So, I'm a fan of [Redux][]. Two days ago I discovered
[rematch](https://rematch.github.io/rematch) alonside a few other frameworks built atop Redux.
2019-10-17 15:15:10 +00:00
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`.
2019-10-19 17:11:30 +00:00
# Synopsis
```
import updux from 'updux';
import otherUpdux from './otherUpdux';
const {
initial,
reducer,
actions,
middleware,
createStore,
} = updux({
initial: {},
subduxes: {
otherUpdux,
},
mutations: {
},
effects: {
},
})
```
# 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
2019-10-17 15:15:10 +00:00
#### battle.js
```
import { updux } from 'updux';
import game from './game';
import log from './log';
import bogeys from './bogeys';
const { createStore } = updux({
reducers: { 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: [],
actions: {
'*': (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
[rematch]: https://rematch.github.io
[Updeep]: https://github.com/substantial/updeep
[VueX]: https://vuex.vuejs.org/