redux-interactions
Advanced tools
Comparing version 1.3.0 to 1.4.1
export { default as Interactions } from './Interactions'; | ||
export { default as combineInteractions } from './combineInteractions'; | ||
export { default as reducer } from './reducer'; | ||
export { default as selector } from './selector'; | ||
import * as types from './types'; | ||
export { types }; |
"use strict"; | ||
var Interactions_1 = require('./Interactions'); | ||
exports.Interactions = Interactions_1.default; | ||
var combineInteractions_1 = require('./combineInteractions'); | ||
exports.combineInteractions = combineInteractions_1.default; | ||
var reducer_1 = require('./reducer'); | ||
exports.reducer = reducer_1.default; | ||
var selector_1 = require('./selector'); | ||
exports.selector = selector_1.default; | ||
var types = require('./types'); | ||
exports.types = types; |
@@ -9,2 +9,3 @@ import * as types from './types'; | ||
export default class Interactions { | ||
mountPoint: string[]; | ||
initialState: any; | ||
@@ -11,0 +12,0 @@ _interactionReducers: { |
@@ -1,2 +0,1 @@ | ||
export declare type Decorator = (target: any, key: string, descriptor: PropertyDescriptor) => void; | ||
/** | ||
@@ -3,0 +2,0 @@ * Shorthand for calling `addInteractionReducer` on an `Interactions`. |
@@ -21,5 +21,5 @@ "use strict"; | ||
} | ||
Counter.prototype.add = function (state, amount) { | ||
Counter.prototype.add = function (scopedState, amount) { | ||
if (amount === void 0) { amount = 1; } | ||
return state + amount; | ||
return scopedState + amount; | ||
}; | ||
@@ -78,5 +78,5 @@ __decorate([ | ||
} | ||
Counter.prototype.add = function (state, amount) { | ||
Counter.prototype.add = function (scopedState, amount) { | ||
if (amount === void 0) { amount = 1; } | ||
return state + amount; | ||
return scopedState + amount; | ||
}; | ||
@@ -83,0 +83,0 @@ __decorate([ |
{ | ||
"name": "redux-interactions", | ||
"version": "1.3.0", | ||
"version": "1.4.1", | ||
"description": "A streamlined approach to managing your Redux action creators and reducers.", | ||
@@ -29,2 +29,3 @@ "homepage": "https://github.com/convoyinc/redux-interactions", | ||
"dependencies": { | ||
"deep-update": "^1.2.0", | ||
"lodash": "^4.0.0", | ||
@@ -38,2 +39,3 @@ "unique-type": "^1.1.0" | ||
"mocha-clean": "^1.0.0", | ||
"react": "^15.0.2", | ||
"sinon": "^1.17.3", | ||
@@ -40,0 +42,0 @@ "sinon-chai": "^2.8.0", |
193
README.md
@@ -17,4 +17,5 @@ # redux-interactions | ||
* A lot of business logic is necessarily asynchronous, which can't be encapsulated in a reducer. | ||
* Most reducers are singletons, and | ||
These observations have landed us on a pattern where you group related action creators and reducers into a single file, which we call an _interaction_. | ||
These observations have landed us on a pattern where you group related action creators, reducers, and selectors into a single file, which we call an _interaction_. | ||
@@ -25,16 +26,54 @@ For example, a hypothetical, offline-only, todos app might have a "todos" interaction that manages the state of the todos being displayed to the user: | ||
```js | ||
import * as _ from 'lodash'; | ||
// Action types | ||
const ADD_TODO = 'ADD_TODO'; | ||
const TOGGLE_TODO = 'TOGGLE_TODO'; | ||
const REMOVE_TODO = 'REMOVE_TODO'; | ||
// Where in the store the reducer should be mounted. | ||
export const MOUNT_POINT = ['entities', 'todos']; | ||
// Selectors | ||
export function getAll(state) { | ||
return _.values(_.get(state, MOUNT_POINT)); | ||
} | ||
export function getById(state, id) { | ||
return _.get(state, [...MOUNT_POINT, id]); | ||
} | ||
// Action Creators | ||
export function add(id, text, completed = false) { | ||
export function add(text, completed = false) { | ||
return async (dispatch, _getState) => { | ||
const todo = {id: uuid.v4(), text, completed, saving: true}; | ||
// Optimisticaly add the todo to the store for immediate user feedback. | ||
dispatch(addLocal(todo)); | ||
try { | ||
// Lets assume this succeeds for any 2xx; and we assume that means the | ||
// todo was successfully persisted. | ||
apiRequest('post', '/todos', {body: todo}); | ||
} catch (error) { | ||
// TERRIBLE user experience, but this is just an example. | ||
alert(`Failed to save todo, please try again`); | ||
dispatch(removeLocal(todo.id)); | ||
} | ||
}; | ||
} | ||
export function addLocal(id, text, completed = false) { | ||
return {type: ADD_TODO, id, text, completed}; | ||
} | ||
export function toggle(id) { | ||
export function toggleLocal(id) { | ||
return {type: TOGGLE_TODO}; | ||
} | ||
export function removeLocal(id, id) { | ||
return {type: REMOVE_TODO, id}; | ||
} | ||
// Reducers | ||
@@ -48,2 +87,4 @@ | ||
return _reduceToggle(state, action); | ||
case REMOVE_TODO: | ||
return _reduceRemove(state, action); | ||
default: | ||
@@ -70,41 +111,11 @@ return state; | ||
} | ||
``` | ||
The store would wire it up by referencing `todos.reducer`, and components would trigger actions via a `dispatch(todos.add())`, etc. Hopefully a pretty straightforward pattern. | ||
The interesting outcome of this approach is that it subtly changes your approach to managing business logic: Each action/reducer pair tends to encapsulate a single _state modification_, rather than an a swath of business logic. More complicated business logic naturally flows via thunked actions that _complete_ all of our state modification actions/reducers. | ||
For example, if we wanted to add a server to the mix, we might modify the above interaction to include another action creator: | ||
`todos.js`: | ||
```js | ||
… | ||
export function addLocal(id, text, completed = false) { | ||
return {type: ADD_TODO, id, text, completed}; | ||
function _reduceRemove(state, action) { | ||
return _.omit(state, action.id); | ||
} | ||
export function add(text, completed = false) { | ||
return async (dispatch, _getState) => { | ||
const todo = {id: uuid.v4(), text, completed, saving: true}; | ||
// Optimisticaly add the todo to the store for immediate user feedback. | ||
dispatch(addLocal(todo)); | ||
try { | ||
// Lets assume this succeeds for any 2xx; and we assume that means the | ||
// todo was successfully persisted. | ||
apiRequest('post', '/todos', {body: todo}); | ||
dispatch(markSaved(todo.id)); | ||
} catch (error) { | ||
// TERRIBLE user experience, but this is just an example. | ||
dispatch(flash.error(`Failed to save todo, please try again`)); | ||
dispatch(removeLocal(todo.id)); | ||
} | ||
}; | ||
} | ||
… | ||
``` | ||
This new action creator nicely encapsulates all business logic around adding a todo to the application. For ease of discussion, let's call it a _business action (creator)_. And the more focused state modification actions like `addLocal` as _state action (creators)_. | ||
The store would wire it up by mounting `todos.reducer` at `entities.todos`, and components would trigger actions via a `dispatch(todos.add())`, etc. Components use `todos.getById` and `todos.getAll` to retrieve state from that slice of the store. | ||
Business actions very rarely dispatch a raw action directly. Instead, they compose state actions and frequently perform asynchronous workflows. | ||
The interesting outcome of this approach is that it subtly changes your approach to managing business logic: Each action/reducer pair tends to encapsulate a single _state modification_, rather than an a swath of business logic. More complicated business logic naturally flows via thunked actions that _complete_ all of our state modification actions/reducers. | ||
@@ -117,3 +128,3 @@ | ||
```js | ||
import { Interactions, reducer } from 'redux-interactions'; | ||
import { Interactions, reducer, selector } from 'redux-interactions'; | ||
@@ -143,2 +154,7 @@ /** | ||
/** | ||
* The path in the store that this interactions' reducer should be mounted at. | ||
*/ | ||
mountPoint = ['entities', 'todos']; | ||
/** | ||
* Initial state for the subset of the store managed by these interactions. | ||
@@ -149,2 +165,44 @@ */ | ||
/** | ||
* Selects all todos in the store. | ||
*/ | ||
@selector | ||
getAll(scopedState) { | ||
return _.values(scopedState); | ||
} | ||
/** | ||
* Selects a single todo by id. | ||
*/ | ||
@selector | ||
getById(scopedState, id) { | ||
return scopedState[id]; | ||
} | ||
/** | ||
* Add a todo and let the server know. | ||
* | ||
* This rounds off the example, in an effort to show off the pattern. Note | ||
* that this method is purely an action creator; there's nothing special going | ||
* on here. | ||
*/ | ||
add(text, completed = false) { | ||
return async (dispatch, _getState) => { | ||
const todo = {id: uuid.v4(), text, completed, saving: true}; | ||
// Optimisticaly add the todo to the store for immediate user feedback. | ||
dispatch(this.addLocal(todo)); | ||
try { | ||
// Lets assume this succeeds for any 2xx; and we assume that means the | ||
// todo was successfully persisted. | ||
apiRequest('post', '/todos', {body: todo}); | ||
dispatch(this.markSaved(todo.id)); | ||
} catch (error) { | ||
// TERRIBLE user experience, but this is just an example. | ||
alert(`Failed to save todo, please try again`); | ||
dispatch(this.removeLocal(todo.id)); | ||
} | ||
}; | ||
} | ||
/** | ||
* Add a todo, without involving the server. | ||
@@ -165,9 +223,9 @@ * | ||
* 3. It registers the decorated function to be run when that action type is | ||
* encountered. `state` is always the first argument, `action.args` are | ||
* encountered. `scopedState` is always the first argument, `action.args` are | ||
* expanded to be the rest of the arguments. | ||
*/ | ||
@reducer | ||
addLocal(state, id, text, completed = false) { | ||
addLocal(scopedState, id, text, completed = false) { | ||
return [ | ||
...state, | ||
...scopedState, | ||
{id, text, completed}, | ||
@@ -188,4 +246,4 @@ ]; | ||
@reducer('CUSTOM_TODOS_TOGGLE') | ||
toggleLocal(state, id) { | ||
return state.map(todo => { | ||
toggleLocal(scopedState, id) { | ||
return scopedState.map(todo => { | ||
if (todo.id !== id) return todo; | ||
@@ -200,25 +258,7 @@ return { | ||
/** | ||
* Add a todo and let the server know. | ||
* | ||
* This rounds off the example, in an effort to show off the pattern. Note | ||
* that this method is purely an action creator; there's nothing special going | ||
* on here. | ||
* Removes a todo locally. | ||
*/ | ||
add(text, completed = false) { | ||
return async (dispatch, _getState) => { | ||
const todo = {id: uuid.v4(), text, completed, saving: true}; | ||
// Optimisticaly add the todo to the store for immediate user feedback. | ||
dispatch(this.addLocal(todo)); | ||
try { | ||
// Lets assume this succeeds for any 2xx; and we assume that means the | ||
// todo was successfully persisted. | ||
apiRequest('post', '/todos', {body: todo}); | ||
dispatch(this.markSaved(todo.id)); | ||
} catch (error) { | ||
// TERRIBLE user experience, but this is just an example. | ||
dispatch(flash.error(`Failed to save todo, please try again`)); | ||
dispatch(this.removeLocal(todo.id)); | ||
} | ||
}; | ||
@reducer | ||
removeLocal(scopedState, id) { | ||
return _.omit(scopedState, id); | ||
} | ||
@@ -228,1 +268,24 @@ | ||
``` | ||
## Mounting Interactions | ||
Interactions need to be aware of where they are mounted in the store, since they are providing selectors. In order to centralize the store's configuration, you will probably want to use `combineInteractions()` to mount them, and to set their mount points: | ||
```js | ||
import * as interactions from './interactions'; | ||
/** | ||
* Returns a reducer with all interactions mounted according to the given | ||
* hierarchy. | ||
* | ||
* This will _modify_ any interactions passed in, setting their `mountPoint` to | ||
* match the location in the store that they are being mounted at. It is an | ||
* error to pass interactions that specify their own `mountPoint`. | ||
*/ | ||
const interactionsReducer = combineInteractions({ | ||
entities: { | ||
todos: interaction.todos, | ||
}, | ||
}); | ||
``` |
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
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
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
42648
29
842
281
0
3
11
+ Addeddeep-update@^1.2.0
+ Addeddeep-update@1.3.4(transitive)
+ Addedimmutability-helper@2.9.1(transitive)
+ Addedinvariant@2.2.4(transitive)
+ Addedjs-tokens@4.0.0(transitive)
+ Addedloose-envify@1.4.0(transitive)