Skip to content

Instantly share code, notes, and snippets.

@jimbolla
Created June 15, 2016 00:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jimbolla/e836cef98d7214fd2ce9627a47bb0943 to your computer and use it in GitHub Desktop.
Save jimbolla/e836cef98d7214fd2ce9627a47bb0943 to your computer and use it in GitHub Desktop.
import { Component, PropTypes, createElement } from 'react';
import {
createSelector,
createSelectorCreator,
createStructuredSelector,
defaultMemoize,
} from 'reselect';
import shallowEqual from 'appcenter/utils/shallowEqual';
const storeShape = PropTypes.shape({
subscribe: PropTypes.func.isRequired,
dispatch: PropTypes.func.isRequired,
getState: PropTypes.func.isRequired,
});
export { createSelector as createSelector };
export { createStructuredSelector as createStructuredSelector };
export const createShallowEqualSelector = createSelectorCreator(defaultMemoize, shallowEqual);
export const selectDispatch = (_, __, dispatch) => dispatch;
export function dispatchable(actionCreator, ...selectorsToPartiallyApply) {
if (selectorsToPartiallyApply.length === 0) {
return createSelector(
selectDispatch,
dispatch => (...args) => dispatch(actionCreator(...args))
);
}
return createSelector(
selectDispatch,
...selectorsToPartiallyApply,
(dispatch, ...partialArgs) => (...args) => dispatch(actionCreator(...partialArgs, ...args))
);
}
export default function connectToStore(
/*
this func is responsible for returning the selector function used to compute new props from
state, props, and dispatch. For example:
export default connectToStore(() => (state, props, dispatch) => ({
thing: state.things[props.thingId],
saveThing: fields => dispatch(actionCreators.saveThing(props.thingId, fields)),
}))(YourComponent)
Alternatively, it can return a plain object which will be passed to reselect's
'createStructuredSelector' function to create the selector. For example:
return connectToStore(() => ({
thing: (state, props) => state.things[props.thingId],
saveThing: (_, props, dispatch) => fields => (
dispatch(actionCreators.saveThing(props.thingId, fields))
),
}))(YourComponent)
This is equivalent to wrapping the returned object in a call to `createStructuredSelector`,
but is supported as a convenience; This is the recommended approach to defining your
selectorFactory methods. The above example can be simplfied by using the `dispatchable` helper
method provided with connectToStore:
connectToStore(() => ({
thing: (state, props) => state.things[props.thingId],
saveThing: dispatchable(actionCreators.saveThing, (_, props) => props.thingId),
}))(YourComponent)
A verbose but descriptive name for `dispatchable` would be `createBoundActionCreatorSelector`.
`dispatchable` will return a selector that binds the passed action creator arg to dispatch. Any
additional args given will be treated as selectors whose results should be partially applied to
the action creator.
*/
selectorFactory,
// options object:
{
// the func used to compute this HOC's displayName from the wrapped component's displayName.
getDisplayName = name => `connectToStore(${name})`,
// if true, shouldComponentUpdate will only be true of the selector recomputes for nextProps.
// if false, shouldComponentUpdate will always be true.
pure = true,
// the name of the property passed to the wrapped element indicating the number of.
// recomputations since it was mounted. useful for watching for unnecessary re-renders.
recomputationsProp = process.env.NODE_ENV !== 'production' ? '__recomputations' : null,
// if true, the props passed to this HOC are merged with the results of the selector; in the
// case of key collision, selector value is kept and prop is discared. if false, only the
// selector results are passed to the wrapped element.
shouldIncludeOriginalProps = true,
// if true, the selector receieves the current store state as the first arg, and this HOC
// subscribes to store changes. if false, null is passed as the first arg of selector.
shouldUseState = true,
// if true, the wrapped element is exposed by this HOC via the getWrappedInstance() function.
withRef = false,
} = {}
) {
function buildSelector(store) {
const factoryResult = selectorFactory();
const ref = withRef ? (c => { this.wrappedInstance = c; }) : undefined;
const selector = createShallowEqualSelector(
// original props selector:
shouldIncludeOriginalProps
? ((_, props) => props)
: (() => null),
// sourceSelector
typeof factoryResult === 'function'
? factoryResult
: createStructuredSelector(factoryResult),
// combine original props + selector props + ref
(props, sourceSelectorResults) => ({
...props,
...sourceSelectorResults,
ref,
}));
return function runSelector(props) {
const recomputationsBefore = selector.recomputations();
const storeState = shouldUseState ? store.getState() : null;
const selectorResults = selector(storeState, props, store.dispatch);
const recomputationsAfter = selector.recomputations();
const finalProps = recomputationsProp
? { ...selectorResults, [recomputationsProp]: recomputationsAfter }
: selectorResults;
return {
props: finalProps,
shouldUpdate: recomputationsBefore !== recomputationsAfter,
};
};
}
return function wrapWithConnect(WrappedComponent) {
class Connect extends Component {
componentWillMount() {
this.store = this.props.store || this.context.store;
this.selector = buildSelector.call(this, this.store);
this.trySubscribe();
}
shouldComponentUpdate(nextProps) {
return !pure || this.selector(nextProps).shouldUpdate;
}
componentWillUnmount() {
if (this.unsubscribe) this.unsubscribe();
this.unsubscribe = null;
this.selector = null;
this.store = null;
}
getWrappedInstance() {
return this.wrappedInstance;
}
trySubscribe() {
if (!shouldUseState || this.unsubscribe) return;
this.unsubscribe = this.store.subscribe(() => {
if (this.selector(this.props).shouldUpdate) this.forceUpdate();
});
}
render() {
const { props } = this.selector(this.props);
return createElement(WrappedComponent, props);
}
}
const wrappedComponentName = WrappedComponent.displayName
|| WrappedComponent.name
|| 'Component';
Connect.displayName = getDisplayName(wrappedComponentName);
Connect.WrappedComponent = WrappedComponent;
Connect.contextTypes = { store: storeShape };
Connect.propTypes = { store: storeShape };
return Connect;
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment