Experimental state management library.
Alternative for 'redux-thunk' 0 reducer boilerplate
Application state can be changed directly from actions creators
Action objects are auto generated and dispatched when (setState / clearState / remove) functions are invoked
Immutability is taken care of by middlewares and published by child reducers
Reducers can be safely extended without any predefined shape.
function grow() {
return function (nonedux) {
console.log(nonedux.state); // {subState: {}}
let child = nonedux.subState;
[1,2,3].forEach(n => {
child.setState({[n]: {}});
child = child[n];
})
console.log(nonedux.state) // {subState: {1: {2: {3: {}}}}}
};
}
function generateMessState(depth = 3, height = 0) {
/* initialState = {mess: {}, ...}*/
return function (nonedux, { dispatch, }) {
const { mess, } = nonedux;
let child = mess;
for (let i = 0; i<depth && child; i++) {
child = child.setState({ [height]: dispatch(generateMessState(i, i+1)) })[height]
}
return nonedux.mess.state;
};
})
npm install --save none-dux
yarn add none-dux
import { Provider, connect, } from 'react-redux';
import { createStore, applyMiddleware, combineReducers, } from 'redux';
import nonedux from 'none-dux';
const initialState = { //Sames as the initial state of store
request: {},
todosByUser: {},
users: {},
};
// don't add 'redux-thunk'
const { reducers, middlewares } = nonedux({ initialState });
// creates the reducers defined in initialState
console.log(Object.keys(reducers)); //['request', 'todosByUser', 'users']
const store = createStore(
combineReducers({ ...reducers, }), // can be combined with other reducers like redux-form
applyMiddleware(...middlewares)
);
const root = (
<Provider store={store}>
<Router history={browserHistory}>
<Route path='/' component={App}>
...
setState remove, clearState can be called to nonedux objects and arrays from inside action creators:
// actions 1st argument is nonedux reference, 2nd one is redux store
// they are injected to dispatched functions by nonedux thunk
export function removeUser(userId) {
return function (nonedux, {dispatch}) {
const {users, todosByUser} = nonedux;
const user = users[userId];
user.setState({ verified: false, });
//All promises must be returned
return api.deleteUser(userId)
.then(()=> {;
users.remove(userId);
todosByUser.remove(userId);
});
}
}
...
function createPayment(userId, data){
return function({users, transactions}){
const user = users[userId];
if(!user.state.pendingPayment){
user.setState({pendingPaymend: true})
const id = uuid();
const {[id]: transaction} = transactions[userId].setState({
[id]: {id, ...data, userId, validated: false, }
})
return api.postTransactions(transaction.state)
.then(() => {
transaction.setState({validated: true})
user.setState({pendingPayment: false})
})
.catch(err => { ... })
}
}
}
All promises must be returned from Actions, otherwise changing app state after promise will throw error. Alternative use async await and call await to all promises.
function returnPromisesAsynAwait() {
return async function ({ data, notifications, }) {
notifications.setState({ dataFetch: 'pending', });
try {
const dataResult = await api.fetchData();
data.setState(dataResult.data);
notifications.setState({ dataFetch: 'success', });
await sleep();
notifications.setState({ dataFetch: '', })
} catch (e) {
notifications.setState({ dataFetch: 'error', });
await sleep();
notifications.setState({ dataFetch: '', });
}
};
}
function returnPromisesTraditional() {
return function ({ data, notifications, }) {
notifications.setState({ dataFetch: 'pending', });
return api.fetchData() // return Promise
.then((result) => {
data.setState(result.data);
notifications.setState({ dataFetch: 'success', });
return sleep() // return Promise
.then(() => notifications.setState({ dataFetch: '', }))
}).catch(() => {
notifications.setState({ dataFetch: 'error', });
return sleep() // return Promise
.then(() => notifications.setState({ dataFetch: '', }))
});
};
}
const {child} = nonedux;
child.setState({subChild: {}}).subChild.setState({noChild: null})
console.log(child.state);
// { child: { subChild: { noChild: null} } }
console.log(target.state); // { a: 1, b: {} }
// setState does shallow merge
target.setState({ a: 2, c: 3 });
console.log(target.state) // { a: 2, b: {}, c: 3 }
// setState takes Objects as parameters
target.setState('test'); //Error("[...]")
target.setState([ 1, 'abc', {} ]); //Error("[...]")
console.log(target.state); //{ a: 1, b: { } }
// clear state removes the previous states outer join
target.clearState({ b: 2 });
console.log(target.state); // { b: 2 }
// clearState takes both Object or Array as parameter
target.setState('text'); //Error("[...]")
const keys = [ 1, 2, 3 ];
target.remove(keys); //removes all children with matching keys
//same as
target.remove(1,2,3);
//or
target.remove(...[1,2,3]);
Root level state variables must be defined at nonedux initialState
Leaf values like String, Numbers, Date, etc. Can only be changed through parent Object/Array and used through parents state
{
console.log(target.state)// { name: 'text' };
console.log(target.name); //undefined
console.log(target.state.name); //'text'
}
...
{
const {data} = nonedux.data.setState({ str: 'abc' });
console.log(data.str); // undefined
console.log(data.str.state); // throws Error(...)
console.log(data.state.str); // 'abc'
}
...
{
const { data } = nonedux.data.setState({ obj: { str: 'ok' } })
console.log(data.obj) //Branch: ...
console.log(data.obj.state) // { str: 'ok' }
}
If you redux stack consists of redux, react-redux and redux-thunk you can try out none-dux with a few steps:
happy path:
replace
...
import thunk from 'redux-thunk'
const middlewares = [ thunk, ];
const createStoreWithMiddleware = applyMiddleware(...middlewares)(createStore);
const store = createStoreWithMiddleware(reducers);
<Provider store={store}> ...
by
// Take the current initial state of from your reducers and use it as initialState
...
import nonedux from 'none-dux'
// ( Do not use 'redux-thunk' )
const { reducers, middlewares, } = nonedux({ initialState });
const createStoreWithMiddleware = applyMiddleware(...middlewares)(createStore);
const store = createStoreWithMiddleware(combineReducers({...reducers}));
<Provider store={store}> ...
replace with something like
//actions redux-thunk
function updateUser(id, changes){
return function(dispatch){
dispatch({type: UPDATE_USER, payload: {id, ...changes}})
}
}
By
//actions nonedux
function updateUser(id, changes){
return function({users}){
users[id].setState(changes);
}
}
import nonedux, { shape } from 'none-dux
import validator from './validator'
...
const { reducer, middlewares, subject, } = nonedux({ initialState });
const createStoreWithMiddleware = applyMiddleware(...middlewares, shape.validatorMiddleware(subject, validators))(createStore);
const store = createStoreWithMiddleware(combineReducers({ ...reducers, })
...
const { types, any, validatorMiddleware } = shape;
const { isRequired, strict, string, number, bool } = types;
const validator = {
...isRequired.strict, // ! Use destructed when you have Objects shape spesification
todosByUser: {
...isRequired, // Not null nor undefined
[any]: { // byUserIds
[any]: {
...strict, // byTodoIds. 'strict' console errors when values outside of spec are added
userId: string.isRequired, // ! No desctructing
id: string.isRequired,
description: string.isRequired,
done: bool,
},
},
users: { // by id
[any]: {
...strict,
//one liner for creating multiple keys with same spec
...string.many('id',
'firstName',
'lastName')
},
},
//more examples
someObjectList: [
isRequired, // ! No desctructing
{
a: number,
b: {}, // Object that can include anything an is not required
}
],
someStringList: [ string ],
request: { ...isRequired },
}
};
//Object shape
Will work:
{ ...isRequired }
{ ...strict }
{ ...strict.isRequired }
{ ...isRequired.strict }
{ isRequired: string } //Assumed key name is actually 'isRequired'!
Won't work:
{ strict }
{ isRequired }
//Array shape
Will work:
[ strict, {} ]
[ isRequired, number ]
[ strict.isRequired, {...isRequired} ] //array with not-null/undefined objects
[ isRequired.strict, [] ] //array that has arrays
Won't work
[ ...isRequired, number ]
[ ...strict, {} ]
[ ...strict.isRequired ]
[ ...isRequired.strict, [] ]
// 'any' key
Will work:
{ [any]: number, myObj: {} } //assumes that values is number is name is not 'myObj'
{ [any]: {} }
{ any: string, } //means that the key name is actually 'any'
Wont work:
{ [any]: any, } //any is not type but identifier
{ something: any } //same here
Using shape makes the performance slower so check process.end.NODE_ENV before adding it as middleware
/* in action creator */
// Store state = { users: {} }
*TRANSACTION*
function playWithTransaction(){
return function(nonedux){
nonedux.transaction(({ users }) => {
users.setState({ a:{}, b: {} })
users.setState({ c:{} })
users.c.setState({ todos:{} })
}) // --> update store state
}
}
*SIMPLE ROLLBACK*
function playWithRollback(){
return function(nonedux){
nonedux.transaction(({ users }) => {
users.setState({ a:{}, b: {} })
users.setState({ c:{} })
nonedux.c.setState({ todos:{} })
throw new Error(); // ROLLBACK ALL
}) // --> no published changes
}
}
*ADVANCED ROLLBACK*
function rollbackAdvanced(){
return function(nonedux, {dispatch}){
nonedux.transaction(({ users }) => {
users.setState({ a:{} })
try{
dispatch(doChangesAndThrowError());
}catch(ignore){ /* explicitly thrown error */ }
}) // --> state = { users: { a: {} } }
}
}
function doChangesAndThrowError(){
return function(nonedux){
nonedux.transaction(({ users }) => {
users.a.setState({ todos: {} })
users.a.transaction(({ todos }) => {
const id = uuid();
todos.setState({[id]: id, done: false, description: 'Buy milk'})
}) // --> rollback scopes changes
})
}
}
Arrays are not shallow merged like objects
'setState' and 'remove' can be expensive if run on older browsers, that do not support es6 Proxy
The technical details about why this is so, boils down to same reasons, why React is advices to not use index as 'key':s for component when creating list of components
Depending of use case, with old browsers the performance can be ~5 times slower than with redux
// Max time usually represents the time, operation takes on legacy browsers
// Bench marking done on MacBook Pro with i7 cpu
function removeRetiringEmployees(){
return function(nonedux){
const {employees} = nonedux;
Object.keys(employees.state)
.map(k => employees[k])
// access single CHILD takes avg 0.005 - 0.02 ms
.filter(employee => employee.state.age>=64)
// access to single STATE takes avg 0.002 - 0.04 ms
.forEach(employee => employees.remove(employee.getId()));
// single object REMOVE takes avg 0.1 - 0.3 ms
}
}
function removeRetiringEmployee_better(){
return function({employees}){
const retiringEmployees = Object.entries(employees.state)
.filter(([k, employeeState])=> employeeState.age>=64)
.map(([k]) => k);
employees.remove(retiringEmployees);
}
}
function fetchEmployees(){
return function({employees}){
return api.fetchEmployees(({data}) => {
employees.setState(data);
// assuming there is entries 1000 and every empty on data is object
// takes about 0.75ms on modern browser
// on legacy browser <7ms
})
}
}
function fetchEmployees_better(){
return function(nonedux){
return api.fetchEmployees(({data}) => {
nonedux.setState({employees: data});
// assuming there is entries 1000 and every empty on data is object
// in avg good case takes about 0.1ms on modern browser
// good case on legacy browser <7ms
// on legacy browser 18ms
})
}
}
In some cases, when changing an array state that has Objects or other Arrays as children can be several times more inefficient compared to using objects
Leafs are types that do not have children, nor they cannot be referenced directly but only through state
...
const {child} = nonedux;
child.clearState({
numb: 1, str: 'abc',
err: new Error(), date: Date.now(),
regexp: /nonedux/,
bool: false,
func: () => console.log('im a function')
arrLeaf = createLeaf([ 1, {}, [] ]),
objLeaf = createLeaf({ a:1, b: {}, c[] })
})
const { ...childsChildren } = child;
Object.keys(childsChildren).length; // 0
Object.keys(child.state).length; // 7
Object.getPrototypeOf(child.state.arrLeaf).constructor.name // ArrayLeaf
Object.getPrototypeOf(child.state.objLeaf).constructor.name // ObjectLeaf
...
import { leafs } from 'none-dux'
leafs.MyClass = true;
...
nonedux.subState.setState({child: new MyClass()};)
nonedux.subState.child; //undefined
nonedux.subState.state.child; // MyClass...
Note that performance is not great on old browsers. This should not be an issue unless someone is using and old IPhone or Windows Phone and you are doing a lot of animations.
const key = () => console.log('I'm key');
target.setState({[key]: {} }) // This will result in bugs
const {a, b, ...rest} = target.setState({ a:{}, b: {}, c: {}, d: {} })
Object.keys(rest).length // 0
for(const child in target){
console.log('This will never be executed')
}
target.setState({ a:{}, b: {}, c: {}, d: {} })
const {a, b, ...rest} = target.getChildren()
Object.keys(rest).length // 2
//or
const {state} = target.setState({ a:{}, b: {}, c: {}, d: {} })
const children = Object.keys(state).map(k => targe[k]);
// In modern browsers next evaluates to false
target.setState({a: {}})
target.a === target.a
// in old browsers the same evaluates to true
const first = {a:1}, second = {b:2}, third = {c:3}
target.clearState([ first, second, third, ]);
const { 0: firstChild, 1: secondChild, 2: thirdChild, } = target;
target.clearState([ third, first, second, ]); //switch order
/* One might expect that 'firstChild' state,
would still points to 'first' value but it doesn't */
firstChild.state; // { c: 3, };
secondChild.state; // { a: 1, };
thirdChild.state; // { b: 2, };
//From 'clearState:s' point of view the previous means:
`target.clearState({0: first, 1: second, 2: third })`
When nonedux code is run on browsers that do not support es6 Proxy the internal logic is implemented differently
With large objects & arrays that include thousands of child objects the performance can be poor.
When accessing data from actions this is roughly what happens:
function juggle(){
return function(nonedux){
nonedux.state // { obj: { primitive: 1, subChild: {} } }
const obj = nonedux.obj
// nonedux --> get() { return child['obj'] || child['obj'] = createBranch('obj') }
// obj --> createGetterFor('subChild')
}
}
... when data is not referenced no work is done.
If objects children are leafs: (string, numbers, etc.) there should not any performance problems
To improve performance Objects and Arrays can be wrapped as leafs by using createLeaf helper function.
import {createLeaf} from 'none-dux'
function fetchCustomerData_Lightweight(){
return function(nonedux){
fetchUserData()
.then(({data}) => {
let { transactions, associations } = data;
transactions = createLeaf(transactions);
associations = createLeaf(associations)
const {statistics} = nonedux;
statistics.setState({transactions, associations}). //no direct refence to children;
statistics.transactions //undefined
statistics.associations //undefined
statistics.state.transactions // is defined
statistics.state.associations // is defined
})
}
}
The key differences compared to v10 is that the performance is 1-10 better in most heavies cases when run on modern browsers
Better support for older browsers
createLeaf has become obsolete (when not used in old browsers)
When used in old browsers 'legacy' (v10) mode will be used, because Proxy features cannot be added using babel
Table of Proxy support can be found at: https://kangax.github.io/compat-table/es6/
const {subject, middlewares} = nonedux(initialState, bool /*flag for saga usage*/)
//constructor parameters is an Object
const {subject, middlewares} = nonedux({
initialState,
saga:bool, //optional
legacy: bool //optional --> if not defined, it will be automatically use legacy when run on a oldbrowser
})
const {a, b, ...rest} = target.setState({ a:{}, b: {}, c: {}, d: {} })
Object.keys(rest).length // 0
for(const child in target){
console.log('This will never be executed')
}
const {a, b, ...rest} = target.getChildren()
Object.keys(rest).length // 2
//or as Array
const {state} = target.setState({ a:{}, b: {}, c: {}, d: {} })
const children = Object.keys(state).map(k => targe[k]);
4. No references are stored, when modern browsers are used. This makes almost everything bizillion times faster
target.setState({ a:{} });
target.a // instance created
target.a // instance created again
target.a === target.a // false
target.a.state === target.a.state; // true