Skip to content

Instantly share code, notes, and snippets.

@goldhand
Created July 20, 2016 14:59
Show Gist options
  • Save goldhand/344abe2597efea46dd3c559a97ac8a7a to your computer and use it in GitHub Desktop.
Save goldhand/344abe2597efea46dd3c559a97ac8a7a to your computer and use it in GitHub Desktop.
Redux midddleware for accruing interest before dispatching an action
import {Map} from 'immutable';
/**
* Accrue redux middleware will only return actions that have accured enough interest
*
* change in accrued interest is calculated by interest += (next - prev)
* Math.abs(interest) >= resistance for action to recieve accrued flag
*
* To opt into this functionality add {meta: {resistance: <value>, accrue: <field>}} to the dispatched action.
* Where <value> is the amount that should be accrued before next dispatch and
* <field> is the field in the dispatched action that contains the value of <value>
*
* @example - Throttle actions until innerWidth delta 50
* function updateWindowWidth() {
* return {
* type: types.RESIZE_WIDTH,
* windowWidth: innerWidth,
* meta: {
* resistance: 50,
* accrue: 'windowWidth',
* },
* };
* }
* @attribute {number} accrued - positive or negative change since last accrued action
* @returns {action} - next(action)
*/
export default () => next => {
// store accrued information in [action.type] buckets
let refs = Map();
return action => {
const {meta, type} = action;
// not opting into this middleware
if (!meta || !meta.resistance) {
return next(action);
}
const
{resistance, accrue} = meta,
nextValue = action[accrue],
prevValue = refs.getIn([type, 'prev'], 0),
change = nextValue - prevValue;
// if no change don't return anything
if (!change) return;
// accrue interest
refs = refs.updateIn([type, 'interest'], 0, v => v + change);
refs = refs.setIn([type, 'prev'], nextValue);
// interest is full
if (Math.abs(refs.getIn([type, 'interest'])) >= resistance) {
refs = refs.setIn([type, 'interest'], 0);
return next(action);
}
return;
};
};
import test from 'ava';
import accrue from 'middleware/accrue';
import {createStore, applyMiddleware} from 'redux';
function counterAction(w) {
return {
type: 'COUNT',
counter: w,
meta: {
resistance: 50,
accrue: 'counter',
},
};
}
function reducer(state = {counter: 0}, action) {
switch (action.type) {
case 'COUNT':
return {
...state,
counter: action.counter,
};
default:
return state;
}
}
test('accrue doesnt dispatch until full', t => {
t.plan(3);
let store = createStore(reducer, applyMiddleware(accrue));
store.dispatch(counterAction(10));
t.is(store.getState().counter, 0, 'only 10 of 50');
store.dispatch(counterAction(20));
t.is(store.getState().counter, 0, 'only 20 of 50');
store.dispatch(counterAction(60));
t.is(store.getState().counter, 60, 'state should update');
});
test('accrue will empty after fill', t => {
t.plan(3);
let store = createStore(reducer, applyMiddleware(accrue));
store.dispatch(counterAction(75));
t.is(store.getState().counter, 75, 'should have filled once');
store.dispatch(counterAction(110));
t.is(store.getState().counter, 75, 'shouldnt change state');
store.dispatch(counterAction(140));
t.is(store.getState().counter, 140, 'should fill again and update state');
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment