Skip to content

Start API change for connect decorator #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions src/components/createConnectDecorator.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import shallowEqualScalar from '../utils/shallowEqualScalar';
export default function createConnectDecorator(React, Connector) {
const { Component } = React;

return function connect(select) {
return function connect(slicer, actionCreators) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's change it to selector. We call them selectors in the Redux documentation, as well as in reselect API.

We can rename slice to selectedState.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Either way, slice isn't a public API so we may as well call it anything.)

return DecoratedComponent => class ConnectorDecorator extends Component {
static displayName = `Connector(${getDisplayName(DecoratedComponent)})`;
static DecoratedComponent = DecoratedComponent;
Expand All @@ -14,8 +14,17 @@ export default function createConnectDecorator(React, Connector) {
}

render() {
// Linter doesn't allow const slicerFn = slicer || () => ({});
let slicerFn = slicer;
if (slicerFn === null) {
slicerFn = () => ({});
}

return (
<Connector select={state => select(state, this.props)}>
<Connector
slicer={state => slicerFn(state, this.props)}
actionCreators={actionCreators}
>
{stuff => <DecoratedComponent {...stuff} {...this.props} />}
</Connector>
);
Expand Down
23 changes: 17 additions & 6 deletions src/components/createConnector.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { bindActionCreators } from 'redux';

import createStoreShape from '../utils/createStoreShape';
import shallowEqual from '../utils/shallowEqual';
import isPlainObject from '../utils/isPlainObject';
Expand All @@ -14,11 +16,11 @@ export default function createConnector(React) {

static propTypes = {
children: PropTypes.func.isRequired,
select: PropTypes.func.isRequired
slicer: PropTypes.func.isRequired
};

static defaultProps = {
select: state => state
slicer: state => state
};

shouldComponentUpdate(nextProps, nextState) {
Expand Down Expand Up @@ -47,7 +49,7 @@ export default function createConnector(React) {
}

componentWillReceiveProps(nextProps) {
if (nextProps.select !== this.props.select) {
if (nextProps.slicer !== this.props.slicer) {
// Force the state slice recalculation
this.handleChange(nextProps);
}
Expand All @@ -66,23 +68,32 @@ export default function createConnector(React) {

selectState(props, context) {
const state = context.store.getState();
const slice = props.select(state);
const slice = props.slicer(state);

invariant(
isPlainObject(slice),
'The return value of `select` prop must be an object. Instead received %s.',
'The return value of `slicer` prop must be an object. Instead received %s.',
slice
);

return { slice };
}

wrapActionCreators(dispatch) {
return typeof this.props.actionCreators === 'function'
? this.props.actionCreators(dispatch)
: bindActionCreators(this.props.actionCreators, dispatch);
}

render() {
const { children } = this.props;
const { slice } = this.state;
const { store: { dispatch } } = this.context;
const actions = this.props.actionCreators
? this.wrapActionCreators(dispatch)
: {};

return children({ dispatch, ...slice });
return children({ dispatch, ...slice, ...actions});
}
};
}
55 changes: 30 additions & 25 deletions test/components/Connector.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import expect from 'expect';
import jsdomReact from './jsdomReact';
import React, { PropTypes, Component } from 'react/addons';
import { createStore } from 'redux';
import { createStore, combineReducers } from 'redux';
import { Connector } from '../../src/index';

const { TestUtils } = React.addons;
Expand Down Expand Up @@ -32,7 +32,7 @@ describe('React', () => {
}

it('should receive the store in the context', () => {
const store = createStore({});
const store = createStore(combineReducers({}));

const tree = TestUtils.renderIntoDocument(
<Provider store={store}>
Expand All @@ -49,12 +49,13 @@ describe('React', () => {
});

it('should subscribe to the store changes', () => {
const store = createStore(stringBuilder);
const reducer = combineReducers({string: stringBuilder});
const store = createStore(reducer);

const tree = TestUtils.renderIntoDocument(
<Provider store={store}>
{() => (
<Connector select={string => ({ string })}>
<Connector slicer={state => ({ string: state.string })}>
{({ string }) => <div string={string} />}
</Connector>
)}
Expand All @@ -70,7 +71,8 @@ describe('React', () => {
});

it('should unsubscribe before unmounting', () => {
const store = createStore(stringBuilder);
const reducer = combineReducers({string: stringBuilder});
const store = createStore(reducer);
const subscribe = store.subscribe;

// Keep track of unsubscribe by wrapping subscribe()
Expand All @@ -86,7 +88,7 @@ describe('React', () => {
const tree = TestUtils.renderIntoDocument(
<Provider store={store}>
{() => (
<Connector select={string => ({ string })}>
<Connector slicer={state => ({ string: state.string })}>
{({ string }) => <div string={string} />}
</Connector>
)}
Expand All @@ -100,7 +102,8 @@ describe('React', () => {
});

it('should shallowly compare the selected state to prevent unnecessary updates', () => {
const store = createStore(stringBuilder);
const reducer = combineReducers({string: stringBuilder});
const store = createStore(reducer);
const spy = expect.createSpy(() => {});
function render({ string }) {
spy();
Expand All @@ -110,7 +113,7 @@ describe('React', () => {
const tree = TestUtils.renderIntoDocument(
<Provider store={store}>
{() => (
<Connector select={string => ({ string })}>
<Connector slicer={state => ({ string: state.string })}>
{render}
</Connector>
)}
Expand All @@ -129,10 +132,10 @@ describe('React', () => {
});

it('should recompute the state slice when the select prop changes', () => {
const store = createStore({
const store = createStore(combineReducers({
a: () => 42,
b: () => 72
});
}));

function selectA(state) {
return { result: state.a };
Expand All @@ -149,14 +152,14 @@ describe('React', () => {
class Container extends Component {
constructor() {
super();
this.state = { select: selectA };
this.state = { slicer: selectA };
}

render() {
return (
<Provider store={store}>
{() =>
<Connector select={this.state.select}>
<Connector slicer={this.state.slicer}>
{render}
</Connector>
}
Expand All @@ -169,12 +172,12 @@ describe('React', () => {
let div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div');
expect(div.props.children).toBe(42);

tree.setState({ select: selectB });
tree.setState({ slicer: selectB });
expect(div.props.children).toBe(72);
});

it('should pass dispatch() to the child function', () => {
const store = createStore({});
const store = createStore(combineReducers({}));

const tree = TestUtils.renderIntoDocument(
<Provider store={store}>
Expand All @@ -191,50 +194,51 @@ describe('React', () => {
});

it('should throw an error if select returns anything but a plain object', () => {
const store = createStore({});
const store = createStore(combineReducers({}));

expect(() => {
TestUtils.renderIntoDocument(
<Provider store={store}>
{() => (
<Connector select={() => 1}>
<Connector slicer={() => 1}>
{() => <div />}
</Connector>
)}
</Provider>
);
}).toThrow(/select/);
}).toThrow(/slicer/);

expect(() => {
TestUtils.renderIntoDocument(
<Provider store={store}>
{() => (
<Connector select={() => 'hey'}>
<Connector slicer={() => 'hey'}>
{() => <div />}
</Connector>
)}
</Provider>
);
}).toThrow(/select/);
}).toThrow(/slicer/);

function AwesomeMap() { }

expect(() => {
TestUtils.renderIntoDocument(
<Provider store={store}>
{() => (
<Connector select={() => new AwesomeMap()}>
<Connector slicer={() => new AwesomeMap()}>
{() => <div />}
</Connector>
)}
</Provider>
);
}).toThrow(/select/);
}).toThrow(/slicer/);
});

it('should not setState when renderToString is called on the server', () => {
const { renderToString } = React;
const store = createStore(stringBuilder);
const reducer = combineReducers({string: stringBuilder});
const store = createStore(reducer);

class TestComp extends Component {
componentWillMount() {
Expand All @@ -252,7 +256,7 @@ describe('React', () => {
const el = (
<Provider store={store}>
{() => (
<Connector select={string => ({ string })}>
<Connector slicer={state => ({ string: state.string })}>
{({ string }) => <TestComp string={string} />}
</Connector>
)}
Expand All @@ -263,7 +267,8 @@ describe('React', () => {
});

it('should handle dispatch inside componentDidMount', () => {
const store = createStore(stringBuilder);
const reducer = combineReducers({string: stringBuilder});
const store = createStore(reducer);

class TestComp extends Component {
componentDidMount() {
Expand All @@ -281,7 +286,7 @@ describe('React', () => {
const tree = TestUtils.renderIntoDocument(
<Provider store={store}>
{() => (
<Connector select={string => ({ string })}>
<Connector slicer={state => ({ string: state.string })}>
{({ string }) => <TestComp string={string} />}
</Connector>
)}
Expand Down
4 changes: 2 additions & 2 deletions test/components/Provider.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import expect from 'expect';
import jsdomReact from './jsdomReact';
import React, { PropTypes, Component } from 'react/addons';
import { createStore } from 'redux';
import { createStore, combineReducers } from 'redux';
import { Provider } from '../../src/index';

const { TestUtils } = React.addons;
Expand All @@ -21,7 +21,7 @@ describe('React', () => {
}

it('should add the store to the child context', () => {
const store = createStore({});
const store = createStore(combineReducers({}));

const tree = TestUtils.renderIntoDocument(
<Provider store={store}>
Expand Down
Loading