Skip to content

Commit 7aea512

Browse files
committed
initial commit
0 parents  commit 7aea512

File tree

9 files changed

+346
-0
lines changed

9 files changed

+346
-0
lines changed

.babelrc

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"presets": ["es2015-loose", "react"],
3+
"plugins": []
4+
}

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lib
2+
node_modules

.npmignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Do not publish .babelrc to npm since it can create problems with babel 5 in other projects
2+
.babelrc

.nvmrc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
5.1

LICENSE.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2015 Alan Johnson
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# react-redux-controller
2+
3+
**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.
4+
5+
## Philosophy
6+
7+
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.
8+
9+
This differs from alternative methods in a number of ways:
10+
11+
- 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.
12+
- The components are not coupled to Redux in any way -- no `connect` distributed throughout the component tree.
13+
- 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.
14+
- 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.
15+
16+
## Usage
17+
18+
The **controller** factory requires 3 parameters:
19+
20+
- The root component of the UI component tree.
21+
- An object that holds controller generator functions.
22+
- 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.
23+
24+
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:
25+
26+
```
27+
// controllers/app_controller.js
28+
29+
import AppLayout from '../components/app_layout';
30+
import * as mySelectors from '../selectors/my_selectors';
31+
32+
const controllerGenerators = {
33+
*onUserActivity(meaningfulParam) {
34+
const { dispatch, otherData } = yield getProps;
35+
dispatch(actions.fetchingData());
36+
try {
37+
const apiData = yield httpRequest(`http://myapi.com/${otherData}`);
38+
return dispatch(actions.fetchingSuccessful(apiData));
39+
} catch (err) {
40+
return dispatch(actions.errorFetching(err));
41+
}
42+
},
43+
// ... other controller generators follow
44+
};
45+
46+
const selectorBundles = [
47+
mySelectors,
48+
];
49+
50+
export default controller(AppLayout, controllerMethodFactories, selectorBundles);
51+
52+
```
53+
54+
Better examples to come.

package.json

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"name": "react-redux-controller",
3+
"version": "0.1.0",
4+
"description": "Library for creating a controller layer to link React and Redux.",
5+
"license": "MIT",
6+
"keywords": [
7+
"controller",
8+
"react",
9+
"redux"
10+
],
11+
"repository": {
12+
"type": "git",
13+
"url": "git://github.com/artsy/react-redux-controller.git"
14+
},
15+
"author": {
16+
"name": "Alan Johnson",
17+
"email": "[email protected]"
18+
},
19+
"engines": {
20+
"node": ">= 5.1.x"
21+
},
22+
"main": "lib/index.js",
23+
"scripts": {
24+
"build": "mkdir -p lib && babel ./src --out-dir ./lib",
25+
"prepublish": "npm run build"
26+
},
27+
"dependencies": {
28+
"co": "^4.6.0",
29+
"ramda": "^0.18.0"
30+
},
31+
"devDependencies": {
32+
"babel-cli": "^6.3.17",
33+
"babel-core": "^6.3.21",
34+
"babel-loader": "^6.2.0",
35+
"babel-polyfill": "^6.3.14",
36+
"babel-preset-es2015": "^6.3.13",
37+
"babel-preset-es2015-loose": "^6.1.3",
38+
"babel-preset-react": "^6.3.13",
39+
"co": "^4.6.0",
40+
"mocha": "*",
41+
"ramda": "^0.18.0",
42+
"react": "0.14.0",
43+
"react-redux": "^4.0.0",
44+
"redux": "^3.0.0",
45+
"should": "*",
46+
"webpack": "^1.12.9"
47+
},
48+
"files": [
49+
"dist",
50+
"lib",
51+
"src"
52+
]
53+
}

src/index.js

+144
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { default as React, PropTypes } from 'react';
2+
import { connect } from 'react-redux';
3+
import R from 'ramda';
4+
import co from 'co';
5+
import { aggregateSelectors } from './selector_utils';
6+
7+
const toDispatchSymbol = Symbol('toDispatch');
8+
9+
/** Request to get the props object at a specific time */
10+
export const getProps = Symbol('getProps');
11+
12+
/**
13+
* Request to get a function that will return the controller `props` object,
14+
* when called.
15+
*/
16+
export const getPropsGetter = Symbol('getPropsGetter');
17+
18+
/**
19+
* Conveniece request to dispatch an action directly from a controller
20+
* generator.
21+
* @param {*} action a Redux action
22+
* @return {*} the result of dispatching the action
23+
*/
24+
export function toDispatch(action) {
25+
return { [toDispatchSymbol]: action };
26+
}
27+
28+
/**
29+
* The default function for converting the controllerGenerators to methods that
30+
* can be directly called. It resolves `yield` statements in the generators by
31+
* delegating Promise to `co` and processing special values that are used to
32+
* request data from the controller.
33+
* @param {Function} propsGetter gets the current controller props.
34+
* @return {GeneratorToMethod} a function that converts a generator to a method
35+
* forwarding on the arguments the generator receives.
36+
*/
37+
export function runControllerGenerator(propsGetter) {
38+
return controllerGenerator => co.wrap(function* coWrapper(...args) {
39+
const gen = controllerGenerator(...args);
40+
let value;
41+
let done;
42+
let toController;
43+
44+
for ({ value, done } = gen.next(); !done; { value, done } = gen.next(toController)) {
45+
const props = propsGetter();
46+
47+
// In the special cases that the yielded value has one of our special
48+
// tags, process it, and then we'll send the result on to `co` anyway
49+
// in case whatever we get back is a promise.
50+
if (value && value[toDispatchSymbol]) {
51+
// Dispatch an action
52+
toController = props.dispatch(value[toDispatchSymbol]);
53+
} else if (value === getProps) {
54+
// Return all props
55+
toController = props;
56+
} else if (value === getPropsGetter) {
57+
// Return the propsGetter itself, so the controller can get props
58+
// values in async continuations
59+
toController = propsGetter;
60+
} else {
61+
// Defer to `co`
62+
toController = yield value;
63+
}
64+
}
65+
66+
return value;
67+
});
68+
}
69+
70+
/**
71+
* This higher-order component introduces a concept of a Controller, which is a
72+
* component that acts as an interface between the proper view component tree
73+
* and the Redux state modeling, building upon react-redux. It attempts to
74+
* solve a couple problems:
75+
*
76+
* - It provides a way for event handlers and other helpers to access the
77+
* application state and dispatch actions to Redux.
78+
* - It conveys those handlers, along with the data from the react-redux
79+
* selectors, to the component tree, using React's [context](bit.ly/1QWHEfC)
80+
* feature.
81+
*
82+
* It was designed to help keep UI components as simple and domain-focused
83+
* as possible (i.e. [dumb components](bit.ly/1RFh7Ui), while concentrating
84+
* the React-Redux integration point at a single place. It frees intermediate
85+
* components from the concern of routing dependencies to their descendents,
86+
* reducing coupling of components to the UI layout.
87+
*
88+
* @param {React.Component} RootComponent is the root of the app's component
89+
* tree.
90+
* @param {Object} controllerGenerators contains generator methods to be used
91+
* to create controller methods, which are distributed to the component tree.
92+
* These are called from UI components to trigger state changes. These
93+
* generators can `yield` Promises to be resolved via `co`, can `yield`
94+
* requests to receive application state or dispatch actions, and can
95+
* `yield*` to delegate to other controller generators.
96+
* @param {(Object|Object[])} selectorBundles maps property names to selector
97+
* functions, which produce property value from the Redux store.
98+
* @param {Function} [controllerGeneratorRunner = runControllerGenerator] is
99+
* the generator wrapper that will be used to run the generator methods.
100+
* @return {React.Component} a decorated version of RootComponent, with
101+
* `context` set up for its descendents.
102+
*/
103+
export function controller(RootComponent, controllerGenerators, selectorBundles, controllerGeneratorRunner = runControllerGenerator) {
104+
// Combine selector bundles into one mapStateToProps function.
105+
const mapStateToProps = aggregateSelectors(R.mergeAll(R.flatten([selectorBundles])));
106+
const selectorPropTypes = mapStateToProps.propTypes;
107+
108+
// All the controller method propTypes should simply be "function" so we can
109+
// synthensize those.
110+
const controllerMethodPropTypes = R.map(() => PropTypes.func.isRequired, controllerGenerators);
111+
112+
// Declare the availability of all of the selectors and controller methods
113+
// in the React context for descendant components.
114+
const contextPropTypes = R.merge(selectorPropTypes, controllerMethodPropTypes);
115+
116+
class Controller extends React.Component {
117+
constructor(...constructorArgs) {
118+
super(...constructorArgs);
119+
120+
const injectedControllerGeneratorRunner = controllerGeneratorRunner(() => this.props);
121+
this.controllerMethods = R.map(controllerGenerator =>
122+
injectedControllerGeneratorRunner(controllerGenerator).bind(controllerGenerators)
123+
, controllerGenerators);
124+
}
125+
126+
getChildContext() {
127+
// Rather than injecting all of the RootComponent props into the context,
128+
// we only explictly pass selector and controller method props.
129+
const selectorProps = R.pick(R.keys(selectorPropTypes), this.props);
130+
return R.merge(selectorProps, this.controllerMethods);
131+
}
132+
133+
render() {
134+
return (
135+
<RootComponent {...this.props} />
136+
);
137+
}
138+
}
139+
140+
Controller.propTypes = R.merge(selectorPropTypes, RootComponent.propTypes || {});
141+
Controller.childContextTypes = contextPropTypes;
142+
143+
return connect(mapStateToProps)(Controller);
144+
}

src/selector_utils.js

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import R from 'ramda';
2+
3+
/**
4+
* Combines bundle of selector functions into a single super selector function
5+
* maps the state to extracts a set of property name-value pairs corresponding
6+
* to the selector function outputs. The selectors materialize a view of the
7+
* Redux store, which will be fed to React components as `props`.
8+
*
9+
* A selector bundle looks like:
10+
*
11+
* {
12+
* selectorName: (state) => value,
13+
* ...
14+
* }
15+
*
16+
* , where each selector function should carry a `propType` property,
17+
* describing its result.
18+
*
19+
* The resulting super selector function looks like:
20+
*
21+
* state => {
22+
* selectorName: value,
23+
* ...
24+
* }
25+
*
26+
* , and has a `propTypes` property of the form:
27+
*
28+
* {
29+
* selectorName: propType,
30+
* ...
31+
* }
32+
*
33+
* This property can be merged directly into a `propTypes` or `contextTypes`
34+
* property on a React component.
35+
*
36+
* A bundle is typically created by importing an entire module of exported
37+
* selector functions. To keep track of React prop types, selector functions
38+
* should be annotated by assigning a `propType` property to the function
39+
* within the module where it is declared.
40+
*
41+
* @param {Object.<string, Function>} selectorBundles contains the
42+
* selectors, as explained above.
43+
* @return {Function} a function that, when given the store state, produces all
44+
* of the selector outputs.
45+
*/
46+
export function aggregateSelectors(bundle) {
47+
const combinedSelector = state => R.map(selectorFunction => selectorFunction(state), bundle);
48+
combinedSelector.propTypes = R.map(selectorFunction => selectorFunction.propType, bundle);
49+
return combinedSelector;
50+
}
51+
52+
/**
53+
* Does the opposite of [[aggregateSelectors]]
54+
*
55+
* @param {Function} superSelector
56+
* @return {Object.<string, Function>} a selector bundle, with each selector
57+
* annotated with a propType property.
58+
*/
59+
export function disaggregateSuperSelector(superSelector) {
60+
return R.mapObjIndexed((propType, propName) => {
61+
const singleSelector = R.pipe(superSelector, R.prop(propName));
62+
singleSelector.propType = propType;
63+
return singleSelector;
64+
}, superSelector.propTypes);
65+
}

0 commit comments

Comments
 (0)