-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 7aea512
Showing
9 changed files
with
346 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"presets": ["es2015-loose", "react"], | ||
"plugins": [] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
lib | ||
node_modules |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
# Do not publish .babelrc to npm since it can create problems with babel 5 in other projects | ||
.babelrc |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
5.1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
The MIT License (MIT) | ||
|
||
Copyright (c) 2015 Alan Johnson | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
# react-redux-controller | ||
|
||
**react-redux-controller** is a library that adds some opinion to the [react-redux](https://github.com/rackt/react-redux) binding of [React](https://facebook.github.io/react/index.html) components to the [Redux](http://redux.js.org/) store. It creates the entity of a `Controller`, which is intended to be the single point of integration between React and Redux. The controller passes data and callbacks to the UI components via the [React `context`](https://facebook.github.io/react/docs/context.html). It's one solution to [the question](http://stackoverflow.com/a/34320909/807674) of how to get data and controller-like methods (e.g. event handlers) to the React UI components. | ||
|
||
## Philosophy | ||
|
||
This library takes the opinion that React components should solely be focused on the job of rendering and capturing user input, and that Redux actions and reducers should be soley focused on the job of managing the store and providing a view of the state of the store in the form of [selectors](http://rackt.org/redux/docs/basics/UsageWithReact.html). The plumbing of distributing data to components, as well as deciding what to fetch, when to fetch, how to manage latency, and what to do with error handling, should be vested in an explicit controller layer. | ||
|
||
This differs from alternative methods in a number of ways: | ||
|
||
- The ancestors of a component are not responsible for conveying dependencies to via `props` -- particularly when it comes to dependencies the ancestors don't use themselves. | ||
- The components are not coupled to Redux in any way -- no `connect` distributed throughout the component tree. | ||
- There are no [smart components](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.m5y0saa0k). Well there's one, but it's hidden inside the Controller. | ||
- Action creators do not peforming any fetching. They are only responsible for constructing action objects, as is the case in vanilla Redux, with no middleware needed. | ||
|
||
## Usage | ||
|
||
The **controller** factory requires 3 parameters: | ||
|
||
- The root component of the UI component tree. | ||
- An object that holds controller generator functions. | ||
- Any number of selector bundles, which are likely simply imported selector modules, each selector annotated a [`propType`](https://facebook.github.io/react/docs/reusable-components.html) that indicates what kind of data it provides. | ||
|
||
The functionality of the controller layer is implemented using [generator functions](http://www.2ality.com/2015/03/es6-generators.html). Within these functions, `yield` may be used await the results of [Promises](http://www.2ality.com/2014/09/es6-promises-foundations.html) and to request selector values and root component properties. As a very rough sketch of how you might use this library: | ||
|
||
``` | ||
// controllers/app_controller.js | ||
import AppLayout from '../components/app_layout'; | ||
import * as mySelectors from '../selectors/my_selectors'; | ||
const controllerGenerators = { | ||
*onUserActivity(meaningfulParam) { | ||
const { dispatch, otherData } = yield getProps; | ||
dispatch(actions.fetchingData()); | ||
try { | ||
const apiData = yield httpRequest(`http://myapi.com/${otherData}`); | ||
return dispatch(actions.fetchingSuccessful(apiData)); | ||
} catch (err) { | ||
return dispatch(actions.errorFetching(err)); | ||
} | ||
}, | ||
// ... other controller generators follow | ||
}; | ||
const selectorBundles = [ | ||
mySelectors, | ||
]; | ||
export default controller(AppLayout, controllerMethodFactories, selectorBundles); | ||
``` | ||
|
||
Better examples to come. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
{ | ||
"name": "react-redux-controller", | ||
"version": "0.1.0", | ||
"description": "Library for creating a controller layer to link React and Redux.", | ||
"license": "MIT", | ||
"keywords": [ | ||
"controller", | ||
"react", | ||
"redux" | ||
], | ||
"repository": { | ||
"type": "git", | ||
"url": "git://github.com/artsy/react-redux-controller.git" | ||
}, | ||
"author": { | ||
"name": "Alan Johnson", | ||
"email": "[email protected]" | ||
}, | ||
"engines": { | ||
"node": ">= 5.1.x" | ||
}, | ||
"main": "lib/index.js", | ||
"scripts": { | ||
"build": "mkdir -p lib && babel ./src --out-dir ./lib", | ||
"prepublish": "npm run build" | ||
}, | ||
"dependencies": { | ||
"co": "^4.6.0", | ||
"ramda": "^0.18.0" | ||
}, | ||
"devDependencies": { | ||
"babel-cli": "^6.3.17", | ||
"babel-core": "^6.3.21", | ||
"babel-loader": "^6.2.0", | ||
"babel-polyfill": "^6.3.14", | ||
"babel-preset-es2015": "^6.3.13", | ||
"babel-preset-es2015-loose": "^6.1.3", | ||
"babel-preset-react": "^6.3.13", | ||
"co": "^4.6.0", | ||
"mocha": "*", | ||
"ramda": "^0.18.0", | ||
"react": "0.14.0", | ||
"react-redux": "^4.0.0", | ||
"redux": "^3.0.0", | ||
"should": "*", | ||
"webpack": "^1.12.9" | ||
}, | ||
"files": [ | ||
"dist", | ||
"lib", | ||
"src" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
import { default as React, PropTypes } from 'react'; | ||
import { connect } from 'react-redux'; | ||
import R from 'ramda'; | ||
import co from 'co'; | ||
import { aggregateSelectors } from './selector_utils'; | ||
|
||
const toDispatchSymbol = Symbol('toDispatch'); | ||
|
||
/** Request to get the props object at a specific time */ | ||
export const getProps = Symbol('getProps'); | ||
|
||
/** | ||
* Request to get a function that will return the controller `props` object, | ||
* when called. | ||
*/ | ||
export const getPropsGetter = Symbol('getPropsGetter'); | ||
|
||
/** | ||
* Conveniece request to dispatch an action directly from a controller | ||
* generator. | ||
* @param {*} action a Redux action | ||
* @return {*} the result of dispatching the action | ||
*/ | ||
export function toDispatch(action) { | ||
return { [toDispatchSymbol]: action }; | ||
} | ||
|
||
/** | ||
* The default function for converting the controllerGenerators to methods that | ||
* can be directly called. It resolves `yield` statements in the generators by | ||
* delegating Promise to `co` and processing special values that are used to | ||
* request data from the controller. | ||
* @param {Function} propsGetter gets the current controller props. | ||
* @return {GeneratorToMethod} a function that converts a generator to a method | ||
* forwarding on the arguments the generator receives. | ||
*/ | ||
export function runControllerGenerator(propsGetter) { | ||
return controllerGenerator => co.wrap(function* coWrapper(...args) { | ||
const gen = controllerGenerator(...args); | ||
let value; | ||
let done; | ||
let toController; | ||
|
||
for ({ value, done } = gen.next(); !done; { value, done } = gen.next(toController)) { | ||
const props = propsGetter(); | ||
|
||
// In the special cases that the yielded value has one of our special | ||
// tags, process it, and then we'll send the result on to `co` anyway | ||
// in case whatever we get back is a promise. | ||
if (value && value[toDispatchSymbol]) { | ||
// Dispatch an action | ||
toController = props.dispatch(value[toDispatchSymbol]); | ||
} else if (value === getProps) { | ||
// Return all props | ||
toController = props; | ||
} else if (value === getPropsGetter) { | ||
// Return the propsGetter itself, so the controller can get props | ||
// values in async continuations | ||
toController = propsGetter; | ||
} else { | ||
// Defer to `co` | ||
toController = yield value; | ||
} | ||
} | ||
|
||
return value; | ||
}); | ||
} | ||
|
||
/** | ||
* This higher-order component introduces a concept of a Controller, which is a | ||
* component that acts as an interface between the proper view component tree | ||
* and the Redux state modeling, building upon react-redux. It attempts to | ||
* solve a couple problems: | ||
* | ||
* - It provides a way for event handlers and other helpers to access the | ||
* application state and dispatch actions to Redux. | ||
* - It conveys those handlers, along with the data from the react-redux | ||
* selectors, to the component tree, using React's [context](bit.ly/1QWHEfC) | ||
* feature. | ||
* | ||
* It was designed to help keep UI components as simple and domain-focused | ||
* as possible (i.e. [dumb components](bit.ly/1RFh7Ui), while concentrating | ||
* the React-Redux integration point at a single place. It frees intermediate | ||
* components from the concern of routing dependencies to their descendents, | ||
* reducing coupling of components to the UI layout. | ||
* | ||
* @param {React.Component} RootComponent is the root of the app's component | ||
* tree. | ||
* @param {Object} controllerGenerators contains generator methods to be used | ||
* to create controller methods, which are distributed to the component tree. | ||
* These are called from UI components to trigger state changes. These | ||
* generators can `yield` Promises to be resolved via `co`, can `yield` | ||
* requests to receive application state or dispatch actions, and can | ||
* `yield*` to delegate to other controller generators. | ||
* @param {(Object|Object[])} selectorBundles maps property names to selector | ||
* functions, which produce property value from the Redux store. | ||
* @param {Function} [controllerGeneratorRunner = runControllerGenerator] is | ||
* the generator wrapper that will be used to run the generator methods. | ||
* @return {React.Component} a decorated version of RootComponent, with | ||
* `context` set up for its descendents. | ||
*/ | ||
export function controller(RootComponent, controllerGenerators, selectorBundles, controllerGeneratorRunner = runControllerGenerator) { | ||
// Combine selector bundles into one mapStateToProps function. | ||
const mapStateToProps = aggregateSelectors(R.mergeAll(R.flatten([selectorBundles]))); | ||
const selectorPropTypes = mapStateToProps.propTypes; | ||
|
||
// All the controller method propTypes should simply be "function" so we can | ||
// synthensize those. | ||
const controllerMethodPropTypes = R.map(() => PropTypes.func.isRequired, controllerGenerators); | ||
|
||
// Declare the availability of all of the selectors and controller methods | ||
// in the React context for descendant components. | ||
const contextPropTypes = R.merge(selectorPropTypes, controllerMethodPropTypes); | ||
|
||
class Controller extends React.Component { | ||
constructor(...constructorArgs) { | ||
super(...constructorArgs); | ||
|
||
const injectedControllerGeneratorRunner = controllerGeneratorRunner(() => this.props); | ||
this.controllerMethods = R.map(controllerGenerator => | ||
injectedControllerGeneratorRunner(controllerGenerator).bind(controllerGenerators) | ||
, controllerGenerators); | ||
} | ||
|
||
getChildContext() { | ||
// Rather than injecting all of the RootComponent props into the context, | ||
// we only explictly pass selector and controller method props. | ||
const selectorProps = R.pick(R.keys(selectorPropTypes), this.props); | ||
return R.merge(selectorProps, this.controllerMethods); | ||
} | ||
|
||
render() { | ||
return ( | ||
<RootComponent {...this.props} /> | ||
); | ||
} | ||
} | ||
|
||
Controller.propTypes = R.merge(selectorPropTypes, RootComponent.propTypes || {}); | ||
Controller.childContextTypes = contextPropTypes; | ||
|
||
return connect(mapStateToProps)(Controller); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import R from 'ramda'; | ||
|
||
/** | ||
* Combines bundle of selector functions into a single super selector function | ||
* maps the state to extracts a set of property name-value pairs corresponding | ||
* to the selector function outputs. The selectors materialize a view of the | ||
* Redux store, which will be fed to React components as `props`. | ||
* | ||
* A selector bundle looks like: | ||
* | ||
* { | ||
* selectorName: (state) => value, | ||
* ... | ||
* } | ||
* | ||
* , where each selector function should carry a `propType` property, | ||
* describing its result. | ||
* | ||
* The resulting super selector function looks like: | ||
* | ||
* state => { | ||
* selectorName: value, | ||
* ... | ||
* } | ||
* | ||
* , and has a `propTypes` property of the form: | ||
* | ||
* { | ||
* selectorName: propType, | ||
* ... | ||
* } | ||
* | ||
* This property can be merged directly into a `propTypes` or `contextTypes` | ||
* property on a React component. | ||
* | ||
* A bundle is typically created by importing an entire module of exported | ||
* selector functions. To keep track of React prop types, selector functions | ||
* should be annotated by assigning a `propType` property to the function | ||
* within the module where it is declared. | ||
* | ||
* @param {Object.<string, Function>} selectorBundles contains the | ||
* selectors, as explained above. | ||
* @return {Function} a function that, when given the store state, produces all | ||
* of the selector outputs. | ||
*/ | ||
export function aggregateSelectors(bundle) { | ||
const combinedSelector = state => R.map(selectorFunction => selectorFunction(state), bundle); | ||
combinedSelector.propTypes = R.map(selectorFunction => selectorFunction.propType, bundle); | ||
return combinedSelector; | ||
} | ||
|
||
/** | ||
* Does the opposite of [[aggregateSelectors]] | ||
* | ||
* @param {Function} superSelector | ||
* @return {Object.<string, Function>} a selector bundle, with each selector | ||
* annotated with a propType property. | ||
*/ | ||
export function disaggregateSuperSelector(superSelector) { | ||
return R.mapObjIndexed((propType, propName) => { | ||
const singleSelector = R.pipe(superSelector, R.prop(propName)); | ||
singleSelector.propType = propType; | ||
return singleSelector; | ||
}, superSelector.propTypes); | ||
} |