chopped-redux
Advanced tools
Comparing version 1.0.4 to 2.0.1
{ | ||
"name": "chopped-redux", | ||
"version": "1.0.4", | ||
"description": "A very small Flux implementation based on @gaearon Redux", | ||
"version": "2.0.1", | ||
"description": "A tiny Flux implementation based on @gaearon Redux", | ||
"keywords": [ | ||
@@ -20,3 +20,3 @@ "flux", | ||
"devDependencies": { | ||
"faucet": "0.0.1", | ||
"immutable": "^3.7.4", | ||
"standard": "^4.3.2", | ||
@@ -26,5 +26,5 @@ "tape": "^4.0.0" | ||
"scripts": { | ||
"test": "npm run lint && node test/index.js | faucet", | ||
"lint": "standard ." | ||
"test": "npm run lint && node test/index.js", | ||
"lint": "standard" | ||
} | ||
} |
236
README.md
@@ -5,8 +5,27 @@ # Chopped Redux | ||
I recommend you try out [Redux](https://github.com/gaearon/redux) and go through its Github issues as it's a great source of knowledge and inspiration. | ||
The idea here is to provide a minimal, solid Flux (à la Redux) base without the [React](http://facebook.github.io/react/index.html) glue (you have to do that yourself), so it's possible to use this library with anything other than React. [Redux](https://github.com/gaearon/redux) allows you to do this too in 1.0, by [splitting itself](https://github.com/gaearon/redux/issues/230). | ||
The idea here is to provide a minimal, solid Flux base without the [React](http://facebook.github.io/react/index.html) glue (you have to do that yourself). | ||
You should be able to switch from Chopped Redux to Redux and viceversa without changing your flux code (constants, action creators and reducers). | ||
This project follows [SemVer](http://semver.org/). 1.0 doesn't mean it's stable or production-ready. | ||
## Bullet points | ||
Chopped main design goal: | ||
- All methods are first-class. You can freely pass them around without the need for `bind`ing. | ||
Why is Flux Redux-style so nice: | ||
- No singletons | ||
- Action creators and reducers (state-less stores) are pure functions | ||
- All state is kept in a single object, and you choose what that is (Immutable, mori, a plain object) | ||
- Plays well with Universal Javascript | ||
The things you'll miss from Redux here: | ||
- Hot reloading | ||
- Middleware | ||
- Built-in time-travel | ||
This project follows [SemVer](http://semver.org/). | ||
## Install | ||
@@ -22,21 +41,33 @@ | ||
- Factory: fluxFactory | ||
- dispatch | ||
- getDispatcher | ||
- getState | ||
- subscribe | ||
- wrapActionCreators | ||
```js | ||
var factory = require('chopped-redux') | ||
``` | ||
#### Factory: fluxFactory(reducer[, initialState]) | ||
Chopped Redux exports a single factory function that returns an object with three main methods: | ||
- *reducer* `Function|Object` These are your stores | ||
- `dispatch` | ||
- `getState` | ||
- `subscribe` | ||
and two helpers: | ||
- `wrap` | ||
- `replaceState` | ||
I like to call this instance object `flux`, in Redux is called the `store`. | ||
The factory has a single mandatory param which is a `reducer` function. | ||
#### factory(reducer[, initialState, listeners]) | ||
- *reducer* `Function` | ||
- *initialState* `Mixed` Anything you want to hold your state in | ||
- Returns `Object` A `flux` instance | ||
- *listeners* `Array` Listener callbacks that subscribe to dispatches | ||
The `reducer` function should have this signature: | ||
The `reducer` function should have the folowwing signature: | ||
```js | ||
function (state, action) { | ||
function (state, action) { | ||
// do something with state depending on the action type, | ||
// ideally generating a fresh new value | ||
// ideally generating a fresh new (immutable) value | ||
return state | ||
@@ -52,37 +83,29 @@ } | ||
If you pass an object with reducer functions, a new function will be created which will map the reduced state of every function to a key on the root state object. Like this: | ||
--- | ||
#### #dispatch(action) | ||
- *action* `Object|Function` | ||
Action creators should mostly return a plain object of the `{ type: DO_STUFF }` kind. If you need to do async stuff, return a function instead. This function receives `dispatch` and `getState` as params, so you can then actually `dispatch` the plain object needed for dispatching. | ||
```js | ||
// Your stores | ||
{ | ||
foo: [Function], | ||
bar: [Function], | ||
baz: [Function] | ||
} | ||
var asyncActionCreator = function (data) { | ||
return function (dispatch, getState) { | ||
// do your async stuff | ||
// The root state inside the `flux` instance | ||
{ | ||
foo: {...} // the reduced state from `stores.foo` | ||
bar: {...} // the reduced state from `stores.bar` | ||
baz: {...} // the reduced state from `stores.baz` | ||
dispatch({ | ||
type: STUFF_DONE, | ||
payload: foo | ||
}) | ||
} | ||
} | ||
// And you could access those like this | ||
flux.getState().foo | ||
``` | ||
#### flux.dispatch(action) | ||
#### #getState() | ||
- *action* `Object` | ||
- Returns `Object` The current state | ||
#### flux.getDispatcher() | ||
#### #subscribe(listener) | ||
- Returns the `dispatch` function bound to the `flux` instance. Literally this: `return this.dispatch.bind(this)` | ||
#### flux.getState() | ||
- Returns `Object` Your state | ||
#### flux.subscribe(listener) | ||
- *listener* `Function` A callback that gets fired after every state update | ||
@@ -93,21 +116,15 @@ - Returns `Function` A function to remove the listener | ||
#### fluxFactory.wrapActionCreators(actionCreators, dispatch) | ||
#### #wrap(methods) | ||
- *actionsCreators* `Object` Your action creators in an object | ||
- *dispatch* `Function` The dispatch function to bind to your action creators. You can get this from `flux.getDispatcher()` | ||
- Returns `Object` Your action creators bound to the dispatcher | ||
- *methods* `Object` An object with your action creators methods | ||
- Returns `Object` The same methods wrapping the dispatcher | ||
This is a helper function, so instead of writing this: | ||
So insted of doing this: | ||
```js | ||
var actionsCreators = { | ||
exampleAction: function (foo) { | ||
return { | ||
type: constants.EXAMPLE_TYPE, | ||
value: foo | ||
} | ||
} | ||
var increment = function () { | ||
return { type: INCREMENT } | ||
} | ||
flux.dispatch(actionCreators.exampleAction('bar')) | ||
flux.dispatch(increment()) | ||
``` | ||
@@ -118,118 +135,31 @@ | ||
```js | ||
var actionsCreators = { | ||
exampleAction: function (foo, dispatch) { | ||
return dispatch({ | ||
type: constants.EXAMPLE_TYPE, | ||
value: foo | ||
}) | ||
} | ||
} | ||
var actions = flux.wrap({ increment: increment }) | ||
var wrap = fluxFactory.wrapActionCreators | ||
var actions = wrap(actionCreators, flux.getDispatcher()) | ||
actions.exampleAction('bar') // dispatches! | ||
actions.increment() | ||
``` | ||
and have the action automatically dispatched. | ||
The nice thing about this is that you can provide you view components with this wrapped action creator methods which you can call directly without needing the `flux` instance available. | ||
## Example | ||
#### #replaceState(state) | ||
### Stores | ||
- *state* `Mixed` Whatever your state is | ||
Stores are pure functions, just like in Redux. They don't hold the state and don't emit events either (that's why in Redux they're called Reducers; I prefer to keep calling them Stores). They just receive the state, update it, and return the new state. | ||
This will replace the current state reference in your `flux` instance. This could be used for debugging, time-travel. For example, you could keep a copy of your `state` object of a specific point in time, and restore it later. | ||
```js | ||
var actionTypes = require('../constants/action-types') | ||
// Copy of current state | ||
var stateCopy = flux.getState() | ||
var initialState = 0 | ||
// Do stuff | ||
module.exports = function (state, action) { | ||
state = state || initialState | ||
switch (action.type) { | ||
case actionTypes.INCREMENT_COUNTER: | ||
return state + 1 | ||
break | ||
case actionTypes.DECREMENT_COUNTER: | ||
return state - 1 | ||
break | ||
default: | ||
return state | ||
} | ||
} | ||
// Some time later | ||
flux.replaceState(stateCopy) | ||
``` | ||
### Actions | ||
--- | ||
Action creators are functions that yield an action object (or *payload* in vanilla Flux terminology). They can simply return that object, or if you need to do async operations, you can pass in a dispatch callback and fire it passing in the action object. If the latter is the case, always pass the `dispatch` function as the last argument, so you can use the utility wrapper function (`wrapActionCreators`). | ||
Further reading: [The Evolution of Flux Frameworks](https://medium.com/@dan_abramov/the-evolution-of-flux-frameworks-6c16ad26bb31). | ||
```js | ||
var actionTypes = require('../constants/action-types') | ||
exports.increment = function (dispatch) { | ||
return dispatch({ | ||
type: actionTypes.INCREMENT_COUNTER | ||
}) | ||
} | ||
exports.decrement = function (dispatch) { | ||
return dispatch({ | ||
type: actionTypes.DECREMENT_COUNTER | ||
}) | ||
} | ||
``` | ||
### Constants (for action types) | ||
Yep. | ||
```js | ||
module.exports = { | ||
INCREMENT_COUNTER: 'INCREMENT_COUNTER', | ||
DECREMENT_COUNTER: 'DECREMENT_COUNTER', | ||
} | ||
``` | ||
### Make it work together | ||
```js | ||
var fluxFactory = require('chopped-redux') | ||
var wrap = fluxFactory.wrapActionCreators | ||
// Define stores and action creators | ||
var stores = { | ||
counter: require('./stores/counter') | ||
} | ||
var actionCreators = { | ||
counter: require('./actions/counter') | ||
} | ||
// Create a flux instance passing in the stores and initial state | ||
var flux = fluxFactory(stores, { counter: 1 }) | ||
// Bind action creators to the dispatcher | ||
var actions = wrap(actionCreators, flux.getDispatcher()) | ||
// Subscribe a callback to state updates | ||
var unsubscribe = flux.subscribe(function () { | ||
console.log(flux.getState()) | ||
}) | ||
// Trigger an action: this dispatches the action, | ||
// the state tree in the flux instance goes through the store function(s), | ||
// and listener callbacks fire | ||
actions.increment() | ||
// => { counter: 2 } | ||
unsubscribe() | ||
``` | ||
## License | ||
MIT |
var mapValues = require('./map-values') | ||
module.exports = function (reducer, state, listeners) { | ||
listeners = listeners || [] | ||
var combineReducers = function combineReducers (reducers) { | ||
return function (tree, action) { | ||
tree = tree || {} | ||
if (typeof reducer !== 'function') { | ||
throw new TypeError('The `reducer` param must be a function') | ||
} | ||
return mapValues(reducers, function (fn, key) { | ||
return fn(tree[key], action) | ||
}) | ||
function getState () { | ||
return state | ||
} | ||
} | ||
module.exports = function fluxFactory (reducer, initialState) { | ||
if (!reducer) { | ||
throw new Error('The `reducer` param is mandatory') | ||
function replaceState (nextState) { | ||
state = nextState | ||
} | ||
var reduce = typeof reducer === 'function' | ||
? reducer | ||
: combineReducers(reducer) | ||
function dispatch (action) { | ||
if (typeof action === 'function') { | ||
return action(dispatch, getState) | ||
} | ||
return { | ||
reduce: reduce, | ||
action = action || {} | ||
state: initialState !== null ? initialState : Object.create(null), | ||
state = reducer(state, action) | ||
listeners.forEach(function (fn) { fn(action) }) | ||
} | ||
listeners: [], | ||
function wrap (methods) { | ||
var wrapped = {} | ||
dispatch: function (action) { | ||
var self = this | ||
self.state = self.reduce(self.state, action) | ||
self.listeners.forEach(function (listener) { | ||
listener(action) | ||
}) | ||
}, | ||
Object.keys(methods).forEach(function (key) { | ||
if (typeof methods[key] === 'function') { | ||
wrapped[key] = function () { | ||
dispatch(methods[key].apply(null, arguments)) | ||
} | ||
} | ||
}) | ||
getDispatcher: function () { | ||
var self = this | ||
return self.dispatch.bind(self) | ||
}, | ||
return wrapped | ||
} | ||
getState: function () { | ||
var self = this | ||
return self.state | ||
}, | ||
function subscribe (fn) { | ||
listeners.push(fn) | ||
subscribe: function (listener) { | ||
var self = this | ||
self.listeners.push(listener) | ||
return function unsubscribe () { | ||
var index = self.listeners.indexOf(listener) | ||
self.listeners.splice(index, 1) | ||
} | ||
return function unsubscribe () { | ||
var index = listeners.indexOf(fn) | ||
listeners.splice(index, 1) | ||
} | ||
} | ||
return { | ||
getState: getState, | ||
replaceState: replaceState, | ||
dispatch: dispatch, | ||
wrap: wrap, | ||
subscribe: subscribe | ||
} | ||
} | ||
module.exports.wrapActionCreators = require('./wrap-action-creators') |
var test = require('tape') | ||
var Immutable = require('immutable') | ||
var fluxFactory = require('../') | ||
var wrapActionCreators = fluxFactory.wrapActionCreators | ||
var MANUAL = 'MANUAL' | ||
var stores = { | ||
counter: require('./fixtures/stores/counter'), | ||
sum: require('./fixtures/stores/sum'), | ||
beep: function (state, action) { | ||
return action.type === MANUAL | ||
? 'boop' | ||
: state || undefined | ||
// Silly constants | ||
var INCREMENT_COUNTER = 'INCREMENT_COUNTER' | ||
var DECREMENT_COUNTER = 'DECREMENT_COUNTER' | ||
// Actions | ||
var increment = function () { | ||
return { | ||
type: INCREMENT_COUNTER | ||
} | ||
} | ||
var actionCreators = { | ||
counter: require('./fixtures/actions/counter'), | ||
sum: require('./fixtures/actions/sum') | ||
var decrement = function () { | ||
return { | ||
type: DECREMENT_COUNTER | ||
} | ||
} | ||
/* | ||
var flux = fluxFactory(stores, {}) | ||
var dispatch = flux.getDispatcher() | ||
var actions = { | ||
counter: wrapActionCreators(actionCreators.counter, dispatch), | ||
sum: wrapActionCreators(actionCreators.sum, dispatch) | ||
// Initial state | ||
var state = { counter: 1 } | ||
var immutableState = Immutable.Map({ counter: 1 }) | ||
// Reducers | ||
var reducer = function (state, action) { | ||
state = state || { counter: 10 } | ||
switch (action.type) { | ||
case INCREMENT_COUNTER: | ||
state.counter++ | ||
break | ||
case DECREMENT_COUNTER: | ||
state.counter-- | ||
break | ||
} | ||
return state | ||
} | ||
*/ | ||
var immutableReducer = function (state, action) { | ||
switch (action.type) { | ||
case INCREMENT_COUNTER: | ||
state = state.set('counter', state.get('counter') + 1) | ||
break | ||
case DECREMENT_COUNTER: | ||
state = state.set('counter', state.get('counter') - 1) | ||
break | ||
} | ||
return state | ||
} | ||
test('fluxFactory', function (t) { | ||
t.plan(6) | ||
test('factory', function (t) { | ||
t.plan(2) | ||
var flux1 = fluxFactory(stores, {}) | ||
var flux2 = fluxFactory(stores, {}) | ||
t.notEqual(flux1, flux2, 'is a factory (no singletons)') | ||
t.ok(typeof flux1.dispatch === 'function', 'has `dispatch` method') | ||
t.ok(typeof flux1.getDispatcher === 'function', 'has `getDispatcher` method') | ||
t.ok(typeof flux1.getState === 'function', 'has `getState` method') | ||
t.ok(typeof flux1.subscribe === 'function', 'has `subscribe` method') | ||
t.throws(fluxFactory, 'should throw if missing first argument') | ||
var identity = function (x) { return x } | ||
var a = fluxFactory(identity) | ||
var b = fluxFactory(identity) | ||
t.throws(function () { | ||
fluxFactory() | ||
}, 'throws if missing reducer param') | ||
t.notEqual(a, b, 'no singleton') | ||
}) | ||
test('flux instance dispatcher', function (t) { | ||
t.plan(5) | ||
test('mutable, listeners', function (t) { | ||
t.plan(4) | ||
var flux = fluxFactory(stores, {}) | ||
var dispatch = flux.getDispatcher() | ||
var actions = { | ||
counter: wrapActionCreators(actionCreators.counter, dispatch), | ||
sum: wrapActionCreators(actionCreators.sum, dispatch) | ||
var flux = fluxFactory(reducer, state) | ||
flux.subscribe(function () {}) | ||
var unsubscribe = flux.subscribe(function () { t.pass('listener called') }) | ||
flux.subscribe(function () {}) | ||
flux.dispatch(increment()) | ||
t.equal(flux.getState().counter, 2, 'action dispatched 1') | ||
unsubscribe() | ||
flux.dispatch(decrement()) | ||
t.equal(flux.getState().counter, 1, 'action dispatched 1') | ||
t.equal(flux.getState(), state, 'state is the same mutable object') | ||
}) | ||
test('immutable', function (t) { | ||
t.plan(3) | ||
var flux = fluxFactory(immutableReducer, immutableState) | ||
flux.dispatch(increment()) | ||
t.equal(flux.getState().get('counter'), 2, 'action dispatched 1') | ||
flux.dispatch(decrement()) | ||
t.equal(flux.getState().get('counter'), 1, 'action dispatched 1') | ||
t.notEqual(flux.getState(), immutableState, 'state is not the same object') | ||
}) | ||
test('no initial state provided', function (t) { | ||
t.plan(1) | ||
var flux = fluxFactory(reducer, null) | ||
flux.dispatch(decrement()) | ||
t.equal(flux.getState().counter, 9, 'gets set in reducer') | ||
}) | ||
test('first-class dispatch and getState, no bind', function (t) { | ||
t.plan(2) | ||
var initialState = { counter: 5 } | ||
var flux = fluxFactory(reducer, initialState) | ||
function wrapper (fn) { | ||
return function (a) { | ||
return fn(a) | ||
} | ||
} | ||
actions.counter.increment() | ||
t.equal(flux.getState().counter, 1, 'dispatches via wrapped action creators (1)') | ||
var dispatch = wrapper(flux.dispatch) | ||
var getState = wrapper(flux.getState) | ||
actions.counter.decrement() | ||
t.equal(flux.getState().counter, 0, 'dispatches via wrapped action creators (2)') | ||
t.equal(getState(), initialState, 'getState') | ||
actionCreators.counter.increment(dispatch) | ||
t.equal(flux.getState().counter, 1, 'dispatches via action creators') | ||
dispatch(increment()) | ||
t.equal(getState().counter, 6, 'dispatch') | ||
}) | ||
flux.dispatch({ type: MANUAL }) | ||
t.equal(flux.getState().beep, 'boop', 'dispatches manually') | ||
test('handle actions being functions', function (t) { | ||
t.plan(2) | ||
actions.sum.sum(10) | ||
t.deepEqual(flux.getState(), { | ||
counter: 1, | ||
beep: 'boop', | ||
sum: 10 | ||
}, 'handles the state tree props correctly') | ||
var flux = fluxFactory(reducer, { counter: 32 }) | ||
flux.dispatch(function (dispatch, getState) { | ||
t.equal(getState().counter, 32, 'getState gets passed in') | ||
dispatch({ type: INCREMENT_COUNTER }) | ||
}) | ||
t.equal(flux.getState().counter, 33, 'alright') | ||
}) | ||
test('flux instance subscribe method', function (t) { | ||
t.plan(6) | ||
test('replaceState', function (t) { | ||
t.plan(2) | ||
var flux = fluxFactory(stores, {}) | ||
var initialState = { counter: -1 } | ||
var flux = fluxFactory(reducer, initialState) | ||
var off1 = flux.subscribe(function (action) { t.pass('fires listener (1) ' + action.type) }) | ||
var off2 = flux.subscribe(function (action) { t.pass('fires listener (2) ' + action.type) }) | ||
var off3 = flux.subscribe(function (action) { t.pass('fires listener (3) ' + action.type) }) | ||
flux.dispatch(increment()) | ||
t.equal(flux.getState().counter, 0, '(test dispatch)') | ||
t.ok(typeof off1 === 'function', 'returns ´unsubscribe´ function') | ||
flux.replaceState({ counter: 24 }) | ||
flux.dispatch(decrement()) | ||
t.equal(flux.getState().counter, 23, 'works') | ||
}) | ||
flux.dispatch({ type: 'A' }) // 3 passes | ||
off2() | ||
flux.dispatch({ type: 'B' }) // 2 passes (listener 2 is off) | ||
off1() | ||
off3() | ||
flux.dispatch({ type: 'C' }) // nothing | ||
test('wrap action creators', function (t) { | ||
t.plan(3) | ||
var inc = function (data) { | ||
t.equal(data.foo, 'bar', 'arguments get passed in') | ||
return { | ||
type: INCREMENT_COUNTER | ||
} | ||
} | ||
var flux = fluxFactory(reducer, { counter: 20 }) | ||
var actions = flux.wrap({ increment: inc, decrement: decrement }) | ||
actions.increment({ foo: 'bar' }) | ||
t.equal(flux.getState().counter, 21, 'works') | ||
actions.decrement() | ||
t.equal(flux.getState().counter, 20, 'correctly') | ||
}) | ||
test('empty dispatching', function (t) { | ||
t.plan(1) | ||
var flux = fluxFactory(reducer) | ||
t.doesNotThrow(flux.dispatch, 'is possible') | ||
}) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
10282
5
181
161
1