From f6ef06eef8bd0b71230cca3e04e4aa53cc482896 Mon Sep 17 00:00:00 2001 From: gnoff Date: Thu, 30 Jul 2015 01:02:44 -0700 Subject: [PATCH 01/31] WIP connectWrapper that will eventually replace connectDecorator and Connector --- src/components/createAll.js | 8 +- src/components/createConnectWrapper.js | 140 ++++++++++++++++++++ src/index.js | 2 +- src/native.js | 2 +- src/utils/wrapActionCreators.js | 5 + test/components/Connector.spec.js | 16 +-- test/components/Provider.spec.js | 4 +- test/components/connect.spec.js | 4 +- test/components/connectDecorator.spec.js | 156 +++++++++++++++++++++++ test/components/provide.spec.js | 4 +- 10 files changed, 323 insertions(+), 18 deletions(-) create mode 100644 src/components/createConnectWrapper.js create mode 100644 src/utils/wrapActionCreators.js create mode 100644 test/components/connectDecorator.spec.js diff --git a/src/components/createAll.js b/src/components/createAll.js index bd6383ccc..43cda3bfe 100644 --- a/src/components/createAll.js +++ b/src/components/createAll.js @@ -3,6 +3,7 @@ import createProvideDecorator from './createProvideDecorator'; import createConnector from './createConnector'; import createConnectDecorator from './createConnectDecorator'; +import createConnectWrapper from './createConnectWrapper'; export default function createAll(React) { // Wrapper components @@ -11,7 +12,10 @@ export default function createAll(React) { // Higher-order components (decorators) const provide = createProvideDecorator(React, Provider); - const connect = createConnectDecorator(React, Connector); + const connectDecorate = createConnectDecorator(React, Connector); + const connect = createConnectWrapper(React); - return { Provider, Connector, provide, connect }; + + + return { Provider, Connector, provide, connectDecorate, connect }; } diff --git a/src/components/createConnectWrapper.js b/src/components/createConnectWrapper.js new file mode 100644 index 000000000..80d6d6f45 --- /dev/null +++ b/src/components/createConnectWrapper.js @@ -0,0 +1,140 @@ +import createStoreShape from '../utils/createStoreShape'; +import getDisplayName from '../utils/getDisplayName'; +import shallowEqual from '../utils/shallowEqual'; +import isPlainObject from '../utils/isPlainObject'; +import wrapActionCreators from '../utils/wrapActionCreators'; +import invariant from 'invariant'; + +const emptySelector = () => ({}); + +const emptyBinder = () => ({}); + +const identityMerge = (slice, actionsCreators, props) => ({...slice, ...actionsCreators, ...props}); + + +export default function createConnectWrapper(React) { + const { Component, PropTypes } = React; + const storeShape = createStoreShape(PropTypes); + + return function connect(select, bindActionCreators, merge) { + + const subscribing = select ? true : false; + + select = select || emptySelector; + + bindActionCreators = bindActionCreators || emptyBinder; + + if (isPlainObject(bindActionCreators)) { + bindActionCreators = wrapActionCreators(bindActionCreators); + } + + merge = merge || identityMerge; + + return DecoratedComponent => class ConnectWrapper extends Component { + static displayName = `ConnectWrapper(${getDisplayName(DecoratedComponent)})`; + static DecoratedComponent = DecoratedComponent; + + static contextTypes = { + store: storeShape.isRequired + }; + + componentWillReceiveProps(nextProps) { + console.log('recieving props', this.props, nextProps) + } + + shouldComponentUpdate(nextProps, nextState) { + console.log('shallowEqual of props', shallowEqual(this.props, nextProps), this.props, nextProps) + return (this.subscribed && !this.isSliceEqual(this.state.slice, nextState.slice)) || + !shallowEqual(this.props, nextProps); + } + + isSliceEqual(slice, nextSlice) { + const isRefEqual = slice === nextSlice; + if (isRefEqual) { + return true; + } else if (typeof slice !== 'object' || typeof nextSlice !== 'object') { + return isRefEqual; + } + return shallowEqual(slice, nextSlice); + } + + constructor(props, context) { + super(props, context); + this.state = { + ...this.selectState(props, context), + ...this.bindActionCreators(context), + }; + } + + componentWillMount() { + console.log('will mount', this.props) + } + + componentDidMount() { + console.log('mounted', this.props) + if (subscribing) { + this.subscribed = true; + this.unsubscribe = this.context.store.subscribe(::this.handleChange); + } + } + + componentWillUnmount() { + if (subscribing) { + this.unsubscribe(); + } + } + + handleChange(props = this.props) { + const nextState = this.selectState(props, this.context); + if (!this.isSliceEqual(this.state.slice, nextState.slice)) { + this.setState(nextState); + } + } + + selectState(props = this.props, context = this.context) { + const state = context.store.getState(); + const slice = select(state); + + invariant( + isPlainObject(slice), + 'The return value of `select` prop must be an object. Instead received %s.', + slice + ); + + return { slice }; + } + + bindActionCreators(context = this.context) { + const { dispatch } = context.store; + const actionCreators = bindActionCreators(dispatch); + + invariant( + isPlainObject(actionCreators), + 'The return value of `bindActionCreators` prop must be an object. Instead received %s.', + actionCreators + ); + + return { actionCreators }; + } + + merge(props = this.props, state = this.state) { + const { slice, actionCreators } = state; + const merged = merge(slice, actionCreators, props); + + invariant( + isPlainObject(merged), + 'The return value of `merge` prop must be an object. Instead received %s.', + merged + ); + + console.log('merging with ', merged) + + return merged; + } + + render() { + return ; + } + }; + }; +} diff --git a/src/index.js b/src/index.js index a2058d695..3ac9aeeb7 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ import React from 'react'; import createAll from './components/createAll'; -export const { Provider, Connector, provide, connect } = createAll(React); +export const { Provider, Connector, provide, connectDecorate, connect } = createAll(React); diff --git a/src/native.js b/src/native.js index c6fc5363e..b7e1fe8d1 100644 --- a/src/native.js +++ b/src/native.js @@ -1,4 +1,4 @@ import React from 'react-native'; import createAll from './components/createAll'; -export const { Provider, Connector, provide, connect } = createAll(React); +export const { Provider, Connector, provide, connectDecorate, connect } = createAll(React); diff --git a/src/utils/wrapActionCreators.js b/src/utils/wrapActionCreators.js new file mode 100644 index 000000000..4d186f7b5 --- /dev/null +++ b/src/utils/wrapActionCreators.js @@ -0,0 +1,5 @@ +import { bindActionCreators } from 'redux' + +export default function wrapActionCreators (actionCreators) { + return dispatch => bindActionCreators(actionCreators, dispatch); +} \ No newline at end of file diff --git a/test/components/Connector.spec.js b/test/components/Connector.spec.js index bb4cef68b..6d386de82 100644 --- a/test/components/Connector.spec.js +++ b/test/components/Connector.spec.js @@ -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; @@ -32,7 +32,7 @@ describe('React', () => { } it('should receive the store in the context', () => { - const store = createStore({}); + const store = createStore(() => ({})); const tree = TestUtils.renderIntoDocument( @@ -74,7 +74,7 @@ describe('React', () => { const subscribe = store.subscribe; // Keep track of unsubscribe by wrapping subscribe() - const spy = expect.createSpy(() => {}); + const spy = expect.createSpy(() => ({})); store.subscribe = (listener) => { const unsubscribe = subscribe(listener); return () => { @@ -101,7 +101,7 @@ describe('React', () => { it('should shallowly compare the selected state to prevent unnecessary updates', () => { const store = createStore(stringBuilder); - const spy = expect.createSpy(() => {}); + const spy = expect.createSpy(() => ({})); function render({ string }) { spy(); return
; @@ -129,10 +129,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 }; @@ -174,7 +174,7 @@ describe('React', () => { }); it('should pass dispatch() to the child function', () => { - const store = createStore({}); + const store = createStore(() => ({})); const tree = TestUtils.renderIntoDocument( @@ -191,7 +191,7 @@ describe('React', () => { }); it('should throw an error if select returns anything but a plain object', () => { - const store = createStore({}); + const store = createStore(() => ({})); expect(() => { TestUtils.renderIntoDocument( diff --git a/test/components/Provider.spec.js b/test/components/Provider.spec.js index d0daf08f1..a85f1b2b7 100644 --- a/test/components/Provider.spec.js +++ b/test/components/Provider.spec.js @@ -21,7 +21,7 @@ describe('React', () => { } it('should add the store to the child context', () => { - const store = createStore({}); + const store = createStore(() => ({})); const tree = TestUtils.renderIntoDocument( @@ -36,7 +36,7 @@ describe('React', () => { it('should replace just the reducer when receiving a new store in props', () => { const store1 = createStore((state = 10) => state + 1); const store2 = createStore((state = 10) => state * 2); - const spy = expect.createSpy(() => {}); + const spy = expect.createSpy(() => ({})); class ProviderContainer extends Component { state = { store: store1 }; diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 743881097..4cdcfd452 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -2,7 +2,7 @@ import expect from 'expect'; import jsdomReact from './jsdomReact'; import React, { PropTypes, Component } from 'react/addons'; import { createStore } from 'redux'; -import { connect, Connector } from '../../src/index'; +import { connect } from '../../src/index'; const { TestUtils } = React.addons; @@ -46,7 +46,7 @@ describe('React', () => { expect(div.props.pass).toEqual('through'); expect(div.props.foo).toEqual('bar'); expect(() => - TestUtils.findRenderedComponentWithType(container, Connector) + TestUtils.findRenderedComponentWithType(container, Container) ).toNotThrow(); }); diff --git a/test/components/connectDecorator.spec.js b/test/components/connectDecorator.spec.js new file mode 100644 index 000000000..47b7a1063 --- /dev/null +++ b/test/components/connectDecorator.spec.js @@ -0,0 +1,156 @@ +import expect from 'expect'; +import jsdomReact from './jsdomReact'; +import React, { PropTypes, Component } from 'react/addons'; +import { createStore } from 'redux'; +import { connectDecorate, Connector } from '../../src/index'; + +const connect = connectDecorate; + +const { TestUtils } = React.addons; + +describe('React', () => { + describe('connectDecorate', () => { + jsdomReact(); + + // Mock minimal Provider interface + class Provider extends Component { + static childContextTypes = { + store: PropTypes.object.isRequired + } + + getChildContext() { + return { store: this.props.store }; + } + + render() { + return this.props.children(); + } + } + + it('should wrap the component into Provider', () => { + const store = createStore(() => ({ + foo: 'bar' + })); + + @connect(state => state) + class Container extends Component { + render() { + return
; + } + } + + const container = TestUtils.renderIntoDocument( + + {() => } + + ); + const div = TestUtils.findRenderedDOMComponentWithTag(container, 'div'); + expect(div.props.pass).toEqual('through'); + expect(div.props.foo).toEqual('bar'); + expect(() => + TestUtils.findRenderedComponentWithType(container, Connector) + ).toNotThrow(); + }); + + it('should handle additional prop changes in addition to slice', () => { + const store = createStore(() => ({ + foo: 'bar' + })); + + @connect(state => state) + class ConnectContainer extends Component { + render() { + return ( +
+ ); + } + } + + class Container extends Component { + constructor() { + super(); + this.state = { + bar: { + baz: '' + } + }; + } + componentDidMount() { + + // Simulate deep object mutation + this.state.bar.baz = 'through'; + this.setState({ + bar: this.state.bar + }); + } + render() { + return ( + + {() => } + + ); + } + } + + const container = TestUtils.renderIntoDocument(); + const div = TestUtils.findRenderedDOMComponentWithTag(container, 'div'); + expect(div.props.foo).toEqual('bar'); + expect(div.props.pass).toEqual('through'); + }); + + it('should pass the only argument as the select prop down', () => { + const store = createStore(() => ({ + foo: 'baz', + bar: 'baz' + })); + + function select({ foo }) { + return { foo }; + } + + @connect(select) + class Container extends Component { + render() { + return
; + } + } + + const container = TestUtils.renderIntoDocument( + + {() => } + + ); + const connector = TestUtils.findRenderedComponentWithType(container, Connector); + expect(connector.props.select({ + foo: 5, + bar: 7 + })).toEqual({ + foo: 5 + }); + }); + + it('should set the displayName correctly', () => { + @connect(state => state) + class Container extends Component { + render() { + return
; + } + } + + expect(Container.displayName).toBe('Connector(Container)'); + }); + + it('should expose the wrapped component as DecoratedComponent', () => { + class Container extends Component { + render() { + return
; + } + } + + const decorator = connect(state => state); + const decorated = decorator(Container); + + expect(decorated.DecoratedComponent).toBe(Container); + }); + }); +}); diff --git a/test/components/provide.spec.js b/test/components/provide.spec.js index add035f15..ea328d9a4 100644 --- a/test/components/provide.spec.js +++ b/test/components/provide.spec.js @@ -21,7 +21,7 @@ describe('React', () => { } it('should wrap the component into Provider', () => { - const store = createStore({}); + const store = createStore(() => ({})); @provide(store) class Container extends Component { @@ -42,7 +42,7 @@ describe('React', () => { }); it('sets the displayName correctly', () => { - @provide(createStore({})) + @provide(createStore(() => ({}))) class Container extends Component { render() { return
; From 063777a5d3c79db01c13838b5310d4c978268a0f Mon Sep 17 00:00:00 2001 From: gnoff Date: Thu, 30 Jul 2015 23:22:45 -0700 Subject: [PATCH 02/31] intermediate commit. old decorator and connector are still present --- src/components/createAll.js | 10 +- src/components/createConnectDecorator.js | 121 +++++- src/components/createConnectDecoratorOld.js | 25 ++ src/components/createConnectWrapper.js | 140 ------- src/index.js | 2 +- src/native.js | 2 +- src/utils/wrapActionCreators.js | 6 +- test/components/connect.spec.js | 394 +++++++++++++++++- ...or.spec.js => connectDecoratorOld.spec.js} | 4 +- test/utils/wrapActionCreators.js | 31 ++ 10 files changed, 555 insertions(+), 180 deletions(-) create mode 100644 src/components/createConnectDecoratorOld.js delete mode 100644 src/components/createConnectWrapper.js rename test/components/{connectDecorator.spec.js => connectDecoratorOld.spec.js} (97%) create mode 100644 test/utils/wrapActionCreators.js diff --git a/src/components/createAll.js b/src/components/createAll.js index 43cda3bfe..4dba1c179 100644 --- a/src/components/createAll.js +++ b/src/components/createAll.js @@ -2,8 +2,8 @@ import createProvider from './createProvider'; import createProvideDecorator from './createProvideDecorator'; import createConnector from './createConnector'; +import createConnectDecoratorOld from './createConnectDecoratorOld'; import createConnectDecorator from './createConnectDecorator'; -import createConnectWrapper from './createConnectWrapper'; export default function createAll(React) { // Wrapper components @@ -12,10 +12,8 @@ export default function createAll(React) { // Higher-order components (decorators) const provide = createProvideDecorator(React, Provider); - const connectDecorate = createConnectDecorator(React, Connector); - const connect = createConnectWrapper(React); + const connectDecoratorOld = createConnectDecoratorOld(React, Connector); + const connect = createConnectDecorator(React); - - - return { Provider, Connector, provide, connectDecorate, connect }; + return { Provider, Connector, provide, connect, connectDecoratorOld }; } diff --git a/src/components/createConnectDecorator.js b/src/components/createConnectDecorator.js index a3196e1c1..a12a1bdf4 100644 --- a/src/components/createConnectDecorator.js +++ b/src/components/createConnectDecorator.js @@ -1,24 +1,121 @@ +import createStoreShape from '../utils/createStoreShape'; import getDisplayName from '../utils/getDisplayName'; import shallowEqualScalar from '../utils/shallowEqualScalar'; +import shallowEqual from '../utils/shallowEqual'; +import isPlainObject from '../utils/isPlainObject'; +import wrapActionCreators from '../utils/wrapActionCreators'; +import invariant from 'invariant'; -export default function createConnectDecorator(React, Connector) { - const { Component } = React; +const emptySelector = () => ({}); - return function connect(select) { - return DecoratedComponent => class ConnectorDecorator extends Component { - static displayName = `Connector(${getDisplayName(DecoratedComponent)})`; +const emptyBinder = () => ({}); + +const identityMerge = (slice, actionsCreators, props) => ({...slice, ...actionsCreators, ...props}); + + +export default function createConnectDecorator(React) { + const { Component, PropTypes } = React; + const storeShape = createStoreShape(PropTypes); + + return function connect(select, dispatchBinder = emptyBinder, mergeHandler = identityMerge) { + + const subscribing = select ? true : false; + const selectState = select || emptySelector; + const bindActionCreators = isPlainObject(dispatchBinder) ? wrapActionCreators(dispatchBinder) : dispatchBinder; + const merge = mergeHandler; + + return DecoratedComponent => class ConnectDecorator extends Component { + static displayName = `ConnectDecorator(${getDisplayName(DecoratedComponent)})`; static DecoratedComponent = DecoratedComponent; - shouldComponentUpdate(nextProps) { - return !shallowEqualScalar(this.props, nextProps); + static contextTypes = { + store: storeShape.isRequired + }; + + shouldComponentUpdate(nextProps, nextState) { + return (this.subscribed && !this.isSliceEqual(this.state.slice, nextState.slice)) || + !shallowEqualScalar(this.props, nextProps); } - render() { - return ( - select(state, this.props)}> - {stuff => } - + isSliceEqual(slice, nextSlice) { + const isRefEqual = slice === nextSlice; + if (isRefEqual) { + return true; + } else if (typeof slice !== 'object' || typeof nextSlice !== 'object') { + return isRefEqual; + } + return shallowEqual(slice, nextSlice); + } + + constructor(props, context) { + super(props, context); + this.state = { + ...this.selectState(props, context), + ...this.bindActionCreators(context) + }; + } + + componentDidMount() { + if (subscribing) { + this.subscribed = true; + this.unsubscribe = this.context.store.subscribe(::this.handleChange); + } + } + + componentWillUnmount() { + if (subscribing) { + this.unsubscribe(); + } + } + + handleChange(props = this.props) { + const nextState = this.selectState(props, this.context); + if (!this.isSliceEqual(this.state.slice, nextState.slice)) { + this.setState(nextState); + } + } + + selectState(props = this.props, context = this.context) { + const state = context.store.getState(); + const slice = selectState(state); + + invariant( + isPlainObject(slice), + 'The return value of `select` prop must be an object. Instead received %s.', + slice + ); + + return { slice }; + } + + bindActionCreators(context = this.context) { + const { dispatch } = context.store; + const actionCreators = bindActionCreators(dispatch); + + invariant( + isPlainObject(actionCreators), + 'The return value of `bindActionCreators` prop must be an object. Instead received %s.', + actionCreators + ); + + return { actionCreators }; + } + + merge(props = this.props, state = this.state) { + const { slice, actionCreators } = state; + const merged = merge(slice, actionCreators, props); + + invariant( + isPlainObject(merged), + 'The return value of `merge` prop must be an object. Instead received %s.', + merged ); + + return merged; + } + + render() { + return ; } }; }; diff --git a/src/components/createConnectDecoratorOld.js b/src/components/createConnectDecoratorOld.js new file mode 100644 index 000000000..a3196e1c1 --- /dev/null +++ b/src/components/createConnectDecoratorOld.js @@ -0,0 +1,25 @@ +import getDisplayName from '../utils/getDisplayName'; +import shallowEqualScalar from '../utils/shallowEqualScalar'; + +export default function createConnectDecorator(React, Connector) { + const { Component } = React; + + return function connect(select) { + return DecoratedComponent => class ConnectorDecorator extends Component { + static displayName = `Connector(${getDisplayName(DecoratedComponent)})`; + static DecoratedComponent = DecoratedComponent; + + shouldComponentUpdate(nextProps) { + return !shallowEqualScalar(this.props, nextProps); + } + + render() { + return ( + select(state, this.props)}> + {stuff => } + + ); + } + }; + }; +} diff --git a/src/components/createConnectWrapper.js b/src/components/createConnectWrapper.js deleted file mode 100644 index 80d6d6f45..000000000 --- a/src/components/createConnectWrapper.js +++ /dev/null @@ -1,140 +0,0 @@ -import createStoreShape from '../utils/createStoreShape'; -import getDisplayName from '../utils/getDisplayName'; -import shallowEqual from '../utils/shallowEqual'; -import isPlainObject from '../utils/isPlainObject'; -import wrapActionCreators from '../utils/wrapActionCreators'; -import invariant from 'invariant'; - -const emptySelector = () => ({}); - -const emptyBinder = () => ({}); - -const identityMerge = (slice, actionsCreators, props) => ({...slice, ...actionsCreators, ...props}); - - -export default function createConnectWrapper(React) { - const { Component, PropTypes } = React; - const storeShape = createStoreShape(PropTypes); - - return function connect(select, bindActionCreators, merge) { - - const subscribing = select ? true : false; - - select = select || emptySelector; - - bindActionCreators = bindActionCreators || emptyBinder; - - if (isPlainObject(bindActionCreators)) { - bindActionCreators = wrapActionCreators(bindActionCreators); - } - - merge = merge || identityMerge; - - return DecoratedComponent => class ConnectWrapper extends Component { - static displayName = `ConnectWrapper(${getDisplayName(DecoratedComponent)})`; - static DecoratedComponent = DecoratedComponent; - - static contextTypes = { - store: storeShape.isRequired - }; - - componentWillReceiveProps(nextProps) { - console.log('recieving props', this.props, nextProps) - } - - shouldComponentUpdate(nextProps, nextState) { - console.log('shallowEqual of props', shallowEqual(this.props, nextProps), this.props, nextProps) - return (this.subscribed && !this.isSliceEqual(this.state.slice, nextState.slice)) || - !shallowEqual(this.props, nextProps); - } - - isSliceEqual(slice, nextSlice) { - const isRefEqual = slice === nextSlice; - if (isRefEqual) { - return true; - } else if (typeof slice !== 'object' || typeof nextSlice !== 'object') { - return isRefEqual; - } - return shallowEqual(slice, nextSlice); - } - - constructor(props, context) { - super(props, context); - this.state = { - ...this.selectState(props, context), - ...this.bindActionCreators(context), - }; - } - - componentWillMount() { - console.log('will mount', this.props) - } - - componentDidMount() { - console.log('mounted', this.props) - if (subscribing) { - this.subscribed = true; - this.unsubscribe = this.context.store.subscribe(::this.handleChange); - } - } - - componentWillUnmount() { - if (subscribing) { - this.unsubscribe(); - } - } - - handleChange(props = this.props) { - const nextState = this.selectState(props, this.context); - if (!this.isSliceEqual(this.state.slice, nextState.slice)) { - this.setState(nextState); - } - } - - selectState(props = this.props, context = this.context) { - const state = context.store.getState(); - const slice = select(state); - - invariant( - isPlainObject(slice), - 'The return value of `select` prop must be an object. Instead received %s.', - slice - ); - - return { slice }; - } - - bindActionCreators(context = this.context) { - const { dispatch } = context.store; - const actionCreators = bindActionCreators(dispatch); - - invariant( - isPlainObject(actionCreators), - 'The return value of `bindActionCreators` prop must be an object. Instead received %s.', - actionCreators - ); - - return { actionCreators }; - } - - merge(props = this.props, state = this.state) { - const { slice, actionCreators } = state; - const merged = merge(slice, actionCreators, props); - - invariant( - isPlainObject(merged), - 'The return value of `merge` prop must be an object. Instead received %s.', - merged - ); - - console.log('merging with ', merged) - - return merged; - } - - render() { - return ; - } - }; - }; -} diff --git a/src/index.js b/src/index.js index 3ac9aeeb7..3990fe043 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ import React from 'react'; import createAll from './components/createAll'; -export const { Provider, Connector, provide, connectDecorate, connect } = createAll(React); +export const { Provider, Connector, provide, connect, connectDecoratorOld } = createAll(React); diff --git a/src/native.js b/src/native.js index b7e1fe8d1..7fc78db8a 100644 --- a/src/native.js +++ b/src/native.js @@ -1,4 +1,4 @@ import React from 'react-native'; import createAll from './components/createAll'; -export const { Provider, Connector, provide, connectDecorate, connect } = createAll(React); +export const { Provider, Connector, provide, connect, connectDecoratorOld } = createAll(React); diff --git a/src/utils/wrapActionCreators.js b/src/utils/wrapActionCreators.js index 4d186f7b5..983fbe606 100644 --- a/src/utils/wrapActionCreators.js +++ b/src/utils/wrapActionCreators.js @@ -1,5 +1,5 @@ -import { bindActionCreators } from 'redux' +import { bindActionCreators } from 'redux'; -export default function wrapActionCreators (actionCreators) { +export default function wrapActionCreators(actionCreators) { return dispatch => bindActionCreators(actionCreators, dispatch); -} \ No newline at end of file +} diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 4cdcfd452..5938d99c3 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -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 { connect } from '../../src/index'; const { TestUtils } = React.addons; @@ -25,6 +25,34 @@ describe('React', () => { } } + function stringBuilder(prev = '', action) { + return action.type === 'APPEND' + ? prev + action.body + : prev; + } + + it('should receive the store in the context', () => { + const store = createStore(() => ({})); + + @connect() + class Container extends Component { + render() { + return
; + } + } + + const tree = TestUtils.renderIntoDocument( + + {() => ( + + )} + + ); + + const container = TestUtils.findRenderedComponentWithType(tree, Container); + expect(container.context.store).toBe(store); + }); + it('should wrap the component into Provider', () => { const store = createStore(() => ({ foo: 'bar' @@ -50,6 +78,33 @@ describe('React', () => { ).toNotThrow(); }); + it('should subscribe to the store changes', () => { + const store = createStore(stringBuilder); + + @connect(state => ({string: state}) ) + class Container extends Component { + render() { + return
; + } + } + + const tree = TestUtils.renderIntoDocument( + + {() => ( + + )} + + ); + + const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); + + expect(div.props.string).toBe(''); + store.dispatch({ type: 'APPEND', body: 'a'}); + expect(div.props.string).toBe('a'); + store.dispatch({ type: 'APPEND', body: 'b'}); + expect(div.props.string).toBe('ab'); + }); + it('should handle additional prop changes in addition to slice', () => { const store = createStore(() => ({ foo: 'bar' @@ -96,17 +151,101 @@ describe('React', () => { expect(div.props.pass).toEqual('through'); }); - it('should pass the only argument as the select prop down', () => { + it('should allow for merge to incorporate state and prop changes', () => { + const store = createStore(stringBuilder); + + function doSomething(thing) { + return { + type: 'APPEND', + body: thing + }; + } + + @connect( + state => ({stateThing: state}), + dispatch => ({doSomething: (whatever) => dispatch(doSomething(whatever)) }), + (stateProps, actionProps, parentProps) => ({ + ...stateProps, + ...actionProps, + mergedDoSomething: (thing) => { + const seed = stateProps.stateThing === '' ? 'HELLO ' : ''; + actionProps.doSomething(seed + thing + parentProps.extra); + } + }) + ) + class Container extends Component { + render() { + return
; + }; + } + + class OuterContainer extends Component { + constructor() { + super(); + this.state = { extra: 'z' }; + } + + render() { + return ( + + {() => } + + ); + } + } + + const tree = TestUtils.renderIntoDocument(); + const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); + + expect(div.props.stateThing).toBe(''); + div.props.mergedDoSomething('a'); + expect(div.props.stateThing).toBe('HELLO az'); + div.props.mergedDoSomething('b'); + expect(div.props.stateThing).toBe('HELLO azbz'); + tree.setState({extra: 'Z'}); + div.props.mergedDoSomething('c'); + expect(div.props.stateThing).toBe('HELLO azbzcZ'); + }); + + it('should merge actionProps into DecoratedComponent', () => { const store = createStore(() => ({ - foo: 'baz', - bar: 'baz' + foo: 'bar' })); - function select({ foo }) { - return { foo }; + @connect( + state => state, + dispatch => ({ dispatch }) + ) + class Container extends Component { + render() { + return
; + } } - @connect(select) + const container = TestUtils.renderIntoDocument( + + {() => } + + ); + const div = TestUtils.findRenderedDOMComponentWithTag(container, 'div'); + expect(div.props.dispatch).toEqual(store.dispatch); + expect(div.props.foo).toEqual('bar'); + expect(() => + TestUtils.findRenderedComponentWithType(container, Container) + ).toNotThrow(); + const decorated = TestUtils.findRenderedComponentWithType(container, Container); + expect(decorated.subscribed).toBe(true); + }); + + it('should not subscribe to stores if select argument is null', () => { + const store = createStore(() => ({ + foo: 'bar' + })); + + @connect( + null, + dispatch => ({ dispatch }) + ) class Container extends Component { render() { return
; @@ -118,13 +257,238 @@ describe('React', () => { {() => } ); - const connector = TestUtils.findRenderedComponentWithType(container, Connector); - expect(connector.props.select({ - foo: 5, - bar: 7 - })).toEqual({ - foo: 5 - }); + const div = TestUtils.findRenderedDOMComponentWithTag(container, 'div'); + expect(div.props.dispatch).toEqual(store.dispatch); + expect(div.props.foo).toBe(undefined); + expect(() => + TestUtils.findRenderedComponentWithType(container, Container) + ).toNotThrow(); + const decorated = TestUtils.findRenderedComponentWithType(container, Container); + expect(decorated.subscribed).toNotBe(true); + + }); + + it('should unsubscribe before unmounting', () => { + const store = createStore(stringBuilder); + const subscribe = store.subscribe; + + // Keep track of unsubscribe by wrapping subscribe() + const spy = expect.createSpy(() => ({})); + store.subscribe = (listener) => { + const unsubscribe = subscribe(listener); + return () => { + spy(); + return unsubscribe(); + }; + }; + + @connect( + state => ({string: state}), + dispatch => ({ dispatch }) + ) + class Container extends Component { + render() { + return
; + } + } + + const tree = TestUtils.renderIntoDocument( + + {() => ( + + )} + + ); + + const connector = TestUtils.findRenderedComponentWithType(tree, Container); + expect(spy.calls.length).toBe(0); + connector.componentWillUnmount(); + expect(spy.calls.length).toBe(1); + }); + + it('should shallowly compare the selected state to prevent unnecessary updates', () => { + const store = createStore(stringBuilder); + const spy = expect.createSpy(() => ({})); + function render({ string }) { + spy(); + return
; + } + + @connect( + state => ({string: state}), + dispatch => ({ dispatch }) + ) + class Container extends Component { + render() { + return render(this.props); + } + } + + const tree = TestUtils.renderIntoDocument( + + {() => ( + + )} + + ); + + const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); + expect(spy.calls.length).toBe(1); + expect(div.props.string).toBe(''); + store.dispatch({ type: 'APPEND', body: 'a'}); + expect(spy.calls.length).toBe(2); + store.dispatch({ type: 'APPEND', body: 'b'}); + expect(spy.calls.length).toBe(3); + store.dispatch({ type: 'APPEND', body: ''}); + expect(spy.calls.length).toBe(3); + }); + + it('should recompute the state slice when the select prop changes', () => { + const store = createStore(combineReducers({ + a: () => 42, + b: () => 72 + })); + + function selectA(state) { + return { result: state.a }; + } + + function selectB(state) { + return { result: state.b }; + } + + function render({ result }) { + return
{result}
; + } + + function getContainer(select) { + return ( + @connect(select) + class Container extends Component { + render() { + return this.props.children(this.props); + } + } + ); + } + + class OuterContainer extends Component { + constructor() { + super(); + this.state = { select: selectA }; + } + + render() { + return ( + + {() => { + const Container = getContainer(this.state.select); + return ( + + {render} + + ); + }} + + ); + } + } + + let tree = TestUtils.renderIntoDocument(); + let div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); + expect(div.props.children).toBe(42); + + tree.setState({ select: selectB }); + expect(div.props.children).toBe(72); + }); + + it('should throw an error if select, bindActionCreators, or merge returns anything but a plain object', () => { + const store = createStore(() => ({})); + + function makeContainer(select, bindActionCreators, merge) { + return React.createElement( + @connect(select, bindActionCreators, merge) + class Container extends Component { + render() { + return
; + } + } + ); + } + + function AwesomeMap() { } + + expect(() => { + TestUtils.renderIntoDocument( + + { () => makeContainer(() => 1, () => ({}), () => ({})) } + + ); + }).toThrow(/select/); + + expect(() => { + TestUtils.renderIntoDocument( + + { () => makeContainer(() => 'hey', () => ({}), () => ({})) } + + ); + }).toThrow(/select/); + + expect(() => { + TestUtils.renderIntoDocument( + + { () => makeContainer(() => new AwesomeMap(), () => ({}), () => ({})) } + + ); + }).toThrow(/select/); + + expect(() => { + TestUtils.renderIntoDocument( + + { () => makeContainer(() => ({}), () => 1, () => ({})) } + + ); + }).toThrow(/bindActionCreators/); + + expect(() => { + TestUtils.renderIntoDocument( + + { () => makeContainer(() => ({}), () => 'hey', () => ({})) } + + ); + }).toThrow(/bindActionCreators/); + + expect(() => { + TestUtils.renderIntoDocument( + + { () => makeContainer(() => ({}), () => new AwesomeMap(), () => ({})) } + + ); + }).toThrow(/bindActionCreators/); + + expect(() => { + TestUtils.renderIntoDocument( + + { () => makeContainer(() => ({}), () => ({}), () => 1) } + + ); + }).toThrow(/merge/); + + expect(() => { + TestUtils.renderIntoDocument( + + { () => makeContainer(() => ({}), () => ({}), () => 'hey') } + + ); + }).toThrow(/merge/); + + expect(() => { + TestUtils.renderIntoDocument( + + { () => makeContainer(() => ({}), () => ({}), () => new AwesomeMap()) } + + ); + }).toThrow(/merge/); }); it('should set the displayName correctly', () => { @@ -135,7 +499,7 @@ describe('React', () => { } } - expect(Container.displayName).toBe('Connector(Container)'); + expect(Container.displayName).toBe('ConnectDecorator(Container)'); }); it('should expose the wrapped component as DecoratedComponent', () => { diff --git a/test/components/connectDecorator.spec.js b/test/components/connectDecoratorOld.spec.js similarity index 97% rename from test/components/connectDecorator.spec.js rename to test/components/connectDecoratorOld.spec.js index 47b7a1063..36b491a38 100644 --- a/test/components/connectDecorator.spec.js +++ b/test/components/connectDecoratorOld.spec.js @@ -2,9 +2,9 @@ import expect from 'expect'; import jsdomReact from './jsdomReact'; import React, { PropTypes, Component } from 'react/addons'; import { createStore } from 'redux'; -import { connectDecorate, Connector } from '../../src/index'; +import { connectDecoratorOld, Connector } from '../../src/index'; -const connect = connectDecorate; +const connect = connectDecoratorOld; const { TestUtils } = React.addons; diff --git a/test/utils/wrapActionCreators.js b/test/utils/wrapActionCreators.js new file mode 100644 index 000000000..af31ce4d6 --- /dev/null +++ b/test/utils/wrapActionCreators.js @@ -0,0 +1,31 @@ +import expect from 'expect'; +import wrapActionCreators from '../../src/utils/wrapActionCreators'; + +describe('Utils', () => { + describe('wrapActionCreators', () => { + it('should return a function that wraps argument in a call to bindActionCreators', () => { + + function dispatch(action) { + return { + dispatched: action + }; + } + + const actionResult = {an: 'action'}; + + const actionCreators = { + action: () => actionResult + }; + + const wrapped = wrapActionCreators(actionCreators); + expect(wrapped).toBeA(Function); + expect(() => wrapped(dispatch)).toNotThrow(); + expect(() => wrapped().action()).toThrow(); + + const bound = wrapped(dispatch); + expect(bound.action).toNotThrow(); + expect(bound.action().dispatched).toBe(actionResult); + + }); + }); +}); From ce8bf1ee5a5c5c09bcf4ea03149aa0bddecfae29 Mon Sep 17 00:00:00 2001 From: gnoff Date: Thu, 30 Jul 2015 23:30:35 -0700 Subject: [PATCH 03/31] implemented new connect api. takes in selectState, dispatchBinder, merge and puts sequential results of such on props of decorated component. Connector is intended to be deprecated. older connectDecorator is now called connecctDeprecated --- src/components/createAll.js | 6 +++--- ...tDecoratorOld.js => createConnectDecoratorDeprecated.js} | 0 src/index.js | 2 +- src/native.js | 2 +- ...onnectDecoratorOld.spec.js => connectDeprecated.spec.js} | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) rename src/components/{createConnectDecoratorOld.js => createConnectDecoratorDeprecated.js} (100%) rename test/components/{connectDecoratorOld.spec.js => connectDeprecated.spec.js} (97%) diff --git a/src/components/createAll.js b/src/components/createAll.js index 4dba1c179..c0de66bfd 100644 --- a/src/components/createAll.js +++ b/src/components/createAll.js @@ -2,7 +2,7 @@ import createProvider from './createProvider'; import createProvideDecorator from './createProvideDecorator'; import createConnector from './createConnector'; -import createConnectDecoratorOld from './createConnectDecoratorOld'; +import createConnectDecoratorDeprecated from './createConnectDecoratorDeprecated'; import createConnectDecorator from './createConnectDecorator'; export default function createAll(React) { @@ -12,8 +12,8 @@ export default function createAll(React) { // Higher-order components (decorators) const provide = createProvideDecorator(React, Provider); - const connectDecoratorOld = createConnectDecoratorOld(React, Connector); + const connectDeprecated = createConnectDecoratorDeprecated(React, Connector); const connect = createConnectDecorator(React); - return { Provider, Connector, provide, connect, connectDecoratorOld }; + return { Provider, Connector, provide, connect, connectDeprecated }; } diff --git a/src/components/createConnectDecoratorOld.js b/src/components/createConnectDecoratorDeprecated.js similarity index 100% rename from src/components/createConnectDecoratorOld.js rename to src/components/createConnectDecoratorDeprecated.js diff --git a/src/index.js b/src/index.js index 3990fe043..f09a801ee 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ import React from 'react'; import createAll from './components/createAll'; -export const { Provider, Connector, provide, connect, connectDecoratorOld } = createAll(React); +export const { Provider, Connector, provide, connect, connectDeprecated } = createAll(React); diff --git a/src/native.js b/src/native.js index 7fc78db8a..16471679b 100644 --- a/src/native.js +++ b/src/native.js @@ -1,4 +1,4 @@ import React from 'react-native'; import createAll from './components/createAll'; -export const { Provider, Connector, provide, connect, connectDecoratorOld } = createAll(React); +export const { Provider, Connector, provide, connect, connectDeprecated } = createAll(React); diff --git a/test/components/connectDecoratorOld.spec.js b/test/components/connectDeprecated.spec.js similarity index 97% rename from test/components/connectDecoratorOld.spec.js rename to test/components/connectDeprecated.spec.js index 36b491a38..3a65f89b0 100644 --- a/test/components/connectDecoratorOld.spec.js +++ b/test/components/connectDeprecated.spec.js @@ -2,9 +2,9 @@ import expect from 'expect'; import jsdomReact from './jsdomReact'; import React, { PropTypes, Component } from 'react/addons'; import { createStore } from 'redux'; -import { connectDecoratorOld, Connector } from '../../src/index'; +import { connectDeprecated, Connector } from '../../src/index'; -const connect = connectDecoratorOld; +const connect = connectDeprecated; const { TestUtils } = React.addons; From a78837612b017e3604e9de42c6bfb59062545f19 Mon Sep 17 00:00:00 2001 From: gnoff Date: Thu, 30 Jul 2015 23:56:03 -0700 Subject: [PATCH 04/31] forgot to add {...this.props} to DecoratedComponent in render --- src/components/createConnectDecorator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/createConnectDecorator.js b/src/components/createConnectDecorator.js index a12a1bdf4..705349009 100644 --- a/src/components/createConnectDecorator.js +++ b/src/components/createConnectDecorator.js @@ -115,7 +115,7 @@ export default function createConnectDecorator(React) { } render() { - return ; + return ; } }; }; From b446894a1d21aa948a34fc39223b42129ef82765 Mon Sep 17 00:00:00 2001 From: gnoff Date: Fri, 31 Jul 2015 00:19:38 -0700 Subject: [PATCH 05/31] use ref callback to place instance of underlying component on wrapped component. provide wrapper method to get underlying ref to provide access to unerlying refs methods --- src/components/createConnectDecorator.js | 6 ++++- test/components/connect.spec.js | 34 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/components/createConnectDecorator.js b/src/components/createConnectDecorator.js index 705349009..d65f8878e 100644 --- a/src/components/createConnectDecorator.js +++ b/src/components/createConnectDecorator.js @@ -114,8 +114,12 @@ export default function createConnectDecorator(React) { return merged; } + getUnderlyingRef() { + return this.underlyingRef; + } + render() { - return ; + return (this.underlyingRef = component)} {...this.props} {...this.merge()} />; } }; }; diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 5938d99c3..d8e35026b 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -514,5 +514,39 @@ describe('React', () => { expect(decorated.DecoratedComponent).toBe(Container); }); + + it('should return the instance of the wrapped component for use in calling child methods', () => { + const store = createStore(() => ({})); + + const someData = { + some: 'data' + }; + + class Container extends Component { + someInstanceMethod() { + return someData; + } + + render() { + return
; + } + } + + const decorator = connect(state => state); + const Decorated = decorator(Container); + + const tree = TestUtils.renderIntoDocument( + + {() => ( + + )} + + ); + + const decorated = TestUtils.findRenderedComponentWithType(tree, Decorated); + + expect(() => decorated.someInstanceMethod()).toThrow(); + expect(decorated.getUnderlyingRef().someInstanceMethod()).toBe(someData); + }); }); }); From df18c6d3ab273e75b5d7a3c080daade08cff84e0 Mon Sep 17 00:00:00 2001 From: gnoff Date: Fri, 31 Jul 2015 00:24:24 -0700 Subject: [PATCH 06/31] remvoing deprecated code for Connector and original connectDecorator --- src/components/createAll.js | 6 +- .../createConnectDecoratorDeprecated.js | 25 -- src/components/createConnector.js | 88 ------ test/components/Connector.spec.js | 295 ------------------ test/components/connectDeprecated.spec.js | 156 --------- 5 files changed, 1 insertion(+), 569 deletions(-) delete mode 100644 src/components/createConnectDecoratorDeprecated.js delete mode 100644 src/components/createConnector.js delete mode 100644 test/components/Connector.spec.js delete mode 100644 test/components/connectDeprecated.spec.js diff --git a/src/components/createAll.js b/src/components/createAll.js index c0de66bfd..4699d819b 100644 --- a/src/components/createAll.js +++ b/src/components/createAll.js @@ -1,19 +1,15 @@ import createProvider from './createProvider'; import createProvideDecorator from './createProvideDecorator'; -import createConnector from './createConnector'; -import createConnectDecoratorDeprecated from './createConnectDecoratorDeprecated'; import createConnectDecorator from './createConnectDecorator'; export default function createAll(React) { // Wrapper components const Provider = createProvider(React); - const Connector = createConnector(React); // Higher-order components (decorators) const provide = createProvideDecorator(React, Provider); - const connectDeprecated = createConnectDecoratorDeprecated(React, Connector); const connect = createConnectDecorator(React); - return { Provider, Connector, provide, connect, connectDeprecated }; + return { Provider, provide, connect }; } diff --git a/src/components/createConnectDecoratorDeprecated.js b/src/components/createConnectDecoratorDeprecated.js deleted file mode 100644 index a3196e1c1..000000000 --- a/src/components/createConnectDecoratorDeprecated.js +++ /dev/null @@ -1,25 +0,0 @@ -import getDisplayName from '../utils/getDisplayName'; -import shallowEqualScalar from '../utils/shallowEqualScalar'; - -export default function createConnectDecorator(React, Connector) { - const { Component } = React; - - return function connect(select) { - return DecoratedComponent => class ConnectorDecorator extends Component { - static displayName = `Connector(${getDisplayName(DecoratedComponent)})`; - static DecoratedComponent = DecoratedComponent; - - shouldComponentUpdate(nextProps) { - return !shallowEqualScalar(this.props, nextProps); - } - - render() { - return ( - select(state, this.props)}> - {stuff => } - - ); - } - }; - }; -} diff --git a/src/components/createConnector.js b/src/components/createConnector.js deleted file mode 100644 index 2318092e5..000000000 --- a/src/components/createConnector.js +++ /dev/null @@ -1,88 +0,0 @@ -import createStoreShape from '../utils/createStoreShape'; -import shallowEqual from '../utils/shallowEqual'; -import isPlainObject from '../utils/isPlainObject'; -import invariant from 'invariant'; - -export default function createConnector(React) { - const { Component, PropTypes } = React; - const storeShape = createStoreShape(PropTypes); - - return class Connector extends Component { - static contextTypes = { - store: storeShape.isRequired - }; - - static propTypes = { - children: PropTypes.func.isRequired, - select: PropTypes.func.isRequired - }; - - static defaultProps = { - select: state => state - }; - - shouldComponentUpdate(nextProps, nextState) { - return !this.isSliceEqual(this.state.slice, nextState.slice) || - !shallowEqual(this.props, nextProps); - } - - isSliceEqual(slice, nextSlice) { - const isRefEqual = slice === nextSlice; - if (isRefEqual) { - return true; - } else if (typeof slice !== 'object' || typeof nextSlice !== 'object') { - return isRefEqual; - } - return shallowEqual(slice, nextSlice); - } - - constructor(props, context) { - super(props, context); - this.state = this.selectState(props, context); - } - - componentDidMount() { - this.unsubscribe = this.context.store.subscribe(::this.handleChange); - this.handleChange(); - } - - componentWillReceiveProps(nextProps) { - if (nextProps.select !== this.props.select) { - // Force the state slice recalculation - this.handleChange(nextProps); - } - } - - componentWillUnmount() { - this.unsubscribe(); - } - - handleChange(props = this.props) { - const nextState = this.selectState(props, this.context); - if (!this.isSliceEqual(this.state.slice, nextState.slice)) { - this.setState(nextState); - } - } - - selectState(props, context) { - const state = context.store.getState(); - const slice = props.select(state); - - invariant( - isPlainObject(slice), - 'The return value of `select` prop must be an object. Instead received %s.', - slice - ); - - return { slice }; - } - - render() { - const { children } = this.props; - const { slice } = this.state; - const { store: { dispatch } } = this.context; - - return children({ dispatch, ...slice }); - } - }; -} diff --git a/test/components/Connector.spec.js b/test/components/Connector.spec.js deleted file mode 100644 index 6d386de82..000000000 --- a/test/components/Connector.spec.js +++ /dev/null @@ -1,295 +0,0 @@ -import expect from 'expect'; -import jsdomReact from './jsdomReact'; -import React, { PropTypes, Component } from 'react/addons'; -import { createStore, combineReducers } from 'redux'; -import { Connector } from '../../src/index'; - -const { TestUtils } = React.addons; - -describe('React', () => { - describe('Connector', () => { - jsdomReact(); - - // Mock minimal Provider interface - class Provider extends Component { - static childContextTypes = { - store: PropTypes.object.isRequired - } - - getChildContext() { - return { store: this.props.store }; - } - - render() { - return this.props.children(); - } - } - - function stringBuilder(prev = '', action) { - return action.type === 'APPEND' - ? prev + action.body - : prev; - } - - it('should receive the store in the context', () => { - const store = createStore(() => ({})); - - const tree = TestUtils.renderIntoDocument( - - {() => ( - - {() =>
} - - )} - - ); - - const connector = TestUtils.findRenderedComponentWithType(tree, Connector); - expect(connector.context.store).toBe(store); - }); - - it('should subscribe to the store changes', () => { - const store = createStore(stringBuilder); - - const tree = TestUtils.renderIntoDocument( - - {() => ( - ({ string })}> - {({ string }) =>
} - - )} - - ); - - const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); - expect(div.props.string).toBe(''); - store.dispatch({ type: 'APPEND', body: 'a'}); - expect(div.props.string).toBe('a'); - store.dispatch({ type: 'APPEND', body: 'b'}); - expect(div.props.string).toBe('ab'); - }); - - it('should unsubscribe before unmounting', () => { - const store = createStore(stringBuilder); - const subscribe = store.subscribe; - - // Keep track of unsubscribe by wrapping subscribe() - const spy = expect.createSpy(() => ({})); - store.subscribe = (listener) => { - const unsubscribe = subscribe(listener); - return () => { - spy(); - return unsubscribe(); - }; - }; - - const tree = TestUtils.renderIntoDocument( - - {() => ( - ({ string })}> - {({ string }) =>
} - - )} - - ); - - const connector = TestUtils.findRenderedComponentWithType(tree, Connector); - expect(spy.calls.length).toBe(0); - connector.componentWillUnmount(); - expect(spy.calls.length).toBe(1); - }); - - it('should shallowly compare the selected state to prevent unnecessary updates', () => { - const store = createStore(stringBuilder); - const spy = expect.createSpy(() => ({})); - function render({ string }) { - spy(); - return
; - } - - const tree = TestUtils.renderIntoDocument( - - {() => ( - ({ string })}> - {render} - - )} - - ); - - const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); - expect(spy.calls.length).toBe(1); - expect(div.props.string).toBe(''); - store.dispatch({ type: 'APPEND', body: 'a'}); - expect(spy.calls.length).toBe(2); - store.dispatch({ type: 'APPEND', body: 'b'}); - expect(spy.calls.length).toBe(3); - store.dispatch({ type: 'APPEND', body: ''}); - expect(spy.calls.length).toBe(3); - }); - - it('should recompute the state slice when the select prop changes', () => { - const store = createStore(combineReducers({ - a: () => 42, - b: () => 72 - })); - - function selectA(state) { - return { result: state.a }; - } - - function selectB(state) { - return { result: state.b }; - } - - function render({ result }) { - return
{result}
; - } - - class Container extends Component { - constructor() { - super(); - this.state = { select: selectA }; - } - - render() { - return ( - - {() => - - {render} - - } - - ); - } - } - - let tree = TestUtils.renderIntoDocument(); - let div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); - expect(div.props.children).toBe(42); - - tree.setState({ select: selectB }); - expect(div.props.children).toBe(72); - }); - - it('should pass dispatch() to the child function', () => { - const store = createStore(() => ({})); - - const tree = TestUtils.renderIntoDocument( - - {() => ( - - {({ dispatch }) =>
} - - )} - - ); - - const div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); - expect(div.props.dispatch).toBe(store.dispatch); - }); - - it('should throw an error if select returns anything but a plain object', () => { - const store = createStore(() => ({})); - - expect(() => { - TestUtils.renderIntoDocument( - - {() => ( - 1}> - {() =>
} - - )} - - ); - }).toThrow(/select/); - - expect(() => { - TestUtils.renderIntoDocument( - - {() => ( - 'hey'}> - {() =>
} - - )} - - ); - }).toThrow(/select/); - - function AwesomeMap() { } - - expect(() => { - TestUtils.renderIntoDocument( - - {() => ( - new AwesomeMap()}> - {() =>
} - - )} - - ); - }).toThrow(/select/); - }); - - it('should not setState when renderToString is called on the server', () => { - const { renderToString } = React; - const store = createStore(stringBuilder); - - class TestComp extends Component { - componentWillMount() { - store.dispatch({ - type: 'APPEND', - body: 'a' - }); - } - - render() { - return
{this.props.string}
; - } - } - - const el = ( - - {() => ( - ({ string })}> - {({ string }) => } - - )} - - ); - - expect(() => renderToString(el)).toNotThrow(); - }); - - it('should handle dispatch inside componentDidMount', () => { - const store = createStore(stringBuilder); - - class TestComp extends Component { - componentDidMount() { - store.dispatch({ - type: 'APPEND', - body: 'a' - }); - } - - render() { - return
{this.props.string}
; - } - } - - const tree = TestUtils.renderIntoDocument( - - {() => ( - ({ string })}> - {({ string }) => } - - )} - - ); - - const testComp = TestUtils.findRenderedComponentWithType(tree, TestComp); - expect(testComp.props.string).toBe('a'); - }); - }); -}); diff --git a/test/components/connectDeprecated.spec.js b/test/components/connectDeprecated.spec.js deleted file mode 100644 index 3a65f89b0..000000000 --- a/test/components/connectDeprecated.spec.js +++ /dev/null @@ -1,156 +0,0 @@ -import expect from 'expect'; -import jsdomReact from './jsdomReact'; -import React, { PropTypes, Component } from 'react/addons'; -import { createStore } from 'redux'; -import { connectDeprecated, Connector } from '../../src/index'; - -const connect = connectDeprecated; - -const { TestUtils } = React.addons; - -describe('React', () => { - describe('connectDecorate', () => { - jsdomReact(); - - // Mock minimal Provider interface - class Provider extends Component { - static childContextTypes = { - store: PropTypes.object.isRequired - } - - getChildContext() { - return { store: this.props.store }; - } - - render() { - return this.props.children(); - } - } - - it('should wrap the component into Provider', () => { - const store = createStore(() => ({ - foo: 'bar' - })); - - @connect(state => state) - class Container extends Component { - render() { - return
; - } - } - - const container = TestUtils.renderIntoDocument( - - {() => } - - ); - const div = TestUtils.findRenderedDOMComponentWithTag(container, 'div'); - expect(div.props.pass).toEqual('through'); - expect(div.props.foo).toEqual('bar'); - expect(() => - TestUtils.findRenderedComponentWithType(container, Connector) - ).toNotThrow(); - }); - - it('should handle additional prop changes in addition to slice', () => { - const store = createStore(() => ({ - foo: 'bar' - })); - - @connect(state => state) - class ConnectContainer extends Component { - render() { - return ( -
- ); - } - } - - class Container extends Component { - constructor() { - super(); - this.state = { - bar: { - baz: '' - } - }; - } - componentDidMount() { - - // Simulate deep object mutation - this.state.bar.baz = 'through'; - this.setState({ - bar: this.state.bar - }); - } - render() { - return ( - - {() => } - - ); - } - } - - const container = TestUtils.renderIntoDocument(); - const div = TestUtils.findRenderedDOMComponentWithTag(container, 'div'); - expect(div.props.foo).toEqual('bar'); - expect(div.props.pass).toEqual('through'); - }); - - it('should pass the only argument as the select prop down', () => { - const store = createStore(() => ({ - foo: 'baz', - bar: 'baz' - })); - - function select({ foo }) { - return { foo }; - } - - @connect(select) - class Container extends Component { - render() { - return
; - } - } - - const container = TestUtils.renderIntoDocument( - - {() => } - - ); - const connector = TestUtils.findRenderedComponentWithType(container, Connector); - expect(connector.props.select({ - foo: 5, - bar: 7 - })).toEqual({ - foo: 5 - }); - }); - - it('should set the displayName correctly', () => { - @connect(state => state) - class Container extends Component { - render() { - return
; - } - } - - expect(Container.displayName).toBe('Connector(Container)'); - }); - - it('should expose the wrapped component as DecoratedComponent', () => { - class Container extends Component { - render() { - return
; - } - } - - const decorator = connect(state => state); - const decorated = decorator(Container); - - expect(decorated.DecoratedComponent).toBe(Container); - }); - }); -}); From 74c3d9d11f732e95b81936255b2f010b44f2873f Mon Sep 17 00:00:00 2001 From: gnoff Date: Fri, 31 Jul 2015 00:26:28 -0700 Subject: [PATCH 07/31] move props in identityMerge to front to allow override by slice and actionCreators, remove from DecoratedComponent render --- src/components/createConnectDecorator.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/createConnectDecorator.js b/src/components/createConnectDecorator.js index 705349009..db56935a3 100644 --- a/src/components/createConnectDecorator.js +++ b/src/components/createConnectDecorator.js @@ -10,7 +10,7 @@ const emptySelector = () => ({}); const emptyBinder = () => ({}); -const identityMerge = (slice, actionsCreators, props) => ({...slice, ...actionsCreators, ...props}); +const identityMerge = (slice, actionsCreators, props) => ({ ...props, ...slice, ...actionsCreators}); export default function createConnectDecorator(React) { @@ -115,7 +115,7 @@ export default function createConnectDecorator(React) { } render() { - return ; + return ; } }; }; From 170b1b7195ec9ee9f79a3e9d23b9e69f4058d07c Mon Sep 17 00:00:00 2001 From: Josh Story Date: Fri, 31 Jul 2015 10:36:29 -0700 Subject: [PATCH 08/31] changed ConnectDecorator#bindActionCreators to ConnectDecorator#bindDispatch and updated corresponding method names. this is to avoid confusion with the redux utility bindActionCreators and the fact that the second argument doesn not have to necessarily be related to action creators in any way --- src/components/createConnectDecorator.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/createConnectDecorator.js b/src/components/createConnectDecorator.js index 4349d4fc3..51510e361 100644 --- a/src/components/createConnectDecorator.js +++ b/src/components/createConnectDecorator.js @@ -21,7 +21,7 @@ export default function createConnectDecorator(React) { const subscribing = select ? true : false; const selectState = select || emptySelector; - const bindActionCreators = isPlainObject(dispatchBinder) ? wrapActionCreators(dispatchBinder) : dispatchBinder; + const bindDispatch = isPlainObject(dispatchBinder) ? wrapActionCreators(dispatchBinder) : dispatchBinder; const merge = mergeHandler; return DecoratedComponent => class ConnectDecorator extends Component { @@ -51,7 +51,7 @@ export default function createConnectDecorator(React) { super(props, context); this.state = { ...this.selectState(props, context), - ...this.bindActionCreators(context) + ...this.bindDispatch(context) }; } @@ -88,13 +88,13 @@ export default function createConnectDecorator(React) { return { slice }; } - bindActionCreators(context = this.context) { + bindDispatch(context = this.context) { const { dispatch } = context.store; - const actionCreators = bindActionCreators(dispatch); + const actionCreators = bindDispatch(dispatch); invariant( isPlainObject(actionCreators), - 'The return value of `bindActionCreators` prop must be an object. Instead received %s.', + 'The return value of `bindDispatch` prop must be an object. Instead received %s.', actionCreators ); From 105a2aa3b613caf54a18bc8904c75192a5084836 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Fri, 31 Jul 2015 10:39:13 -0700 Subject: [PATCH 09/31] remove test for changing select prop as it no longer applies to new api --- test/components/connect.spec.js | 59 --------------------------------- 1 file changed, 59 deletions(-) diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index d8e35026b..2ffb2bec4 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -343,65 +343,6 @@ describe('React', () => { expect(spy.calls.length).toBe(3); }); - it('should recompute the state slice when the select prop changes', () => { - const store = createStore(combineReducers({ - a: () => 42, - b: () => 72 - })); - - function selectA(state) { - return { result: state.a }; - } - - function selectB(state) { - return { result: state.b }; - } - - function render({ result }) { - return
{result}
; - } - - function getContainer(select) { - return ( - @connect(select) - class Container extends Component { - render() { - return this.props.children(this.props); - } - } - ); - } - - class OuterContainer extends Component { - constructor() { - super(); - this.state = { select: selectA }; - } - - render() { - return ( - - {() => { - const Container = getContainer(this.state.select); - return ( - - {render} - - ); - }} - - ); - } - } - - let tree = TestUtils.renderIntoDocument(); - let div = TestUtils.findRenderedDOMComponentWithTag(tree, 'div'); - expect(div.props.children).toBe(42); - - tree.setState({ select: selectB }); - expect(div.props.children).toBe(72); - }); - it('should throw an error if select, bindActionCreators, or merge returns anything but a plain object', () => { const store = createStore(() => ({})); From c54a8b80da1dcf865bf54da02fffda84c0094fe3 Mon Sep 17 00:00:00 2001 From: gnoff Date: Wed, 5 Aug 2015 21:48:29 -0700 Subject: [PATCH 10/31] fixing test reference to bindActionCreators which was renamed bindDispatch --- test/components/connect.spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 2ffb2bec4..227d5f123 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -343,7 +343,7 @@ describe('React', () => { expect(spy.calls.length).toBe(3); }); - it('should throw an error if select, bindActionCreators, or merge returns anything but a plain object', () => { + it('should throw an error if select, bindDispatch, or merge returns anything but a plain object', () => { const store = createStore(() => ({})); function makeContainer(select, bindActionCreators, merge) { @@ -389,7 +389,7 @@ describe('React', () => { { () => makeContainer(() => ({}), () => 1, () => ({})) } ); - }).toThrow(/bindActionCreators/); + }).toThrow(/bindDispatch/); expect(() => { TestUtils.renderIntoDocument( @@ -397,7 +397,7 @@ describe('React', () => { { () => makeContainer(() => ({}), () => 'hey', () => ({})) } ); - }).toThrow(/bindActionCreators/); + }).toThrow(/bindDispatch/); expect(() => { TestUtils.renderIntoDocument( @@ -405,7 +405,7 @@ describe('React', () => { { () => makeContainer(() => ({}), () => new AwesomeMap(), () => ({})) } ); - }).toThrow(/bindActionCreators/); + }).toThrow(/bindDispatch/); expect(() => { TestUtils.renderIntoDocument( From 1e9809e9d9355f68eb8b996bcb1f5ed02fa52d22 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Aug 2015 13:42:29 +0300 Subject: [PATCH 11/31] Revert deprecation in favor of real PR --- src/components/createAll.js | 11 ++-- src/components/createConnector.js | 1 - src/components/createProvideDecorator.js | 20 +++++++ src/index.js | 3 +- src/native.js | 3 +- test/components/Connector.spec.js | 3 +- test/components/connect.spec.js | 26 ++++----- test/components/provide.spec.js | 68 ++++++++++++++++++++++++ 8 files changed, 110 insertions(+), 25 deletions(-) create mode 100644 src/components/createProvideDecorator.js create mode 100644 test/components/provide.spec.js diff --git a/src/components/createAll.js b/src/components/createAll.js index 725f5a901..bd6383ccc 100644 --- a/src/components/createAll.js +++ b/src/components/createAll.js @@ -1,12 +1,17 @@ import createProvider from './createProvider'; +import createProvideDecorator from './createProvideDecorator'; import createConnector from './createConnector'; import createConnectDecorator from './createConnectDecorator'; export default function createAll(React) { + // Wrapper components const Provider = createProvider(React); - const connect = createConnectDecorator(React, createConnector(React)); + const Connector = createConnector(React); - // provider and Connector are deprecated and removed from public API - return { Provider, connect }; + // Higher-order components (decorators) + const provide = createProvideDecorator(React, Provider); + const connect = createConnectDecorator(React, Connector); + + return { Provider, Connector, provide, connect }; } diff --git a/src/components/createConnector.js b/src/components/createConnector.js index 3e4fb2dd4..2318092e5 100644 --- a/src/components/createConnector.js +++ b/src/components/createConnector.js @@ -3,7 +3,6 @@ import shallowEqual from '../utils/shallowEqual'; import isPlainObject from '../utils/isPlainObject'; import invariant from 'invariant'; -// Connector is deprecated and removed from public API export default function createConnector(React) { const { Component, PropTypes } = React; const storeShape = createStoreShape(PropTypes); diff --git a/src/components/createProvideDecorator.js b/src/components/createProvideDecorator.js new file mode 100644 index 000000000..d181865a4 --- /dev/null +++ b/src/components/createProvideDecorator.js @@ -0,0 +1,20 @@ +import getDisplayName from '../utils/getDisplayName'; + +export default function createProvideDecorator(React, Provider) { + const { Component } = React; + + return function provide(store) { + return DecoratedComponent => class ProviderDecorator extends Component { + static displayName = `Provider(${getDisplayName(DecoratedComponent)})`; + static DecoratedComponent = DecoratedComponent; + + render() { + return ( + + {() => } + + ); + } + }; + }; +} diff --git a/src/index.js b/src/index.js index b74f00c5e..a2058d695 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,4 @@ import React from 'react'; import createAll from './components/createAll'; -// provide and Connector are deprecated and removed from public API -export const { Provider, connect } = createAll(React); +export const { Provider, Connector, provide, connect } = createAll(React); diff --git a/src/native.js b/src/native.js index b21826040..c6fc5363e 100644 --- a/src/native.js +++ b/src/native.js @@ -1,5 +1,4 @@ import React from 'react-native'; import createAll from './components/createAll'; -// provide and Connector are deprecated and removed from public API -export const { Provider, connect } = createAll(React); +export const { Provider, Connector, provide, connect } = createAll(React); diff --git a/test/components/Connector.spec.js b/test/components/Connector.spec.js index 4f1141abb..bb4cef68b 100644 --- a/test/components/Connector.spec.js +++ b/test/components/Connector.spec.js @@ -2,9 +2,8 @@ import expect from 'expect'; import jsdomReact from './jsdomReact'; import React, { PropTypes, Component } from 'react/addons'; import { createStore } from 'redux'; -import createConnector from '../../src/components/createConnector'; +import { Connector } from '../../src/index'; -const Connector = createConnector(React); const { TestUtils } = React.addons; describe('React', () => { diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 57e22ab1c..743881097 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -2,7 +2,7 @@ import expect from 'expect'; import jsdomReact from './jsdomReact'; import React, { PropTypes, Component } from 'react/addons'; import { createStore } from 'redux'; -import { connect } from '../../src/index'; +import { connect, Connector } from '../../src/index'; const { TestUtils } = React.addons; @@ -45,11 +45,9 @@ describe('React', () => { const div = TestUtils.findRenderedDOMComponentWithTag(container, 'div'); expect(div.props.pass).toEqual('through'); expect(div.props.foo).toEqual('bar'); - - // Connector is deprecated and removed from public API - // expect(() => - // TestUtils.findRenderedComponentWithType(container, Connector) - // ).toNotThrow(); + expect(() => + TestUtils.findRenderedComponentWithType(container, Connector) + ).toNotThrow(); }); it('should handle additional prop changes in addition to slice', () => { @@ -120,15 +118,13 @@ describe('React', () => { {() => } ); - - // Connector is deprecated and removed from public API - // const connector = TestUtils.findRenderedComponentWithType(container, Connector); - // expect(connector.props.select({ - // foo: 5, - // bar: 7 - // })).toEqual({ - // foo: 5 - // }); + const connector = TestUtils.findRenderedComponentWithType(container, Connector); + expect(connector.props.select({ + foo: 5, + bar: 7 + })).toEqual({ + foo: 5 + }); }); it('should set the displayName correctly', () => { diff --git a/test/components/provide.spec.js b/test/components/provide.spec.js new file mode 100644 index 000000000..add035f15 --- /dev/null +++ b/test/components/provide.spec.js @@ -0,0 +1,68 @@ +import expect from 'expect'; +import jsdomReact from './jsdomReact'; +import React, { PropTypes, Component } from 'react/addons'; +import { createStore } from 'redux'; +import { provide, Provider } from '../../src/index'; + +const { TestUtils } = React.addons; + +describe('React', () => { + describe('provide', () => { + jsdomReact(); + + class Child extends Component { + static contextTypes = { + store: PropTypes.object.isRequired + } + + render() { + return
; + } + } + + it('should wrap the component into Provider', () => { + const store = createStore({}); + + @provide(store) + class Container extends Component { + render() { + return ; + } + } + + const container = TestUtils.renderIntoDocument( + + ); + const child = TestUtils.findRenderedComponentWithType(container, Child); + expect(child.props.pass).toEqual('through'); + expect(() => + TestUtils.findRenderedComponentWithType(container, Provider) + ).toNotThrow(); + expect(child.context.store).toBe(store); + }); + + it('sets the displayName correctly', () => { + @provide(createStore({})) + class Container extends Component { + render() { + return
; + } + } + + expect(Container.displayName).toBe('Provider(Container)'); + }); + + it('should expose the wrapped component as DecoratedComponent', () => { + class Container extends Component { + render() { + return
; + } + } + + const decorator = provide(state => state); + const decorated = decorator(Container); + + expect(decorated.DecoratedComponent).toBe(Container); + }); + }); +}); From 09494885cad9cebd744b8bf05b3b22e45061c214 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Aug 2015 14:06:14 +0300 Subject: [PATCH 12/31] Move stuff around, finish removing provide() and --- src/components/createAll.js | 12 +--- ...teConnectDecorator.js => createConnect.js} | 8 ++- src/components/createProvideDecorator.js | 20 ------ src/index.js | 2 +- src/native.js | 2 +- src/utils/getDisplayName.js | 3 - test/components/connect.spec.js | 32 ++++++--- test/components/provide.spec.js | 68 ------------------- test/utils/getDisplayName.spec.js | 17 ----- 9 files changed, 34 insertions(+), 130 deletions(-) rename src/components/{createConnectDecorator.js => createConnect.js} (94%) delete mode 100644 src/components/createProvideDecorator.js delete mode 100644 src/utils/getDisplayName.js delete mode 100644 test/components/provide.spec.js delete mode 100644 test/utils/getDisplayName.spec.js diff --git a/src/components/createAll.js b/src/components/createAll.js index 4699d819b..e83b7c9f6 100644 --- a/src/components/createAll.js +++ b/src/components/createAll.js @@ -1,15 +1,9 @@ import createProvider from './createProvider'; -import createProvideDecorator from './createProvideDecorator'; - -import createConnectDecorator from './createConnectDecorator'; +import createConnect from './createConnect'; export default function createAll(React) { - // Wrapper components const Provider = createProvider(React); + const connect = createConnect(React); - // Higher-order components (decorators) - const provide = createProvideDecorator(React, Provider); - const connect = createConnectDecorator(React); - - return { Provider, provide, connect }; + return { Provider, connect }; } diff --git a/src/components/createConnectDecorator.js b/src/components/createConnect.js similarity index 94% rename from src/components/createConnectDecorator.js rename to src/components/createConnect.js index 51510e361..4ead0d13e 100644 --- a/src/components/createConnectDecorator.js +++ b/src/components/createConnect.js @@ -1,5 +1,4 @@ import createStoreShape from '../utils/createStoreShape'; -import getDisplayName from '../utils/getDisplayName'; import shallowEqualScalar from '../utils/shallowEqualScalar'; import shallowEqual from '../utils/shallowEqual'; import isPlainObject from '../utils/isPlainObject'; @@ -12,8 +11,11 @@ const emptyBinder = () => ({}); const identityMerge = (slice, actionsCreators, props) => ({ ...props, ...slice, ...actionsCreators}); +function getDisplayName(Component) { + return Component.displayName || Component.name || 'Component'; +} -export default function createConnectDecorator(React) { +export default function createConnect(React) { const { Component, PropTypes } = React; const storeShape = createStoreShape(PropTypes); @@ -25,7 +27,7 @@ export default function createConnectDecorator(React) { const merge = mergeHandler; return DecoratedComponent => class ConnectDecorator extends Component { - static displayName = `ConnectDecorator(${getDisplayName(DecoratedComponent)})`; + static displayName = `Connect(${getDisplayName(DecoratedComponent)})`; static DecoratedComponent = DecoratedComponent; static contextTypes = { diff --git a/src/components/createProvideDecorator.js b/src/components/createProvideDecorator.js deleted file mode 100644 index d181865a4..000000000 --- a/src/components/createProvideDecorator.js +++ /dev/null @@ -1,20 +0,0 @@ -import getDisplayName from '../utils/getDisplayName'; - -export default function createProvideDecorator(React, Provider) { - const { Component } = React; - - return function provide(store) { - return DecoratedComponent => class ProviderDecorator extends Component { - static displayName = `Provider(${getDisplayName(DecoratedComponent)})`; - static DecoratedComponent = DecoratedComponent; - - render() { - return ( - - {() => } - - ); - } - }; - }; -} diff --git a/src/index.js b/src/index.js index f09a801ee..f6a257dd4 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ import React from 'react'; import createAll from './components/createAll'; -export const { Provider, Connector, provide, connect, connectDeprecated } = createAll(React); +export const { Provider, connect } = createAll(React); diff --git a/src/native.js b/src/native.js index 16471679b..24a1d67cd 100644 --- a/src/native.js +++ b/src/native.js @@ -1,4 +1,4 @@ import React from 'react-native'; import createAll from './components/createAll'; -export const { Provider, Connector, provide, connect, connectDeprecated } = createAll(React); +export const { Provider, connect } = createAll(React); diff --git a/src/utils/getDisplayName.js b/src/utils/getDisplayName.js deleted file mode 100644 index 512702c87..000000000 --- a/src/utils/getDisplayName.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function getDisplayName(Component) { - return Component.displayName || Component.name || 'Component'; -} diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 227d5f123..afddd691e 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -1,7 +1,7 @@ import expect from 'expect'; import jsdomReact from './jsdomReact'; -import React, { PropTypes, Component } from 'react/addons'; -import { createStore, combineReducers } from 'redux'; +import React, { createClass, PropTypes, Component } from 'react/addons'; +import { createStore } from 'redux'; import { connect } from '../../src/index'; const { TestUtils } = React.addons; @@ -433,14 +433,30 @@ describe('React', () => { }); it('should set the displayName correctly', () => { - @connect(state => state) - class Container extends Component { - render() { - return
; + expect(connect(state => state)( + class Foo extends Component { + render() { + return
; + } } - } + ).displayName).toBe('Connect(Foo)'); - expect(Container.displayName).toBe('ConnectDecorator(Container)'); + expect(connect(state => state)( + createClass({ + displayName: 'Bar', + render() { + return
; + } + }) + ).displayName).toBe('Connect(Bar)'); + + expect(connect(state => state)( + createClass({ + render() { + return
; + } + }) + ).displayName).toBe('Connect(Component)'); }); it('should expose the wrapped component as DecoratedComponent', () => { diff --git a/test/components/provide.spec.js b/test/components/provide.spec.js deleted file mode 100644 index ea328d9a4..000000000 --- a/test/components/provide.spec.js +++ /dev/null @@ -1,68 +0,0 @@ -import expect from 'expect'; -import jsdomReact from './jsdomReact'; -import React, { PropTypes, Component } from 'react/addons'; -import { createStore } from 'redux'; -import { provide, Provider } from '../../src/index'; - -const { TestUtils } = React.addons; - -describe('React', () => { - describe('provide', () => { - jsdomReact(); - - class Child extends Component { - static contextTypes = { - store: PropTypes.object.isRequired - } - - render() { - return
; - } - } - - it('should wrap the component into Provider', () => { - const store = createStore(() => ({})); - - @provide(store) - class Container extends Component { - render() { - return ; - } - } - - const container = TestUtils.renderIntoDocument( - - ); - const child = TestUtils.findRenderedComponentWithType(container, Child); - expect(child.props.pass).toEqual('through'); - expect(() => - TestUtils.findRenderedComponentWithType(container, Provider) - ).toNotThrow(); - expect(child.context.store).toBe(store); - }); - - it('sets the displayName correctly', () => { - @provide(createStore(() => ({}))) - class Container extends Component { - render() { - return
; - } - } - - expect(Container.displayName).toBe('Provider(Container)'); - }); - - it('should expose the wrapped component as DecoratedComponent', () => { - class Container extends Component { - render() { - return
; - } - } - - const decorator = provide(state => state); - const decorated = decorator(Container); - - expect(decorated.DecoratedComponent).toBe(Container); - }); - }); -}); diff --git a/test/utils/getDisplayName.spec.js b/test/utils/getDisplayName.spec.js deleted file mode 100644 index 6a1740d76..000000000 --- a/test/utils/getDisplayName.spec.js +++ /dev/null @@ -1,17 +0,0 @@ -import expect from 'expect'; -import { createClass, Component } from 'react'; -import getDisplayName from '../../src/utils/getDisplayName'; - -describe('Utils', () => { - describe('getDisplayName', () => { - it('should extract the component class name', () => { - const names = [ - createClass({ displayName: 'Foo', render() {} }), - class Bar extends Component {}, - createClass({ render() {} }) - ].map(getDisplayName); - - expect(names).toEqual(['Foo', 'Bar', 'Component']); - }); - }); -}); From 999282743254a486367216ce96f3abf3af50d654 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Aug 2015 14:19:47 +0300 Subject: [PATCH 13/31] Style tweaks --- src/components/createConnect.js | 45 +++++++++++++++++++++++---------- test/components/connect.spec.js | 9 +++---- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/src/components/createConnect.js b/src/components/createConnect.js index 4ead0d13e..7344646df 100644 --- a/src/components/createConnect.js +++ b/src/components/createConnect.js @@ -6,10 +6,12 @@ import wrapActionCreators from '../utils/wrapActionCreators'; import invariant from 'invariant'; const emptySelector = () => ({}); - const emptyBinder = () => ({}); - -const identityMerge = (slice, actionsCreators, props) => ({ ...props, ...slice, ...actionsCreators}); +const identityMerge = (slice, actionsCreators, props) => ({ + ...props, + ...slice, + ...actionsCreators +}); function getDisplayName(Component) { return Component.displayName || Component.name || 'Component'; @@ -19,11 +21,17 @@ export default function createConnect(React) { const { Component, PropTypes } = React; const storeShape = createStoreShape(PropTypes); - return function connect(select, dispatchBinder = emptyBinder, mergeHandler = identityMerge) { - - const subscribing = select ? true : false; + return function connect( + select, + dispatchBinder = emptyBinder, + mergeHandler = identityMerge + ) { + const shouldSubscribe = select ? true : false; const selectState = select || emptySelector; - const bindDispatch = isPlainObject(dispatchBinder) ? wrapActionCreators(dispatchBinder) : dispatchBinder; + const bindDispatch = isPlainObject(dispatchBinder) ? + wrapActionCreators(dispatchBinder) : + dispatchBinder; + const merge = mergeHandler; return DecoratedComponent => class ConnectDecorator extends Component { @@ -41,16 +49,20 @@ export default function createConnect(React) { isSliceEqual(slice, nextSlice) { const isRefEqual = slice === nextSlice; - if (isRefEqual) { - return true; - } else if (typeof slice !== 'object' || typeof nextSlice !== 'object') { + if ( + isRefEqual || + typeof slice !== 'object' || + typeof nextSlice !== 'object' + ) { return isRefEqual; } + return shallowEqual(slice, nextSlice); } constructor(props, context) { super(props, context); + this.setUnderlyingRef = ::this.setUnderlyingRef; this.state = { ...this.selectState(props, context), ...this.bindDispatch(context) @@ -58,14 +70,14 @@ export default function createConnect(React) { } componentDidMount() { - if (subscribing) { + if (shouldSubscribe) { this.subscribed = true; this.unsubscribe = this.context.store.subscribe(::this.handleChange); } } componentWillUnmount() { - if (subscribing) { + if (shouldSubscribe) { this.unsubscribe(); } } @@ -120,8 +132,15 @@ export default function createConnect(React) { return this.underlyingRef; } + setUnderlyingRef(instance) { + this.underlyingRef = instance; + } + render() { - return (this.underlyingRef = component)} {...this.merge()} />; + return ( + + ); } }; }; diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index afddd691e..85c34f7ad 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -215,7 +215,7 @@ describe('React', () => { @connect( state => state, dispatch => ({ dispatch }) - ) + ) class Container extends Component { render() { return
; @@ -245,7 +245,7 @@ describe('React', () => { @connect( null, dispatch => ({ dispatch }) - ) + ) class Container extends Component { render() { return
; @@ -265,7 +265,6 @@ describe('React', () => { ).toNotThrow(); const decorated = TestUtils.findRenderedComponentWithType(container, Container); expect(decorated.subscribed).toNotBe(true); - }); it('should unsubscribe before unmounting', () => { @@ -285,7 +284,7 @@ describe('React', () => { @connect( state => ({string: state}), dispatch => ({ dispatch }) - ) + ) class Container extends Component { render() { return
; @@ -317,7 +316,7 @@ describe('React', () => { @connect( state => ({string: state}), dispatch => ({ dispatch }) - ) + ) class Container extends Component { render() { return render(this.props); From 47a7df7b858b1ef7762498900373557dae9eab56 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Aug 2015 14:47:03 +0300 Subject: [PATCH 14/31] Add a test and some style fixes --- src/components/createConnect.js | 1 - test/components/connect.spec.js | 23 +++++++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/components/createConnect.js b/src/components/createConnect.js index 7344646df..adcdbd41f 100644 --- a/src/components/createConnect.js +++ b/src/components/createConnect.js @@ -31,7 +31,6 @@ export default function createConnect(React) { const bindDispatch = isPlainObject(dispatchBinder) ? wrapActionCreators(dispatchBinder) : dispatchBinder; - const merge = mergeHandler; return DecoratedComponent => class ConnectDecorator extends Component { diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 85c34f7ad..2c0bfee86 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -53,12 +53,14 @@ describe('React', () => { expect(container.context.store).toBe(store); }); - it('should wrap the component into Provider', () => { + it('should pass the state and the props given component', () => { const store = createStore(() => ({ - foo: 'bar' + foo: 'bar', + baz: 42, + hello: 'world' })); - @connect(state => state) + @connect(({ foo, baz }) => ({ foo, baz })) class Container extends Component { render() { return
; @@ -67,12 +69,14 @@ describe('React', () => { const container = TestUtils.renderIntoDocument( - {() => } + {() => } ); const div = TestUtils.findRenderedDOMComponentWithTag(container, 'div'); expect(div.props.pass).toEqual('through'); expect(div.props.foo).toEqual('bar'); + expect(div.props.baz).toEqual(42); + expect(div.props.hello).toEqual(undefined); expect(() => TestUtils.findRenderedComponentWithType(container, Container) ).toNotThrow(); @@ -81,7 +85,7 @@ describe('React', () => { it('should subscribe to the store changes', () => { const store = createStore(stringBuilder); - @connect(state => ({string: state}) ) + @connect(state => ({ string: state }) ) class Container extends Component { render() { return
; @@ -128,14 +132,15 @@ describe('React', () => { } }; } - componentDidMount() { + componentDidMount() { // Simulate deep object mutation this.state.bar.baz = 'through'; this.setState({ bar: this.state.bar }); } + render() { return ( @@ -163,11 +168,13 @@ describe('React', () => { @connect( state => ({stateThing: state}), - dispatch => ({doSomething: (whatever) => dispatch(doSomething(whatever)) }), + dispatch => ({ + doSomething: (whatever) => dispatch(doSomething(whatever)) + }), (stateProps, actionProps, parentProps) => ({ ...stateProps, ...actionProps, - mergedDoSomething: (thing) => { + mergedDoSomething(thing) { const seed = stateProps.stateThing === '' ? 'HELLO ' : ''; actionProps.doSomething(seed + thing + parentProps.extra); } From ca47a0ad5fdd5f7690a53f2ceb4d4d89643133a6 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Aug 2015 15:05:52 +0300 Subject: [PATCH 15/31] Dispatch should be passed by default --- src/components/createConnect.js | 4 ++-- test/components/connect.spec.js | 9 +++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/components/createConnect.js b/src/components/createConnect.js index adcdbd41f..d8b7ef943 100644 --- a/src/components/createConnect.js +++ b/src/components/createConnect.js @@ -6,7 +6,7 @@ import wrapActionCreators from '../utils/wrapActionCreators'; import invariant from 'invariant'; const emptySelector = () => ({}); -const emptyBinder = () => ({}); +const defaultBinder = dispatch => ({ dispatch }); const identityMerge = (slice, actionsCreators, props) => ({ ...props, ...slice, @@ -23,7 +23,7 @@ export default function createConnect(React) { return function connect( select, - dispatchBinder = emptyBinder, + dispatchBinder = defaultBinder, mergeHandler = identityMerge ) { const shouldSubscribe = select ? true : false; diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 2c0bfee86..787cc81cf 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -53,7 +53,7 @@ describe('React', () => { expect(container.context.store).toBe(store); }); - it('should pass the state and the props given component', () => { + it('should pass state and props to the given component', () => { const store = createStore(() => ({ foo: 'bar', baz: 42, @@ -244,15 +244,12 @@ describe('React', () => { expect(decorated.subscribed).toBe(true); }); - it('should not subscribe to stores if select argument is null', () => { + it('should not subscribe to stores if select argument is falsy', () => { const store = createStore(() => ({ foo: 'bar' })); - @connect( - null, - dispatch => ({ dispatch }) - ) + @connect() class Container extends Component { render() { return
; From d8d416e61520619456d9a6b98cac8556068aa963 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Aug 2015 15:07:16 +0300 Subject: [PATCH 16/31] Use better isPlainObject --- src/utils/isPlainObject.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/utils/isPlainObject.js b/src/utils/isPlainObject.js index a5845486c..da6b78cf2 100644 --- a/src/utils/isPlainObject.js +++ b/src/utils/isPlainObject.js @@ -1,3 +1,25 @@ +const fnToString = (fn) => Function.prototype.toString.call(fn); + +/** + * @param {any} obj The object to inspect. + * @returns {boolean} True if the argument appears to be a plain object. + */ export default function isPlainObject(obj) { - return obj ? typeof obj === 'object' && Object.getPrototypeOf(obj) === Object.prototype : false; + if (!obj || typeof obj !== 'object') { + return false; + } + + const proto = typeof obj.constructor === 'function' ? + Object.getPrototypeOf(obj) : + Object.prototype; + + if (proto === null) { + return true; + } + + const constructor = proto.constructor; + + return typeof constructor === 'function' + && constructor instanceof constructor + && fnToString(constructor) === fnToString(Object); } From 6e99536956e3e698559666eef06e7a5011f99757 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Aug 2015 15:58:13 +0300 Subject: [PATCH 17/31] Rename arguments --- src/components/createConnect.js | 46 ++++++++++++++++----------------- test/components/connect.spec.js | 26 +++++++++---------- 2 files changed, 35 insertions(+), 37 deletions(-) diff --git a/src/components/createConnect.js b/src/components/createConnect.js index d8b7ef943..26606cfa3 100644 --- a/src/components/createConnect.js +++ b/src/components/createConnect.js @@ -5,11 +5,11 @@ import isPlainObject from '../utils/isPlainObject'; import wrapActionCreators from '../utils/wrapActionCreators'; import invariant from 'invariant'; -const emptySelector = () => ({}); -const defaultBinder = dispatch => ({ dispatch }); -const identityMerge = (slice, actionsCreators, props) => ({ +const defaultMapState = () => ({}); +const defaultMapDispatch = dispatch => ({ dispatch }); +const defaultMergeProps = (stateSlice, actionsCreators, props) => ({ ...props, - ...slice, + ...stateSlice, ...actionsCreators }); @@ -22,16 +22,14 @@ export default function createConnect(React) { const storeShape = createStoreShape(PropTypes); return function connect( - select, - dispatchBinder = defaultBinder, - mergeHandler = identityMerge + mapState = defaultMapState, + mapDispatchOrActionCreators = defaultMapDispatch, + mergeProps = defaultMergeProps ) { - const shouldSubscribe = select ? true : false; - const selectState = select || emptySelector; - const bindDispatch = isPlainObject(dispatchBinder) ? - wrapActionCreators(dispatchBinder) : - dispatchBinder; - const merge = mergeHandler; + const shouldSubscribe = mapState !== defaultMapState; + const mapDispatch = isPlainObject(mapDispatchOrActionCreators) ? + wrapActionCreators(mapDispatchOrActionCreators) : + mapDispatchOrActionCreators; return DecoratedComponent => class ConnectDecorator extends Component { static displayName = `Connect(${getDisplayName(DecoratedComponent)})`; @@ -63,8 +61,8 @@ export default function createConnect(React) { super(props, context); this.setUnderlyingRef = ::this.setUnderlyingRef; this.state = { - ...this.selectState(props, context), - ...this.bindDispatch(context) + ...this.mapState(props, context), + ...this.mapDispatch(context) }; } @@ -82,32 +80,32 @@ export default function createConnect(React) { } handleChange(props = this.props) { - const nextState = this.selectState(props, this.context); + const nextState = this.mapState(props, this.context); if (!this.isSliceEqual(this.state.slice, nextState.slice)) { this.setState(nextState); } } - selectState(props = this.props, context = this.context) { + mapState(props = this.props, context = this.context) { const state = context.store.getState(); - const slice = selectState(state); + const slice = mapState(state); invariant( isPlainObject(slice), - 'The return value of `select` prop must be an object. Instead received %s.', + '`mapState` must return an object. Instead received %s.', slice ); return { slice }; } - bindDispatch(context = this.context) { + mapDispatch(context = this.context) { const { dispatch } = context.store; - const actionCreators = bindDispatch(dispatch); + const actionCreators = mapDispatch(dispatch); invariant( isPlainObject(actionCreators), - 'The return value of `bindDispatch` prop must be an object. Instead received %s.', + '`mapDispatch` must return an object. Instead received %s.', actionCreators ); @@ -116,11 +114,11 @@ export default function createConnect(React) { merge(props = this.props, state = this.state) { const { slice, actionCreators } = state; - const merged = merge(slice, actionCreators, props); + const merged = mergeProps(slice, actionCreators, props); invariant( isPlainObject(merged), - 'The return value of `merge` prop must be an object. Instead received %s.', + '`mergeProps` must return an object. Instead received %s.', merged ); diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 787cc81cf..790e1a7bb 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -244,7 +244,7 @@ describe('React', () => { expect(decorated.subscribed).toBe(true); }); - it('should not subscribe to stores if select argument is falsy', () => { + it('should not subscribe to stores if mapState argument is falsy', () => { const store = createStore(() => ({ foo: 'bar' })); @@ -346,12 +346,12 @@ describe('React', () => { expect(spy.calls.length).toBe(3); }); - it('should throw an error if select, bindDispatch, or merge returns anything but a plain object', () => { + it('should throw an error if mapState, mapDispatch, or mergeProps returns anything but a plain object', () => { const store = createStore(() => ({})); - function makeContainer(select, bindActionCreators, merge) { + function makeContainer(mapState, mapDispatch, mergeProps) { return React.createElement( - @connect(select, bindActionCreators, merge) + @connect(mapState, mapDispatch, mergeProps) class Container extends Component { render() { return
; @@ -368,7 +368,7 @@ describe('React', () => { { () => makeContainer(() => 1, () => ({}), () => ({})) } ); - }).toThrow(/select/); + }).toThrow(/mapState/); expect(() => { TestUtils.renderIntoDocument( @@ -376,7 +376,7 @@ describe('React', () => { { () => makeContainer(() => 'hey', () => ({}), () => ({})) } ); - }).toThrow(/select/); + }).toThrow(/mapState/); expect(() => { TestUtils.renderIntoDocument( @@ -384,7 +384,7 @@ describe('React', () => { { () => makeContainer(() => new AwesomeMap(), () => ({}), () => ({})) } ); - }).toThrow(/select/); + }).toThrow(/mapState/); expect(() => { TestUtils.renderIntoDocument( @@ -392,7 +392,7 @@ describe('React', () => { { () => makeContainer(() => ({}), () => 1, () => ({})) } ); - }).toThrow(/bindDispatch/); + }).toThrow(/mapDispatch/); expect(() => { TestUtils.renderIntoDocument( @@ -400,7 +400,7 @@ describe('React', () => { { () => makeContainer(() => ({}), () => 'hey', () => ({})) } ); - }).toThrow(/bindDispatch/); + }).toThrow(/mapDispatch/); expect(() => { TestUtils.renderIntoDocument( @@ -408,7 +408,7 @@ describe('React', () => { { () => makeContainer(() => ({}), () => new AwesomeMap(), () => ({})) } ); - }).toThrow(/bindDispatch/); + }).toThrow(/mapDispatch/); expect(() => { TestUtils.renderIntoDocument( @@ -416,7 +416,7 @@ describe('React', () => { { () => makeContainer(() => ({}), () => ({}), () => 1) } ); - }).toThrow(/merge/); + }).toThrow(/mergeProps/); expect(() => { TestUtils.renderIntoDocument( @@ -424,7 +424,7 @@ describe('React', () => { { () => makeContainer(() => ({}), () => ({}), () => 'hey') } ); - }).toThrow(/merge/); + }).toThrow(/mergeProps/); expect(() => { TestUtils.renderIntoDocument( @@ -432,7 +432,7 @@ describe('React', () => { { () => makeContainer(() => ({}), () => ({}), () => new AwesomeMap()) } ); - }).toThrow(/merge/); + }).toThrow(/mergeProps/); }); it('should set the displayName correctly', () => { From 3079d0045f8ced7c79db9b255172295d2294ae51 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Aug 2015 16:21:56 +0300 Subject: [PATCH 18/31] Update README.md --- README.md | 120 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 107 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index d1060f4f1..8906bc938 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ For React Native, import from `react-redux/native` instead. - [Quick Start](#quick-start) - [API](#api) - - [`connect`](#connect) - - [`Provider`](#provider) + - [`connect([mapState], [mapDispatch], [mergeProps])`](#connect) + - [``](#provider) - [License](#license) ## Quick Start @@ -89,7 +89,7 @@ import Counter from '../components/Counter'; // Assuming these are Redux action creators import { increment } from './actionCreators'; -function select(state) { +function mapState(state) { // Which part of the Redux global state does our component want to receive as props? return { counter: state.counter @@ -99,7 +99,7 @@ function select(state) { class CounterContainer extends Component { render() { // connect() call below will inject `dispatch` and - // every key returned by `select` as props into our container: + // every key returned by `mapState` as props into our container: const { dispatch, counter } = this.props; // render our “dumb” component, hooking up state to data props @@ -114,13 +114,13 @@ class CounterContainer extends Component { } // Don't forget to actually use connect! -export default connect(select)(CounterContainer); +export default connect(mapState)(CounterContainer); // You might have noticed that we used parens twice. // This is called partial applications, and it lets people // use ES7 decorator proposal syntax: // -// @connect(select) +// @connect(mapState) // export default class CounterContainer { ... } // // Don’t forget decorators are experimental! And they @@ -144,7 +144,7 @@ import { bindActionCreators } from 'redux'; import * as CounterActionCreators from './actionCreators'; import Counter from '../components/Counter'; -function select(state) { +function mapState(state) { return { counter: state.counter }; @@ -165,7 +165,7 @@ class CounterContainer extends Component { } // Don't forget to actually use connect! -export default connect(select)(CounterContainer); +export default connect(mapState)(CounterContainer); ``` You can have many `connect()`-ed components in your app at any depth, and you can even nest them. It is however preferable that you try to only `connect()` top-level components such as route handlers, so the data flow in your application stays predictable. @@ -216,17 +216,110 @@ React.render(( ## API -### `connect` +### `connect([mapState], [mapDispatch], [mergeProps])` ```js -export default connect(select)(MyComponent); +// Inject just `dispatch` and don't listen to store +export default connect()(TodoApp); + +// Inject `dispatch` and every field in the global state (SLOW!) +export default connect(state => state)(TodoApp); + +// Inject `dispatch` and `todos` +function mapState(state) { + return { todos: state.todos }; +} +export default connect(mapState)(TodoApp); + +// Inject `todos` and all action creators (`addTodo`, `completeTodo`, ...) +import * as actionCreators from './actionCreators'; +function mapState(state) { + return { todos: state.todos }; +} +export default connect(mapState, actionCreators)(TodoApp); + +// Inject `todos` and all action creators (`addTodo`, `completeTodo`, ...) as `actions` +import * as actionCreators from './actionCreators'; +import { bindActionCreators } from 'redux'; +function mapState(state) { + return { todos: state.todos }; +} +function mapDispatch(dispatch) { + return { actions: bindActionCreators(actionCreators, dispatch) }; +} +export default connect(mapState, actionCreators)(TodoApp); + +// Inject `todos` and a specific action creator (`addTodo`) +import { addTodo } from './actionCreators'; +import { bindActionCreators } from 'redux'; +function mapState(state) { + return { todos: state.todos }; +} +function mapDispatch(dispatch) { + return { addTodo: bindActionCreators(addTodo, dispatch) }; +} +export default connect(mapState, mapDispatch)(TodoApp); + +// Inject `todos`, todoActionCreators as `todoActions`, and counterActionCreators as `counterActions` +import * as todoActionCreators from './todoActionCreators'; +import * as counterActionCreators from './counterActionCreators'; +import { bindActionCreators } from 'redux'; +function mapState(state) { + return { todos: state.todos }; +} +function mapDispatch(dispatch) { + return { + todoActions: bindActionCreators(todoActionCreators, dispatch), + counterActions: bindActionCreators(counterActionCreators, dispatch) + }; +} +export default connect(mapState, mapDispatch)(TodoApp); + +// Inject `todos`, and todoActionCreators and counterActionCreators together as `actions` +import * as todoActionCreators from './todoActionCreators'; +import * as counterActionCreators from './counterActionCreators'; +import { bindActionCreators } from 'redux'; +function mapState(state) { + return { todos: state.todos }; +} +function mapDispatch(dispatch) { + return { + actions: bindActionCreators({ ...todoActionCreators, ...counterActionCreators }, dispatch) + }; +} +export default connect(mapState, mapDispatch)(TodoApp); + +// Inject `todos`, and all todoActionCreators and counterActionCreators directly as props +import * as todoActionCreators from './todoActionCreators'; +import * as counterActionCreators from './counterActionCreators'; +import { bindActionCreators } from 'redux'; +function mapState(state) { + return { todos: state.todos }; +} +function mapDispatch(dispatch) { + return bindActionCreators({ ...todoActionCreators, ...counterActionCreators }, dispatch); +} +export default connect(mapState, mapDispatch)(TodoApp); + +// Inject `todos` of a specific user depending on props, and inject `props.userId` into the action +import * as actionCreators from './actionCreators'; +function mapState(state) { + return { todos: state.todos }; +} +function mergeProps(selectedState, boundActions, props) { + return Object.assign({}, props, { + todos: selectedState.todos[props.userId], + addTodo: (text) => boundActions.addTodo(props.userId, text) + }); +} +export default connect(mapState, actionCreators)(TodoApp); ``` Returns a component class that injects the Redux Store’s `dispatch` as a prop into `Component` so it can dispatch Redux actions. -The returned component also subscribes to the updates of Redux store. Any time the state changes, it calls the `select` function passed to it. The selector function takes a single argument of the entire Redux store’s state and returns an object to be passed as props. Use [reselect](https://github.com/faassen/reselect) to efficiently compose selectors and memoize derived data. +The returned component also subscribes to the updates of Redux store. Any time the state changes, it calls the `mapState` function passed to it. It is called a **selector**. The selector function takes a single argument of the entire Redux store’s state and returns an object to be passed as props. Use [reselect](https://github.com/faassen/reselect) to efficiently compose selectors and memoize derived data. -Both `dispatch` and every property returned by `select` will be provided to your `Component` as `props`. +Both `dispatch` and every property returned by `mapState` will be provided to your `Component` as `props`. It is the responsibility of a Smart Component to bind action creators to the given `dispatch` function and pass those bound creators to Dumb Components. Redux provides a `bindActionCreators` to streamline the process of binding action @@ -236,9 +329,10 @@ creators to the dispatch function. See the usage example in the quick start above. -### `Provider` +### `` ```js +// Make store available to connect() below in hierarchy {() => } From 423a5908db3f2bd2563b46eed0acf292829415f1 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Aug 2015 16:24:29 +0300 Subject: [PATCH 19/31] Update README.md --- README.md | 89 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 61 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 8906bc938..0d926e9cc 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ For React Native, import from `react-redux/native` instead. - [API](#api) - [`connect([mapState], [mapDispatch], [mergeProps])`](#connect) - [``](#provider) +- [Recipes](#recipes) - [License](#license) ## Quick Start @@ -218,130 +219,162 @@ React.render(( ### `connect([mapState], [mapDispatch], [mergeProps])` +Returns a component class that injects the Redux Store’s `dispatch` as a prop into `Component` so it can dispatch Redux actions. + +The returned component also subscribes to the updates of Redux store. Any time the state changes, it calls the `mapState` function passed to it. It is called a **selector**. The selector function takes a single argument of the entire Redux store’s state and returns an object to be passed as props. Use [reselect](https://github.com/faassen/reselect) to efficiently compose selectors and memoize derived data. + +Both `dispatch` and every property returned by `mapState` will be provided to your `Component` as `props`. + +It is the responsibility of a Smart Component to bind action creators to the given `dispatch` function and pass those +bound creators to Dumb Components. Redux provides a `bindActionCreators` to streamline the process of binding action +creators to the dispatch function. + +**To use `connect()`, the root component of your app must be wrapped into `{() => ... }` before being rendered.** + +See the usage example in the quick start above. + +### `` + +```js +// Make store available to connect() below in hierarchy + + {() => } + +``` + +The `Provider` component takes a `store` prop and a [function as a child](#child-must-be-a-function) with your root +component. The `store` is then passed to the child via React's `context`. This is the entry point for Redux and must be +present in order to use the `connect` component. + +## Recipes + ```js // Inject just `dispatch` and don't listen to store export default connect()(TodoApp); + // Inject `dispatch` and every field in the global state (SLOW!) export default connect(state => state)(TodoApp); + // Inject `dispatch` and `todos` function mapState(state) { return { todos: state.todos }; } + export default connect(mapState)(TodoApp); + // Inject `todos` and all action creators (`addTodo`, `completeTodo`, ...) import * as actionCreators from './actionCreators'; + function mapState(state) { return { todos: state.todos }; } + export default connect(mapState, actionCreators)(TodoApp); + // Inject `todos` and all action creators (`addTodo`, `completeTodo`, ...) as `actions` import * as actionCreators from './actionCreators'; import { bindActionCreators } from 'redux'; + function mapState(state) { return { todos: state.todos }; } + function mapDispatch(dispatch) { return { actions: bindActionCreators(actionCreators, dispatch) }; } + export default connect(mapState, actionCreators)(TodoApp); + // Inject `todos` and a specific action creator (`addTodo`) import { addTodo } from './actionCreators'; import { bindActionCreators } from 'redux'; + function mapState(state) { return { todos: state.todos }; } + function mapDispatch(dispatch) { return { addTodo: bindActionCreators(addTodo, dispatch) }; } + export default connect(mapState, mapDispatch)(TodoApp); + // Inject `todos`, todoActionCreators as `todoActions`, and counterActionCreators as `counterActions` import * as todoActionCreators from './todoActionCreators'; import * as counterActionCreators from './counterActionCreators'; import { bindActionCreators } from 'redux'; + function mapState(state) { return { todos: state.todos }; } + function mapDispatch(dispatch) { return { todoActions: bindActionCreators(todoActionCreators, dispatch), counterActions: bindActionCreators(counterActionCreators, dispatch) }; } + export default connect(mapState, mapDispatch)(TodoApp); + // Inject `todos`, and todoActionCreators and counterActionCreators together as `actions` import * as todoActionCreators from './todoActionCreators'; import * as counterActionCreators from './counterActionCreators'; import { bindActionCreators } from 'redux'; + function mapState(state) { return { todos: state.todos }; } + function mapDispatch(dispatch) { return { actions: bindActionCreators({ ...todoActionCreators, ...counterActionCreators }, dispatch) }; } + export default connect(mapState, mapDispatch)(TodoApp); + // Inject `todos`, and all todoActionCreators and counterActionCreators directly as props import * as todoActionCreators from './todoActionCreators'; import * as counterActionCreators from './counterActionCreators'; import { bindActionCreators } from 'redux'; + function mapState(state) { return { todos: state.todos }; } + function mapDispatch(dispatch) { - return bindActionCreators({ ...todoActionCreators, ...counterActionCreators }, dispatch); + return bindActionCreators(Object.assign({}, todoActionCreators, counterActionCreators), dispatch); } + export default connect(mapState, mapDispatch)(TodoApp); + // Inject `todos` of a specific user depending on props, and inject `props.userId` into the action import * as actionCreators from './actionCreators'; + function mapState(state) { return { todos: state.todos }; } + function mergeProps(selectedState, boundActions, props) { return Object.assign({}, props, { todos: selectedState.todos[props.userId], addTodo: (text) => boundActions.addTodo(props.userId, text) }); } -export default connect(mapState, actionCreators)(TodoApp); -``` - -Returns a component class that injects the Redux Store’s `dispatch` as a prop into `Component` so it can dispatch Redux actions. - -The returned component also subscribes to the updates of Redux store. Any time the state changes, it calls the `mapState` function passed to it. It is called a **selector**. The selector function takes a single argument of the entire Redux store’s state and returns an object to be passed as props. Use [reselect](https://github.com/faassen/reselect) to efficiently compose selectors and memoize derived data. - -Both `dispatch` and every property returned by `mapState` will be provided to your `Component` as `props`. - -It is the responsibility of a Smart Component to bind action creators to the given `dispatch` function and pass those -bound creators to Dumb Components. Redux provides a `bindActionCreators` to streamline the process of binding action -creators to the dispatch function. - -**To use `connect()`, the root component of your app must be wrapped into `{() => ... }` before being rendered.** - -See the usage example in the quick start above. - -### `` -```js -// Make store available to connect() below in hierarchy - - {() => } - +export default connect(mapState, actionCreators)(TodoApp); ``` -The `Provider` component takes a `store` prop and a [function as a child](#child-must-be-a-function) with your root -component. The `store` is then passed to the child via React's `context`. This is the entry point for Redux and must be -present in order to use the `connect` component. - ## License MIT From 817e710bb017dbc8b11f309679f7666a4eaf1200 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Aug 2015 16:28:43 +0300 Subject: [PATCH 20/31] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0d926e9cc..74a5f1aa6 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ For React Native, import from `react-redux/native` instead. - [Quick Start](#quick-start) - [API](#api) - - [`connect([mapState], [mapDispatch], [mergeProps])`](#connect) - - [``](#provider) + - [`connect([mapState], [mapDispatch], [mergeProps])`](#connectmapstate-mapdispatch-mergeprops) + - [``](#provider-store) - [Recipes](#recipes) - [License](#license) From fcbce6f54815a14a0eefd0878f7dceeb586caeda Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Aug 2015 16:30:21 +0300 Subject: [PATCH 21/31] Update README.md From 06a060dfdae194ca7598d9c11643087deeeda8c3 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Aug 2015 16:30:28 +0300 Subject: [PATCH 22/31] Update README.md From 25946cf872cd7f0f58a4cf6e05918e433a67e9b0 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Aug 2015 16:30:35 +0300 Subject: [PATCH 23/31] Update README.md From 65ce19725b1a6530ad6571e78aecec2f7bfc66f5 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Aug 2015 16:46:44 +0300 Subject: [PATCH 24/31] Update README.md --- README.md | 144 ++++++++++++++++++++++-------------------------------- 1 file changed, 59 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 74a5f1aa6..2091096a9 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,14 @@ react-redux ========================= -Higher-order React components for [Redux](https://github.com/gaearon/redux). +Official React bindings for [Redux](https://github.com/gaearon/redux). +Performant and flexible. -What you get from `react-redux` is for React. -For React Native, import from `react-redux/native` instead. - ->**Note: There is a project called “redux-react” on NPM that is completely unrelated to the official bindings. This documentation (and any other official Redux documentation) is for `react-redux`.** +>**Note: There is a project called `redux-react` on NPM that is [completely unrelated](https://github.com/cgarvis/redux-react/issues/1) to the official bindings. This documentation (and any other official Redux documentation) is for `react-redux`.** ## Table of Contents +- [React Native](#react-native) - [Quick Start](#quick-start) - [API](#api) - [`connect([mapState], [mapDispatch], [mergeProps])`](#connectmapstate-mapdispatch-mergeprops) @@ -17,6 +16,11 @@ For React Native, import from `react-redux/native` instead. - [Recipes](#recipes) - [License](#license) +## React Native + +What you get from `react-redux` is for React. +For React Native, import from `react-redux/native` instead. + ## Quick Start React bindings for Redux embrace the idea of [dividing “smart” and “dumb” components](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0). @@ -75,10 +79,11 @@ export default class Counter extends Component { Here’s how we hook it up to the Redux Store. -We will use `connect()` function provided by `react-redux` to turn a “dumb” `Counter` into a smart component. With the current API, we’ll need to add an intermediate `CounterContainer` component, but we will soon make `connect` API more powerful so this won’t be required. The `connect()` function lets you specify *which exactly* state from the Redux store your component wants to track. This lets you subscribe on any level of granularity. +We will use `connect()` function provided by `react-redux` to turn a “dumb” `Counter` into a smart component. The `connect()` function lets you specify *which exactly* state from the Redux store your component wants to track. This lets you subscribe on any level of granularity. + +Passing action creator functions as the second parameter will bind them to the specific store instance, and they will be injected as props with the same names they were exported with. -Our `CounterContainer` that’s necessary to hook `Counter` up to a Redux store looks like this: -(This will be much less verbose in the next versions.) +Why don’t we bind action creators to a store right away? This is because of the so-called “universal” apps that need to render on the server. They would have a different store instance for every request, so we don’t know the store instance during the definition! ```js import { Component } from 'react'; @@ -87,89 +92,40 @@ import { connect } from 'react-redux'; // Assuming this is our “dumb” counter import Counter from '../components/Counter'; -// Assuming these are Redux action creators -import { increment } from './actionCreators'; +// Assuming action creators as named exports: +import * as counterActionCreators from '../actionsCreators'; +// Which part of the Redux global state does our component want to receive as props? function mapState(state) { - // Which part of the Redux global state does our component want to receive as props? return { counter: state.counter }; } -class CounterContainer extends Component { - render() { - // connect() call below will inject `dispatch` and - // every key returned by `mapState` as props into our container: - const { dispatch, counter } = this.props; - - // render our “dumb” component, hooking up state to data props - // and using “dispatch action produced by this action creator” as callbacks. - // this is a “bridge” between a Redux-aware world above and Redux-unaware world below. - - return ( - dispatch(increment())} /> - ); - } -} - // Don't forget to actually use connect! -export default connect(mapState)(CounterContainer); - -// You might have noticed that we used parens twice. -// This is called partial applications, and it lets people -// use ES7 decorator proposal syntax: -// -// @connect(mapState) -// export default class CounterContainer { ... } -// -// Don’t forget decorators are experimental! And they -// desugar to function calls anyway as example above demonstrates. +export default connect(mapState, counterActionCreators)(CounterContainer); ``` -As you can see, action creators in Redux just return actions, but we need to manually “bind” them to the `dispatch` function for our Redux store. Why don’t we bind action creators to a store right away? This is because of the so-called “universal” apps that need to render on the server. They would have a different store instance for every request, so we don’t know the store instance during the definition! +### Usage Notes -### Binding many action creators - -Binding can get cumbersome, so Redux provides a `bindActionCreators` helper to turn many action creator methods into an object with methods called the same, but bound to a particular `dispatch` function: - -```js +You can have many `connect()`-ed components in your app at any depth, and you can even nest them. It is however preferable that you try to only `connect()` top-level components such as route handlers, so the data flow in your application stays predictable. -import { Component } from 'react'; -import { connect } from 'react-redux'; +### Support for Decorators -// A helper provided by Redux! -import { bindActionCreators } from 'redux'; -// Import many action creators as a single object (like `require('./actionCreators')` in CommonJS) -import * as CounterActionCreators from './actionCreators'; -import Counter from '../components/Counter'; +You might have noticed that we used parens twice. This is called partial applications, and it lets people +use ES7 decorator proposal syntax: -function mapState(state) { - return { - counter: state.counter - }; -} - -class CounterContainer extends Component { - render() { - const { dispatch, counter } = this.props; - - // This time, we use `bindActionCreators` to bind many action creators - // to a particular dispatch function from our Redux store. +```js +// Unstable syntax! It might change or break in production. +@connect(mapState) +export default class CounterContainer { ... } +``` - return ( - - ); - } -} +Don’t forget decorators are experimental! And they desugar to function calls anyway as example above demonstrates. -// Don't forget to actually use connect! -export default connect(mapState)(CounterContainer); -``` +### Additional Flexibility -You can have many `connect()`-ed components in your app at any depth, and you can even nest them. It is however preferable that you try to only `connect()` top-level components such as route handlers, so the data flow in your application stays predictable. +This the most basic usage, but `connect()` supports many other different patterns: just passing the vanilla `dispatch()` function down, binding multiple action creators, putting them as `actions` prop, selecting parts of state and binding action creators depending on `props`, and so on. Check out [Recipes](#recipes) for some ideas about advanced `connect()` usage. ### Injecting Redux store @@ -248,24 +204,30 @@ present in order to use the `connect` component. ## Recipes +##### Inject just `dispatch` and don't listen to store + ```js -// Inject just `dispatch` and don't listen to store export default connect()(TodoApp); +``` - -// Inject `dispatch` and every field in the global state (SLOW!) +##### Inject `dispatch` and every field in the global state (SLOW!) +```js export default connect(state => state)(TodoApp); +``` +##### Inject `dispatch` and `todos` -// Inject `dispatch` and `todos` +```js function mapState(state) { return { todos: state.todos }; } export default connect(mapState)(TodoApp); +``` +##### Inject `todos` and all action creators (`addTodo`, `completeTodo`, ...) -// Inject `todos` and all action creators (`addTodo`, `completeTodo`, ...) +```js import * as actionCreators from './actionCreators'; function mapState(state) { @@ -273,9 +235,11 @@ function mapState(state) { } export default connect(mapState, actionCreators)(TodoApp); +``` +##### Inject `todos` and all action creators (`addTodo`, `completeTodo`, ...) as `actions` -// Inject `todos` and all action creators (`addTodo`, `completeTodo`, ...) as `actions` +```js import * as actionCreators from './actionCreators'; import { bindActionCreators } from 'redux'; @@ -288,9 +252,11 @@ function mapDispatch(dispatch) { } export default connect(mapState, actionCreators)(TodoApp); +``` +##### Inject `todos` and a specific action creator (`addTodo`) -// Inject `todos` and a specific action creator (`addTodo`) +```js import { addTodo } from './actionCreators'; import { bindActionCreators } from 'redux'; @@ -303,9 +269,11 @@ function mapDispatch(dispatch) { } export default connect(mapState, mapDispatch)(TodoApp); +``` +##### Inject `todos`, todoActionCreators as `todoActions`, and counterActionCreators as `counterActions` -// Inject `todos`, todoActionCreators as `todoActions`, and counterActionCreators as `counterActions` +```js import * as todoActionCreators from './todoActionCreators'; import * as counterActionCreators from './counterActionCreators'; import { bindActionCreators } from 'redux'; @@ -322,9 +290,11 @@ function mapDispatch(dispatch) { } export default connect(mapState, mapDispatch)(TodoApp); +``` +##### Inject `todos`, and todoActionCreators and counterActionCreators together as `actions` -// Inject `todos`, and todoActionCreators and counterActionCreators together as `actions` +```js import * as todoActionCreators from './todoActionCreators'; import * as counterActionCreators from './counterActionCreators'; import { bindActionCreators } from 'redux'; @@ -340,9 +310,11 @@ function mapDispatch(dispatch) { } export default connect(mapState, mapDispatch)(TodoApp); +``` +##### Inject `todos`, and all todoActionCreators and counterActionCreators directly as props -// Inject `todos`, and all todoActionCreators and counterActionCreators directly as props +```js import * as todoActionCreators from './todoActionCreators'; import * as counterActionCreators from './counterActionCreators'; import { bindActionCreators } from 'redux'; @@ -356,9 +328,11 @@ function mapDispatch(dispatch) { } export default connect(mapState, mapDispatch)(TodoApp); +``` +##### Inject `todos` of a specific user depending on props, and inject `props.userId` into the action -// Inject `todos` of a specific user depending on props, and inject `props.userId` into the action +```js import * as actionCreators from './actionCreators'; function mapState(state) { From 4a3d6999486615b6c89c7993630aebf5c141e04c Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Aug 2015 17:03:47 +0300 Subject: [PATCH 25/31] Update README.md --- README.md | 65 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 2091096a9..743a078d6 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,8 @@ Performant and flexible. - [React Native](#react-native) - [Quick Start](#quick-start) - [API](#api) - - [`connect([mapState], [mapDispatch], [mergeProps])`](#connectmapstate-mapdispatch-mergeprops) - [``](#provider-store) -- [Recipes](#recipes) + - [`connect([mapState], [mapDispatch], [mergeProps])(Component)`](#connectmapstate-mapdispatch-mergeprops-component) - [License](#license) ## React Native @@ -85,6 +84,8 @@ Passing action creator functions as the second parameter will bind them to the s Why don’t we bind action creators to a store right away? This is because of the so-called “universal” apps that need to render on the server. They would have a different store instance for every request, so we don’t know the store instance during the definition! +##### `containers/CounterContainer.js` + ```js import { Component } from 'react'; import { connect } from 'react-redux'; @@ -106,6 +107,9 @@ function mapState(state) { export default connect(mapState, counterActionCreators)(CounterContainer); ``` +Whether to put `connect()` call in the same file as the “dumb” component, or separately, is up to you. +Ask yourself whether you'd want to reuse this component but bind it to different data, or not. + ### Usage Notes You can have many `connect()`-ed components in your app at any depth, and you can even nest them. It is however preferable that you try to only `connect()` top-level components such as route handlers, so the data flow in your application stays predictable. @@ -125,7 +129,7 @@ Don’t forget decorators are experimental! And they desugar to function calls a ### Additional Flexibility -This the most basic usage, but `connect()` supports many other different patterns: just passing the vanilla `dispatch()` function down, binding multiple action creators, putting them as `actions` prop, selecting parts of state and binding action creators depending on `props`, and so on. Check out [Recipes](#recipes) for some ideas about advanced `connect()` usage. +This the most basic usage, but `connect()` supports many other different patterns: just passing the vanilla `dispatch()` function down, binding multiple action creators, putting them as `actions` prop, selecting parts of state and binding action creators depending on `props`, and so on. Check out `connect()` docs below to learn more. ### Injecting Redux store @@ -173,36 +177,51 @@ React.render(( ## API -### `connect([mapState], [mapDispatch], [mergeProps])` - -Returns a component class that injects the Redux Store’s `dispatch` as a prop into `Component` so it can dispatch Redux actions. - -The returned component also subscribes to the updates of Redux store. Any time the state changes, it calls the `mapState` function passed to it. It is called a **selector**. The selector function takes a single argument of the entire Redux store’s state and returns an object to be passed as props. Use [reselect](https://github.com/faassen/reselect) to efficiently compose selectors and memoize derived data. - -Both `dispatch` and every property returned by `mapState` will be provided to your `Component` as `props`. +### `` -It is the responsibility of a Smart Component to bind action creators to the given `dispatch` function and pass those -bound creators to Dumb Components. Redux provides a `bindActionCreators` to streamline the process of binding action -creators to the dispatch function. +Makes Redux store available to the `connect()` calls in the component hierarchy below. +You can’t use `connect()` without wrapping the root component in ``. -**To use `connect()`, the root component of your app must be wrapped into `{() => ... }` before being rendered.** +#### Props -See the usage example in the quick start above. +* `store`: (*[Redux Store](http://gaearon.github.io/redux/docs/api/Store.html)*): The single Redux store in your application. +* `children`: (*Function*): Unlike most React components, `` accepts a [function as a child](#child-must-be-a-function) with your root component. This is a temporary workaround for a React 0.13 context issue, which will be fixed when React 0.14 comes out. -### `` +#### Example ```js // Make store available to connect() below in hierarchy - - {() => } - +React.render( + + {() => } + , + rootEl +); ``` -The `Provider` component takes a `store` prop and a [function as a child](#child-must-be-a-function) with your root -component. The `store` is then passed to the child via React's `context`. This is the entry point for Redux and must be -present in order to use the `connect` component. +### `connect([mapState], [mapDispatch], [mergeProps])(Component)` + +Connects a React component to a Redux store. + +#### Arguments + +* [`mapState`] (*Function*): If specified, the component will subscribe to Redux store updates. Any time it updates, `mapState` will be called. Its result must be a plain object, and it will be merged into the component’s props. If you omit it, the component will not be subscribed to the Redux store. + +* [`mapDispatch`] (*Object* or *Function*): If an object is passed, each function inside it will be assumed to be a Redux action creator, and an object with the same function names, but bound to a Redux store, will be merged into the component’s props. If a function is passed, it will be given `dispatch`. It’s up to you to return an object that somehow uses `dispatch` to bind action creators in your own way. (Tip: you may use `bindActionCreators` helper from Redux.) If you pass omit it, the default implementation just injects `dispatch` into your component’s props. + +* [`mergeProps`] (*Function*): If specified, it is passed the result of `mapState()`, `mapDispatch()`, and the parent `props`. The plain object you return from it will be passed as props to the wrapped component. You may specify this function to select a slice of the state based on props, or to bind action creators to a particular variable from props. If you omit it, `{ ...props, ...mapStateResult, ...mapDispatchResult }` is used by default. + +#### Returns + +A React component class that injects state and action creators into your component according to the specified options. + +#### Remarks + +* The `mapState` function takes a single argument of the entire Redux store’s state and returns an object to be passed as props. It is often called a **selector**. Use [reselect](https://github.com/faassen/reselect) to efficiently compose selectors and [compute derived data](http://gaearon.github.io/redux/docs/recipes/ComputingDerivedData.html). + +* **To use `connect()`, the root component of your app must be wrapped into `{() => ... }` before being rendered.** -## Recipes +#### Examples ##### Inject just `dispatch` and don't listen to store From 9ed8f4826e295406a286a510e6930a6b39e78272 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Aug 2015 17:05:21 +0300 Subject: [PATCH 26/31] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 743a078d6..96ca1ad3a 100644 --- a/README.md +++ b/README.md @@ -205,11 +205,11 @@ Connects a React component to a Redux store. #### Arguments -* [`mapState`] (*Function*): If specified, the component will subscribe to Redux store updates. Any time it updates, `mapState` will be called. Its result must be a plain object, and it will be merged into the component’s props. If you omit it, the component will not be subscribed to the Redux store. +* [`mapState`] \(*Function*): If specified, the component will subscribe to Redux store updates. Any time it updates, `mapState` will be called. Its result must be a plain object, and it will be merged into the component’s props. If you omit it, the component will not be subscribed to the Redux store. -* [`mapDispatch`] (*Object* or *Function*): If an object is passed, each function inside it will be assumed to be a Redux action creator, and an object with the same function names, but bound to a Redux store, will be merged into the component’s props. If a function is passed, it will be given `dispatch`. It’s up to you to return an object that somehow uses `dispatch` to bind action creators in your own way. (Tip: you may use `bindActionCreators` helper from Redux.) If you pass omit it, the default implementation just injects `dispatch` into your component’s props. +* [`mapDispatch`] \(*Object* or *Function*): If an object is passed, each function inside it will be assumed to be a Redux action creator, and an object with the same function names, but bound to a Redux store, will be merged into the component’s props. If a function is passed, it will be given `dispatch`. It’s up to you to return an object that somehow uses `dispatch` to bind action creators in your own way. (Tip: you may use `bindActionCreators` helper from Redux.) If you pass omit it, the default implementation just injects `dispatch` into your component’s props. -* [`mergeProps`] (*Function*): If specified, it is passed the result of `mapState()`, `mapDispatch()`, and the parent `props`. The plain object you return from it will be passed as props to the wrapped component. You may specify this function to select a slice of the state based on props, or to bind action creators to a particular variable from props. If you omit it, `{ ...props, ...mapStateResult, ...mapDispatchResult }` is used by default. +* [`mergeProps`] \(*Function*): If specified, it is passed the result of `mapState()`, `mapDispatch()`, and the parent `props`. The plain object you return from it will be passed as props to the wrapped component. You may specify this function to select a slice of the state based on props, or to bind action creators to a particular variable from props. If you omit it, `{ ...props, ...mapStateResult, ...mapDispatchResult }` is used by default. #### Returns From 95bb962c96d6783b8a19c96cb90e65d5063878dc Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Aug 2015 17:07:09 +0300 Subject: [PATCH 27/31] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 96ca1ad3a..dadcc698e 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,7 @@ Connects a React component to a Redux store. * [`mapState`] \(*Function*): If specified, the component will subscribe to Redux store updates. Any time it updates, `mapState` will be called. Its result must be a plain object, and it will be merged into the component’s props. If you omit it, the component will not be subscribed to the Redux store. -* [`mapDispatch`] \(*Object* or *Function*): If an object is passed, each function inside it will be assumed to be a Redux action creator, and an object with the same function names, but bound to a Redux store, will be merged into the component’s props. If a function is passed, it will be given `dispatch`. It’s up to you to return an object that somehow uses `dispatch` to bind action creators in your own way. (Tip: you may use `bindActionCreators` helper from Redux.) If you pass omit it, the default implementation just injects `dispatch` into your component’s props. +* [`mapDispatch`] \(*Object* or *Function*): If an object is passed, each function inside it will be assumed to be a Redux action creator, and an object with the same function names, but bound to a Redux store, will be merged into the component’s props. If a function is passed, it will be given `dispatch`. It’s up to you to return an object that somehow uses `dispatch` to bind action creators in your own way. (Tip: you may use [`bindActionCreators()`](http://gaearon.github.io/redux/docs/api/bindActionCreators.html) helper from Redux.) If you omit it, the default implementation just injects `dispatch` into your component’s props. * [`mergeProps`] \(*Function*): If specified, it is passed the result of `mapState()`, `mapDispatch()`, and the parent `props`. The plain object you return from it will be passed as props to the wrapped component. You may specify this function to select a slice of the state based on props, or to bind action creators to a particular variable from props. If you omit it, `{ ...props, ...mapStateResult, ...mapDispatchResult }` is used by default. From 5fdeade9f97bec5af95f74b60778428e572f765f Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Aug 2015 17:09:39 +0300 Subject: [PATCH 28/31] Update README.md --- README.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index dadcc698e..7e34eec2e 100644 --- a/README.md +++ b/README.md @@ -90,12 +90,11 @@ Why don’t we bind action creators to a store right away? This is because of th import { Component } from 'react'; import { connect } from 'react-redux'; -// Assuming this is our “dumb” counter +// Action creators: +import { increment } from '../actionsCreators'; +// “Dumb” component: import Counter from '../components/Counter'; -// Assuming action creators as named exports: -import * as counterActionCreators from '../actionsCreators'; - // Which part of the Redux global state does our component want to receive as props? function mapState(state) { return { @@ -103,14 +102,17 @@ function mapState(state) { }; } -// Don't forget to actually use connect! -export default connect(mapState, counterActionCreators)(CounterContainer); +// First argument tells which state fields it’s interested in. +// Second argument tells which action creators to bind and inject. +// You may also pass a `dispatch` => Object function as a second argument. + +export default connect(mapState, { increment })(CounterContainer); ``` Whether to put `connect()` call in the same file as the “dumb” component, or separately, is up to you. Ask yourself whether you'd want to reuse this component but bind it to different data, or not. -### Usage Notes +### Nesting You can have many `connect()`-ed components in your app at any depth, and you can even nest them. It is however preferable that you try to only `connect()` top-level components such as route handlers, so the data flow in your application stays predictable. @@ -131,7 +133,7 @@ Don’t forget decorators are experimental! And they desugar to function calls a This the most basic usage, but `connect()` supports many other different patterns: just passing the vanilla `dispatch()` function down, binding multiple action creators, putting them as `actions` prop, selecting parts of state and binding action creators depending on `props`, and so on. Check out `connect()` docs below to learn more. -### Injecting Redux store +### Injecting Redux Store Finally, how do we actually hook it up to a Redux store? We need to create the store somewhere at the root of our component hierarchy. For client apps, the root component is a good place. For server rendering, you can do this in the request handler. From ea136fa67fd068925ded2f2e2b745d623926b5cb Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Aug 2015 17:12:07 +0300 Subject: [PATCH 29/31] Update README.md --- README.md | 46 ++++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 7e34eec2e..94d22526e 100644 --- a/README.md +++ b/README.md @@ -156,25 +156,6 @@ React.render(( {() => } ), targetEl); - -// or, if you use React Router 0.13, - -// Router.run(routes, Router.HistoryLocation, (Handler) => { -// React.render( -// -// {() => } -// , -// targetEl -// ); -// }); - -// or, if you use React Router 1.0, -// React.render( -// -// {() => ...} -// , -// targetEl -// ); ``` ## API @@ -191,8 +172,9 @@ You can’t use `connect()` without wrapping the root component in ``. #### Example +##### Vanilla React + ```js -// Make store available to connect() below in hierarchy React.render( {() => } @@ -201,6 +183,30 @@ React.render( ); ``` +##### React Router 0.13 + +```js +Router.run(routes, Router.HistoryLocation, (Handler) => { + React.render( + + {() => } + , + targetEl + ); +}); +``` + +##### React Router 1.0 + +```js +React.render( + + {() => ...} + , + targetEl +); +``` + ### `connect([mapState], [mapDispatch], [mergeProps])(Component)` Connects a React component to a Redux store. From 60eb884120f0c4989ee8575cb74880a985086b10 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Aug 2015 17:13:19 +0300 Subject: [PATCH 30/31] Update README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 94d22526e..d5c7723f9 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,14 @@ -react-redux +React Redux ========================= Official React bindings for [Redux](https://github.com/gaearon/redux). Performant and flexible. +[![npm version](https://img.shields.io/npm/v/react-redux.svg?style=flat-square)](https://www.npmjs.com/package/react-redux) +[![npm downloads](https://img.shields.io/npm/dm/react-redux.svg?style=flat-square)](https://www.npmjs.com/package/react-redux) +[![redux channel on slack](https://img.shields.io/badge/slack-redux@reactiflux-61DAFB.svg?style=flat-square)](http://www.reactiflux.com) + + >**Note: There is a project called `redux-react` on NPM that is [completely unrelated](https://github.com/cgarvis/redux-react/issues/1) to the official bindings. This documentation (and any other official Redux documentation) is for `react-redux`.** ## Table of Contents From 187532cefeec7b68b57a2a2c3617944053410183 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 7 Aug 2015 17:16:03 +0300 Subject: [PATCH 31/31] 0.5.0 --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index b620d2699..03b8e37e4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-redux", - "version": "0.4.0", - "description": "Redux bindings for React", + "version": "0.5.0", + "description": "React bindings for Redux", "main": "./lib/index.js", "scripts": { "build:lib": "babel src --out-dir lib", @@ -60,6 +60,6 @@ "invariant": "^2.0.0" }, "peerDependencies": { - "redux": "^1.0.0 || 1.0.0-alpha || 1.0.0-rc" + "redux": "^1.0.0 || 1.0.0-rc" } }