actionsreducer v0.5.1
ActionsReducer
Helpers to create actions and reducers for Redux.
Actionsreducer simplifies:
- The creation of actions, state, reducers; you create all three in one fell swoop.
- The creation of async actions (must use redux-thunk)
- Return values: - values are automatically re-assigned to state at the right location, no need for reducers composition. - Returning null, or an unchanged value, cancels an update. - Reducers can be infinitely nested (more than one level of nesting is discouraged, but possible)
Furthermore, the ActionsReducer project itself demonstrates:
- typescript + babel + webpack usage with browser and node bundles
- karma + mocha + chai testing
Usage
npm install --save actionsreduceralso, in case you don't have, already:
npm install --save redux react-redux redux-thunkthen
// data.js
import actionsreducer from 'actionreducer';
const [reducer,state,actions] = actionsreducer({
timer:{
state:{
dates:[]
},
actions:{
now:(state)=>[...state,Date.now()]
}
}
, counter:{
state:{
value:0
},
actions:{
inc:({value})=>({value:value++}),
dec:({value})=>({value:value--}),
reset:()=>({value:0})
}
}
});
console.log(reducer) //> function reducer(state,action){...}
console.log(state) //> {timer:[],counter:{value:1}}
console.log(actions) //> {timerNow:function,counterInc:function,counterDec:function,counterReset:function}This will do the following:
- create a
reducerthat can be used by Redux. This reducer is fully compatible withcombineReducersand other niceties - create an initial state that combines all the sub-states
- create action creators that returns Flux Standard Actions. The actions
will be, in this case, called
timerNow, counterInc,counterDecandcounterReset, and will dispatch the respectiveTIMER_NOW,COUNTER_INC,COUNTER_DEC, andCOUNTER_RESET` actions.
Example
There are several examples included with Actionsreducer.
To run them:
- clone the repo
git clone https://github.com/Xananax/actionsreducer.git && cd actionsreducer - install needed dev modules:
npm install - install typescript typings:
npm run typings - run the example server:
npm start
Too lazy? Here's an example for your reading pleasure:
// data.js
import actionsreducer from 'actionreducer';
let ids = 0;
const getId = ()=>ids++;
const [reducer,state,actions] = actionsreducer({
visibility:{
state:'ALL'
actions:{
filter:(state,filter)=>filter
}
}
, todos:{
state:[]
actions:{
add:(todos,text)=>(
[...todos,{ text, id:getId(), completed:false}]
)
, remove(todos,id)=>todos.filter(
todo=>todo.id!=id
)
, complete(todos,id)=>todos.map(
todo=>todo.id==id ?
Object.assign(todo,{completed:!todo.completed}) :
todo
)
}
}
});
export {reducer,state,actions};
// later, in 'configureStore.js':
import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import {reducer,state} from './data'
const store = createStore(
reducer,
state,
applyMiddleware(thunkMiddleware)
);
// in your app, 'App.jsx':
import * as React from 'react'
import { connect } from 'react-redux'
import {actions} from './data'
const {
todosAdd,
todosRemove,
todosComplete,
visibilityFilter
} = actions;
const Todo = ({text,id,completed,remove,complete}) =>(
<div id={`todo_${id}`}>
<input type='checkbox' checked={completed} onClick={()=>complete(id)}/>
<span>{text}</span>
<button onClick={()=>remove(id)}>delete</button>
</div>
)
const Link = ({filter,visibility,text,onClick}) =>(
(visibility == filter) ?
(<span>{text}</span>) :
(<a href="#" onClick={e=>{e.preventDefault();onClick(filter)}}>{text}</a>)
)
const Todos = ({visibility,todos,add,remove,complete,show}) => {
let input;
function onSubmit(e){
e.preventDefault();
const value = input.value.trim();
if (!value) {return;}
add(value);
input.value = ''
}
return (<ul>
<form onSubmit={onSubmit}>
<input type='text' ref={node=>{input = node}}/>
<button type="submit">add</button>
</form>
<label>
<Link text='all' filter={'ALL'} onClick={show} visibility={visibility}/>
<Link text='all' filter={'COMPLETE'} onClick={show} visibility={visibility}/>
<Link text='all' filter={'UNCOMPLETE'} onClick={show} visibility={visibility}/>
</label>
{ todos.map(todo=>(
<Todo ...todo remove={remove} complete={complete}/>
))}
</ul>);
}
function mapStateToProps({visibility,todos},ownProps){
return {
visibility,
todos:(
(visibility == 'COMPLETE') ? todos.filter(todo=>todo.completed) :
(visibility == 'UNCOMPLETE') ? todos.filter(todo=>!todo.completed) :
todos
)
}
}
function mapDispatchToProps(dispatch){
return {
add:(text)=>todosAdd(text)
, remove(id)=>todosRemove(id)
, complete(id)=>todosComplete(id)
, show(filter)=>visibilityFilter(filter)
};
}
export default connect(mapeStateToProps,mapDispatchToProps)(Todos);....And voilà! The full redux example.
Admittedly, I've cheated a bit because the todos store is very brittle.
But here's another options: actionsreducer comes with an easy to use store creator.
You use is by just calling simpleStore(factory,makeConfig).
factoryis a function that is used when adding an object.makeConfigis called once and returns a structure similar to the above.
Both are optional.
Here's an example:
import actionsreducer,{ simpleStore, assign } from 'actionsreducer';
const [reducer,state,actions] = actionsreducer({
visibility:{
state:'ALL'
actions:{
filter:(state,filter)=>filter
}
}
, todos:simpleStore(
(id,text)=>{
return {
id,
text,
completed:false
}
},
({state,add,addMany,remove,update,toggle,get},edit)=>(
{
state:addMany(state, [ { text: 'My first todo!' }]),
actions:{
add:(state,text)=>add(state,{text}), // `add` expects an object of functions
remove,
complete:(state,id)=>toggle(state,{id,prop:'completed'})
}
}
)
)
});Async actions are possible too:
const [reducer,state,actions] = actionsreducer({
//...
notes:{
state:{
notes:[]
, status:'nothing'
}
actions:{
add:{
_(state,payload,meta,actions,dispatch,type){
if(payload == 'error'){
return actions.error('error triggered by you!');
}
return new Promise((resolve,reject)=>{
setTimeout(()=>{resolve('a new note')},500);
})
}
, started(state,payload){
return {status:'loading'}
}
, success({notes},text){
return {
notes:notes.concat([{text}])
}
}
, error(state,payload){
return {status:'error'}
}
}
}
}
//...
})API
There's only one important function:
actionsreducer(config)=>[reducer,state,actions];Config
is an object of stateChunks
StateChunk
Signature:
{
state:any
, actions:{
[name:string]:ActionProcessor | AsyncActionProcessor | StateChunk
}
}StateChunks can be nested; If a StateChunk is nested in another, then it will receive only the relevant part of state.
in other words, this:
{
// ...
store{
state:{} error?:ActionProcessor;
started?:ActionProcessor;
cancelled?:ActionProcessor;
actions:{
subState:{
state:[]
, actions:{
doSomething(){}
}
}
}
}
// ...
}will resolve to a state {store:{subState:[]}} and to the action creator actions.storeSubStateDoSomething() which will dispatch the action STORE_SUBSTATE_DOSOMETHING.
ActionProcessor
someAction(state:any,payload?:any,meta?:any,type?:string)=>stateThis will transform into:
- an `ActionCreator` called `someAction` which will dispatch an action of type `'SOMEACTION'`
- a reducer `SOMEACTION` that will be called upon dispatching the action Note that an action processor does not need to return the whole state it is passed. It only needs to return the part that it is concerned with.
Anything returned will extend the current state. A new state will be created if necessary (if the returned value is different from the previous one). Note, however, that this operation is not recursive.
Returning null, false, or the CANCEL symbol (available as an export, import {CANCEL} from 'actionsreducer' will short-circuit the operation. There is no benefit in returning CANCEL, only more code clarity.
AsyncActionProcessor
{
_ : ( state:any, payload:any, meta:any, actions, dispatch, type:string )=>any;
, success:ActionProcessor
, error?:ActionProcessor
, started?:ActionProcessor
, cancelled?:ActionProcessor
}The function '_' is your async function. It's expected to return a Promise, but if you don't, what you return will be promisified.
Returning an Error, or rejecting a Promise will trigger the error ActionProcessor. returning null, false, or the CANCEL constant (available as an export import {CANCEL} from 'actionsreducer') will trigger the cancel action.started will be called as soon as you run the async function;success, the only required member besides '_', will be called if the async function returns a truthy value or a resolved Promise.
Additionally to the regular state, payload, and meta, an AsyncActionProcessor receives an actions object which contains:
actions.success(any): dispatches the success actionactions.error(any): dispatches the error actionactions.cancelled(any): dispatches the cancelled action
Just like a sync ActionCreator, an AsyncActionCreator does not need to return the whole state, but only the part it is concerned with.
Returned Objects
actionsreducer returns an array [reducer,state,actions].
Reducer
(state,action)=>stateA regular Redux reducer. The reducer takes care of checking for equality (with ==) and of not updating if nothing is returned.
State
{
[name:string]:any
}The combined state of all the passed StateChunks
Actions
{
[name:string]:ActionCreator
}An object of all ActionCreators. ActionCreators names are generated by path+actionName, where path is the whole set of previous StateChunks, and actionName is the key of the particular ActionProcessor.
If you have a deeply nested actionProcessor, this can result in forumRoomsUsersActiveSelect, which is one more reason to try to keep the state as flat as possible.
ActionCreator
actionCreator(payload:any,meta?:any,err?:boolean)=>ActionReturns an action. the object containing all ActionCreators can be found on the third element of the array returned by actionsreducer.
If err is set to true, then whatever payload is will be transformed into an Error (unless it's already an Error)
Development
Oh please yes. I could use some help.
There's no coding guidelines, as long as it's readable, anything goes.
Just submit a ticket, fork, PR.
git clone https://github.com/Xananax/actionsreducer.git && cd actionsreducer &&\
npm install &&\
npm run typingsThen:
- test:
npm test - test & exit:
npm run test:once - compile browser:
npm run build:client - compile for server:
npm run build:server - compile everything
npm run dist - run examples:
npm start(you can specify the port:PORT=3000 npm start, defaults to8080) - build examples:
npm run build:example.
Tests
Very lacking for the moment, but coming soon...
npm testLicense
The MIT License (MIT) Copyright (c) 2016 Jad Sarout
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.