chopped-redux
Advanced tools
Comparing version 2.0.1 to 3.0.0
{ | ||
"name": "chopped-redux", | ||
"version": "2.0.1", | ||
"description": "A tiny Flux implementation based on @gaearon Redux", | ||
"version": "3.0.0", | ||
"description": "A subset of @gaearon Redux", | ||
"keywords": [ | ||
"flux", | ||
"redux" | ||
"redux", | ||
"state" | ||
], | ||
"homepage": "https://github.com/acstll/chopped-redux", | ||
"main": "src/", | ||
"main": "index.js", | ||
"author": "acstll <arturo@arturu.com>", | ||
@@ -18,3 +19,5 @@ "repository": "acstll/chopped-redux", | ||
"license": "MIT", | ||
"dependencies": {}, | ||
"dependencies": { | ||
"curry": "^1.2.0" | ||
}, | ||
"devDependencies": { | ||
@@ -26,5 +29,6 @@ "immutable": "^3.7.4", | ||
"scripts": { | ||
"test": "npm run lint && node test/index.js", | ||
"test": "node test/index.js", | ||
"pretest": "npm run lint", | ||
"lint": "standard" | ||
} | ||
} |
182
README.md
# Chopped Redux | ||
A very small Flux implementation based on [@gaearon](https://github.com/gaearon) [Redux](https://github.com/gaearon/redux), mainly inspired by [this](https://github.com/gaearon/redux/pull/166) and [this](https://github.com/gaearon/redux/issues/113#issuecomment-114049804). | ||
![npm version](https://img.shields.io/npm/v/chopped-redux.svg) ![npm downloads](https://img.shields.io/npm/dm/chopped-redux.svg) | ||
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). | ||
This library is a subset of [@gaearon](https://github.com/gaearon) [Redux](https://github.com/gaearon/redux), which claims to be a "Predictable state container for JavaScript apps". | ||
You should be able to switch from Chopped Redux to Redux and viceversa without changing your flux code (constants, action creators and reducers). | ||
Redux is based on [Facebook's Flux](https://facebook.github.io/flux/) but it's a lot more simple a straightforward. Chopped Redux follows the same principles and ideas but cutting off features. If you care, it's 30 sloc (0.75 kB). | ||
## Bullet points | ||
This project follows [SemVer](http://semver.org/). | ||
Chopped main design goal: | ||
## Motivation | ||
- All methods are first-class. You can freely pass them around without the need for `bind`ing. | ||
In the beginning, Redux was a [React](http://facebook.github.io/react/) thing. So I wanted to have a similar library not tight to any rendering/view-layer library, and I was mainly inspired by [this](https://github.com/gaearon/redux/pull/166) and [this](https://github.com/gaearon/redux/issues/113#issuecomment-114049804), ideas which made the Flux unidirectional data-flow very simple. Redux is [free from React](https://github.com/gaearon/redux/issues/230) starting at 1.0. Still Chopped is a simpler alternative to it (though Redux is itself very small and simple). The things you'll miss from Redux here are basically `Middleware`, ES2015/7 magic and restrictions. Hot-reloading and time-travel are possible if you know what you're doing, or **why** you're doing it, but it's **not built-in**. | ||
Why is Flux Redux-style so nice: | ||
## Install | ||
- 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 | ||
With [`npm`](http://npmjs.org) do: | ||
The things you'll miss from Redux here: | ||
```bash | ||
npm install chopped-redux --save | ||
``` | ||
- Hot reloading | ||
- Middleware | ||
- Built-in time-travel | ||
## Usage | ||
This project follows [SemVer](http://semver.org/). | ||
This is how it works: | ||
## Install | ||
- You `dispatch` an `action` | ||
- The `state` gets updated based on that `action` | ||
- All `listeners` get notified of the `state` change | ||
With [`npm`](http://npmjs.org) do: | ||
```js | ||
var chopped = require('chopped-redux') | ||
```bash | ||
npm install chopped-redux --save | ||
function reducer (state, action) { | ||
state = state || 0 // initialize state if empty | ||
if (action.type === 'increment') { | ||
return state + 1 | ||
} | ||
return state // always return state | ||
} | ||
var store = chopped(reducer) | ||
var action = { type: 'increment' } // actions are objects | ||
store.subscribe(function () { | ||
console.log(store.getState()) | ||
}) | ||
store.dispatch(action) | ||
// => 1 | ||
``` | ||
Guidelines for success: | ||
- All state of your app goes into `state`, a single object | ||
- The `reducer` function is **pure** (it should *only* update and return new `state` and nothing else) | ||
- `actions` are plain objects with at least two properties `type` (String) and `payload` (Mixed) | ||
- You do async inside helper functions [(action dispatchers)](#async-and-action-creators) that call `dispatch` when done | ||
## API | ||
```js | ||
var factory = require('chopped-redux') | ||
var createStore = require('chopped-redux') | ||
``` | ||
Chopped Redux exports a single factory function that returns an object with three main methods: | ||
Chopped Redux exports a single factory function that returns an object with four methods: | ||
@@ -49,13 +73,7 @@ - `dispatch` | ||
- `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]) | ||
#### `createStore(reducer[, initialState, listeners])` | ||
@@ -66,3 +84,3 @@ - *reducer* `Function` | ||
The `reducer` function should have the folowwing signature: | ||
The `reducer` function should have the following signature: | ||
@@ -85,76 +103,96 @@ ```js | ||
#### #dispatch(action) | ||
#### `dispatch(action)` | ||
- *action* `Object|Function` | ||
- Returns `undefined` | ||
- *action* `Object` | ||
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. | ||
#### `getState()` | ||
```js | ||
var asyncActionCreator = function (data) { | ||
return function (dispatch, getState) { | ||
// do your async stuff | ||
- Returns `Object` The current state | ||
dispatch({ | ||
type: STUFF_DONE, | ||
payload: foo | ||
}) | ||
} | ||
} | ||
``` | ||
#### `subscribe(listener)` | ||
#### #getState() | ||
- Returns `Function` A function to remove the listener | ||
- *listener* `Function` A callback that gets fired after every state update | ||
- Returns `Object` The current state | ||
#### `replaceState(state)` | ||
#### #subscribe(listener) | ||
- Returns `undefined` | ||
- *state* `Mixed` Whatever your state is | ||
- *listener* `Function` A callback that gets fired after every state update | ||
- Returns `Function` A function to remove the listener | ||
This will replace the current state reference in your `store` instance. This could be used for debugging, time-travel, etc. Beware you need to call `dispatch` after replacing the state if you want your views to update or whatever. | ||
--- | ||
### Helpers | ||
#### #wrap(methods) | ||
#### `wrap(methods, dispatch)` | ||
- *methods* `Object` An object with your action creators methods | ||
Available at `require('chopped-redux/wrap')`. | ||
This is a highly opinionated helper that binds your action dispatchers (aka action creators) to a `store.dispatch` instance, by currying them. | ||
This functions are meant to have this signature `function (dispatch, payload) {}`. See [Async and action creators](#async-and-action-creators) below. | ||
- Returns `Object` The same methods wrapping the dispatcher | ||
- *methods* `Object` An object with your action dispatcher functions | ||
- *dispatch* `Function` The `dispatch` method from your `store` instance | ||
So insted of doing this: | ||
--- | ||
## Async and action creators | ||
Handling async stuff in vanilla Flux is a pain. In the beginning of Flux we were making API calls inside our Stores, that turned out to be a bad idea. So they came up with this pompous concept of Action Creators to confuse us all (at least for a while). [If you’re still confused, Action Creators are functions that return Actions, which are simply objects; so Action == plain object, Action Creator == function that creates an Action.] Apparently no-one knows how to do this right. | ||
In Redux there’s middleware. The [thunk](https://github.com/gaearon/redux-thunk) middleware *transforms* an Action Creator (they call it “intent”) into an object that you can dispatch, and you *create* Action Creators like this: | ||
```js | ||
var increment = function () { | ||
return { type: INCREMENT } | ||
function foo (bar) { | ||
// do async stuff | ||
return function (dispatch) { | ||
dispatch({ | ||
type: FOO, | ||
bar: bar | ||
}) | ||
} | ||
} | ||
flux.dispatch(increment()) | ||
// after binding it and what not, call it | ||
foo() | ||
``` | ||
you can do this: | ||
I prefer to (partly) avoid the concept of Action Creators with a simpler approach, namely this: | ||
```js | ||
var actions = flux.wrap({ increment: increment }) | ||
function foo (dispatch, payload) { | ||
// do async stuff | ||
actions.increment() | ||
dispatch({ | ||
type: FOO, | ||
payload: payload | ||
}) | ||
} | ||
foo(store.dispatch, { foo: ‘bar’ }) | ||
``` | ||
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. | ||
in which the `dispatch` callback always gets passed in as first argument. | ||
#### #replaceState(state) | ||
If you care about names, I would call this an *action dispatcher* function, because that’s what it does. There’s no nesting, no type checking, no complexity. You just pass in a callback for dispatching an action with some payload. You’re just delegating `dispatch`ing actions to a helper function to do some things before the dispatch. | ||
- *state* `Mixed` Whatever your state is | ||
If you don’t need async, simply `dispatch` the action directly and you’ve got one less function to care about. | ||
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 | ||
// Copy of current state | ||
var stateCopy = flux.getState() | ||
store.dispatch({ type: FOO, payload: payload }) | ||
``` | ||
// Do stuff | ||
If you want to be consistent, go always the async way no matter what. | ||
// Some time later | ||
flux.replaceState(stateCopy) | ||
``` | ||
No more `ActionCreators.addTodo(text)`. | ||
--- | ||
## Further reading | ||
Further reading: [The Evolution of Flux Frameworks](https://medium.com/@dan_abramov/the-evolution-of-flux-frameworks-6c16ad26bb31). | ||
https://gist.github.com/vslinko/cab24085f029def8997b by @vslinko | ||
[The Evolution of Flux Frameworks](https://medium.com/@dan_abramov/the-evolution-of-flux-frameworks-6c16ad26bb31) | ||
@@ -161,0 +199,0 @@ ## License |
@@ -5,18 +5,15 @@ | ||
var fluxFactory = require('../') | ||
var createStore = require('../') | ||
var wrap = require('../wrap') | ||
// Silly constants | ||
// Action types | ||
var INCREMENT_COUNTER = 'INCREMENT_COUNTER' | ||
var DECREMENT_COUNTER = 'DECREMENT_COUNTER' | ||
// Actions | ||
var increment = function () { | ||
return { | ||
type: INCREMENT_COUNTER | ||
} | ||
// Action factories (creators) | ||
var increment = function (dispatch) { | ||
dispatch({ type: INCREMENT_COUNTER }) | ||
} | ||
var decrement = function () { | ||
return { | ||
type: DECREMENT_COUNTER | ||
} | ||
var decrement = function (dispatch) { | ||
dispatch({ type: DECREMENT_COUNTER }) | ||
} | ||
@@ -60,7 +57,7 @@ | ||
var identity = function (x) { return x } | ||
var a = fluxFactory(identity) | ||
var b = fluxFactory(identity) | ||
var a = createStore(identity) | ||
var b = createStore(identity) | ||
t.throws(function () { | ||
fluxFactory() | ||
createStore() | ||
}, 'throws if missing reducer param') | ||
@@ -74,17 +71,17 @@ | ||
var flux = fluxFactory(reducer, state) | ||
var store = createStore(reducer, state) | ||
flux.subscribe(function () {}) | ||
var unsubscribe = flux.subscribe(function () { t.pass('listener called') }) | ||
flux.subscribe(function () {}) | ||
store.subscribe(function () {}) | ||
var unsubscribe = store.subscribe(function () { t.pass('listener called') }) | ||
store.subscribe(function () {}) | ||
flux.dispatch(increment()) | ||
t.equal(flux.getState().counter, 2, 'action dispatched 1') | ||
increment(store.dispatch) | ||
t.equal(store.getState().counter, 2, 'action dispatched 1') | ||
unsubscribe() | ||
flux.dispatch(decrement()) | ||
t.equal(flux.getState().counter, 1, 'action dispatched 1') | ||
decrement(store.dispatch) | ||
t.equal(store.getState().counter, 1, 'action dispatched 1') | ||
t.equal(flux.getState(), state, 'state is the same mutable object') | ||
t.equal(store.getState(), state, 'state is the same mutable object') | ||
}) | ||
@@ -95,11 +92,11 @@ | ||
var flux = fluxFactory(immutableReducer, immutableState) | ||
var store = createStore(immutableReducer, immutableState) | ||
flux.dispatch(increment()) | ||
t.equal(flux.getState().get('counter'), 2, 'action dispatched 1') | ||
increment(store.dispatch) | ||
t.equal(store.getState().get('counter'), 2, 'action dispatched 1') | ||
flux.dispatch(decrement()) | ||
t.equal(flux.getState().get('counter'), 1, 'action dispatched 1') | ||
decrement(store.dispatch) | ||
t.equal(store.getState().get('counter'), 1, 'action dispatched 1') | ||
t.notEqual(flux.getState(), immutableState, 'state is not the same object') | ||
t.notEqual(store.getState(), immutableState, 'state is not the same object') | ||
}) | ||
@@ -110,6 +107,6 @@ | ||
var flux = fluxFactory(reducer, null) | ||
var store = createStore(reducer, null) | ||
flux.dispatch(decrement()) | ||
t.equal(flux.getState().counter, 9, 'gets set in reducer') | ||
store.dispatch({ type: DECREMENT_COUNTER }) | ||
t.equal(store.getState().counter, 9, 'gets set in reducer') | ||
}) | ||
@@ -121,3 +118,3 @@ | ||
var initialState = { counter: 5 } | ||
var flux = fluxFactory(reducer, initialState) | ||
var store = createStore(reducer, initialState) | ||
@@ -130,24 +127,11 @@ function wrapper (fn) { | ||
var dispatch = wrapper(flux.dispatch) | ||
var getState = wrapper(flux.getState) | ||
var dispatch = wrapper(store.dispatch) | ||
var getState = wrapper(store.getState) | ||
t.equal(getState(), initialState, 'getState') | ||
dispatch(increment()) | ||
increment(dispatch) | ||
t.equal(getState().counter, 6, 'dispatch') | ||
}) | ||
test('handle actions being functions', function (t) { | ||
t.plan(2) | ||
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('replaceState', function (t) { | ||
@@ -157,31 +141,32 @@ t.plan(2) | ||
var initialState = { counter: -1 } | ||
var flux = fluxFactory(reducer, initialState) | ||
var store = createStore(reducer, initialState) | ||
flux.dispatch(increment()) | ||
t.equal(flux.getState().counter, 0, '(test dispatch)') | ||
increment(store.dispatch) | ||
t.equal(store.getState().counter, 0, '(test dispatch)') | ||
flux.replaceState({ counter: 24 }) | ||
flux.dispatch(decrement()) | ||
t.equal(flux.getState().counter, 23, 'works') | ||
store.replaceState({ counter: 24 }) | ||
decrement(store.dispatch) | ||
t.equal(store.getState().counter, 23, 'works') | ||
}) | ||
test('wrap action creators', function (t) { | ||
test('wrap/curry action factories', function (t) { | ||
t.plan(3) | ||
var inc = function (data) { | ||
var inc = function (dispatch, data) { | ||
t.equal(data.foo, 'bar', 'arguments get passed in') | ||
return { | ||
dispatch({ | ||
type: INCREMENT_COUNTER | ||
} | ||
}) | ||
} | ||
var flux = fluxFactory(reducer, { counter: 20 }) | ||
var actions = flux.wrap({ increment: inc, decrement: decrement }) | ||
var store = createStore(reducer, { counter: 20 }) | ||
var actions = wrap({ increment: inc }, store.dispatch) | ||
actions.increment({ foo: 'bar' }) | ||
t.equal(flux.getState().counter, 21, 'works') | ||
t.equal(store.getState().counter, 21, 'works') | ||
actions.decrement() | ||
t.equal(flux.getState().counter, 20, 'correctly') | ||
t.throws(function () { | ||
wrap({ increment: increment }, store.dispatch) | ||
}, 'must take at least 2 arguments') | ||
}) | ||
@@ -192,4 +177,4 @@ | ||
var flux = fluxFactory(reducer) | ||
t.doesNotThrow(flux.dispatch, 'is possible') | ||
var store = createStore(reducer) | ||
t.doesNotThrow(store.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
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
12668
6
199
1
3
173
+ Addedcurry@^1.2.0
+ Addedcurry@1.2.0(transitive)