From c54ffd976a14ffba549df1ee6bf3b39f499aa734 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 15 Oct 2015 13:20:12 +0300 Subject: [PATCH 1/3] Drop support for React 0.13 --- native.js | 1 - package.json | 7 +- src/components/Provider.js | 51 +++++++ src/components/connect.js | 234 ++++++++++++++++++++++++++++++ src/components/createAll.js | 9 -- src/components/createConnect.js | 238 ------------------------------- src/components/createProvider.js | 107 -------------- src/index.js | 6 +- src/native.js | 4 - src/utils/createStoreShape.js | 7 - src/utils/storeShape.js | 7 + test/components/Provider.spec.js | 156 ++++---------------- test/components/connect.spec.js | 2 +- webpack.config.base.js | 1 - 14 files changed, 324 insertions(+), 506 deletions(-) delete mode 100644 native.js create mode 100644 src/components/Provider.js create mode 100644 src/components/connect.js delete mode 100644 src/components/createAll.js delete mode 100644 src/components/createConnect.js delete mode 100644 src/components/createProvider.js delete mode 100644 src/native.js delete mode 100644 src/utils/createStoreShape.js create mode 100644 src/utils/storeShape.js diff --git a/native.js b/native.js deleted file mode 100644 index bfea32da6..000000000 --- a/native.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./lib/native'); \ No newline at end of file diff --git a/package.json b/package.json index 8169b8331..8d432ccb3 100644 --- a/package.json +++ b/package.json @@ -50,9 +50,9 @@ "jsdom": "~5.4.3", "mocha": "^2.2.5", "mocha-jsdom": "~0.4.0", - "react": "^0.14.0-rc1", - "react-addons-test-utils": "^0.14.0-rc1", - "react-dom": "^0.14.0-rc1", + "react": "^0.14.0", + "react-addons-test-utils": "^0.14.0", + "react-dom": "^0.14.0", "redux": "^3.0.0", "rimraf": "^2.3.4", "webpack": "^1.11.0" @@ -62,6 +62,7 @@ "invariant": "^2.0.0" }, "peerDependencies": { + "react": "^0.14.0", "redux": "^2.0.0 || ^3.0.0" } } diff --git a/src/components/Provider.js b/src/components/Provider.js new file mode 100644 index 000000000..9acd761be --- /dev/null +++ b/src/components/Provider.js @@ -0,0 +1,51 @@ +import { Component, PropTypes, Children } from 'react'; +import storeShape from '../utils/storeShape'; + +let didWarnAboutReceivingStore = false; +function warnAboutReceivingStore() { + if (didWarnAboutReceivingStore) { + return; + } + + didWarnAboutReceivingStore = true; + console.error( // eslint-disable-line no-console + ' does not support changing `store` on the fly. ' + + 'It is most likely that you see this error because you updated to ' + + 'Redux 2.x and React Redux 2.x which no longer hot reload reducers ' + + 'automatically. See https://github.com/rackt/react-redux/releases/' + + 'tag/v2.0.0 for the migration instructions.' + ); +} + +export default class Provider extends Component { + getChildContext() { + return { store: this.store }; + } + + constructor(props, context) { + super(props, context); + this.store = props.store; + } + + componentWillReceiveProps(nextProps) { + const { store } = this; + const { store: nextStore } = nextProps; + + if (store !== nextStore) { + warnAboutReceivingStore(); + } + } + + render() { + let { children } = this.props; + return Children.only(children); + } +} + +Provider.propTypes = { + store: storeShape.isRequired, + children: PropTypes.element.isRequired +}; +Provider.childContextTypes = { + store: storeShape.isRequired +}; diff --git a/src/components/connect.js b/src/components/connect.js new file mode 100644 index 000000000..8ce0dc2ee --- /dev/null +++ b/src/components/connect.js @@ -0,0 +1,234 @@ +import React, { Component } from 'react'; +import storeShape from '../utils/storeShape'; +import shallowEqual from '../utils/shallowEqual'; +import isPlainObject from '../utils/isPlainObject'; +import wrapActionCreators from '../utils/wrapActionCreators'; +import hoistStatics from 'hoist-non-react-statics'; +import invariant from 'invariant'; + +const defaultMapStateToProps = () => ({}); +const defaultMapDispatchToProps = dispatch => ({ dispatch }); +const defaultMergeProps = (stateProps, dispatchProps, parentProps) => ({ + ...parentProps, + ...stateProps, + ...dispatchProps +}); + +function getDisplayName(WrappedComponent) { + return WrappedComponent.displayName || WrappedComponent.name || 'Component'; +} + +// Helps track hot reloading. +let nextVersion = 0; + +export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) { + const shouldSubscribe = Boolean(mapStateToProps); + const finalMapStateToProps = mapStateToProps || defaultMapStateToProps; + const finalMapDispatchToProps = isPlainObject(mapDispatchToProps) ? + wrapActionCreators(mapDispatchToProps) : + mapDispatchToProps || defaultMapDispatchToProps; + const finalMergeProps = mergeProps || defaultMergeProps; + const shouldUpdateStateProps = finalMapStateToProps.length > 1; + const shouldUpdateDispatchProps = finalMapDispatchToProps.length > 1; + const { pure = true } = options; + + // Helps track hot reloading. + const version = nextVersion++; + + function computeStateProps(store, props) { + const state = store.getState(); + const stateProps = shouldUpdateStateProps ? + finalMapStateToProps(state, props) : + finalMapStateToProps(state); + + invariant( + isPlainObject(stateProps), + '`mapStateToProps` must return an object. Instead received %s.', + stateProps + ); + return stateProps; + } + + function computeDispatchProps(store, props) { + const { dispatch } = store; + const dispatchProps = shouldUpdateDispatchProps ? + finalMapDispatchToProps(dispatch, props) : + finalMapDispatchToProps(dispatch); + + invariant( + isPlainObject(dispatchProps), + '`mapDispatchToProps` must return an object. Instead received %s.', + dispatchProps + ); + return dispatchProps; + } + + function computeNextState(stateProps, dispatchProps, parentProps) { + const mergedProps = finalMergeProps(stateProps, dispatchProps, parentProps); + invariant( + isPlainObject(mergedProps), + '`mergeProps` must return an object. Instead received %s.', + mergedProps + ); + return mergedProps; + } + + return function wrapWithConnect(WrappedComponent) { + class Connect extends Component { + + shouldComponentUpdate(nextProps, nextState) { + if (!pure) { + this.updateStateProps(nextProps); + this.updateDispatchProps(nextProps); + this.updateState(nextProps); + return true; + } + + const storeChanged = nextState.storeState !== this.state.storeState; + const propsChanged = !shallowEqual(nextProps, this.props); + let mapStateProducedChange = false; + let dispatchPropsChanged = false; + + if (storeChanged || (propsChanged && shouldUpdateStateProps)) { + mapStateProducedChange = this.updateStateProps(nextProps); + } + + if (propsChanged && shouldUpdateDispatchProps) { + dispatchPropsChanged = this.updateDispatchProps(nextProps); + } + + if (propsChanged || mapStateProducedChange || dispatchPropsChanged) { + this.updateState(nextProps); + return true; + } + + return false; + } + + constructor(props, context) { + super(props, context); + this.version = version; + this.store = props.store || context.store; + + invariant(this.store, + `Could not find "store" in either the context or ` + + `props of "${this.constructor.displayName}". ` + + `Either wrap the root component in a , ` + + `or explicitly pass "store" as a prop to "${this.constructor.displayName}".` + ); + + this.stateProps = computeStateProps(this.store, props); + this.dispatchProps = computeDispatchProps(this.store, props); + this.state = { storeState: null }; + this.updateState(); + } + + computeNextState(props = this.props) { + return computeNextState( + this.stateProps, + this.dispatchProps, + props + ); + } + + updateStateProps(props = this.props) { + const nextStateProps = computeStateProps(this.store, props); + if (shallowEqual(nextStateProps, this.stateProps)) { + return false; + } + + this.stateProps = nextStateProps; + return true; + } + + updateDispatchProps(props = this.props) { + const nextDispatchProps = computeDispatchProps(this.store, props); + if (shallowEqual(nextDispatchProps, this.dispatchProps)) { + return false; + } + + this.dispatchProps = nextDispatchProps; + return true; + } + + updateState(props = this.props) { + this.nextState = this.computeNextState(props); + } + + isSubscribed() { + return typeof this.unsubscribe === 'function'; + } + + trySubscribe() { + if (shouldSubscribe && !this.unsubscribe) { + this.unsubscribe = this.store.subscribe(::this.handleChange); + this.handleChange(); + } + } + + tryUnsubscribe() { + if (this.unsubscribe) { + this.unsubscribe(); + this.unsubscribe = null; + } + } + + componentDidMount() { + this.trySubscribe(); + } + + componentWillUnmount() { + this.tryUnsubscribe(); + } + + handleChange() { + if (!this.unsubscribe) { + return; + } + + this.setState({ + storeState: this.store.getState() + }); + } + + getWrappedInstance() { + return this.refs.wrappedInstance; + } + + render() { + return ( + + ); + } + } + + Connect.displayName = `Connect(${getDisplayName(WrappedComponent)})`; + Connect.WrappedComponent = WrappedComponent; + Connect.contextTypes = { + store: storeShape + }; + Connect.propTypes = { + store: storeShape + }; + + if (process.env.NODE_ENV !== 'production') { + Connect.prototype.componentWillUpdate = function componentWillUpdate() { + if (this.version === version) { + return; + } + + // We are hot reloading! + this.version = version; + + // Update the state and bindings. + this.trySubscribe(); + this.updateStateProps(); + this.updateDispatchProps(); + this.updateState(); + }; + } + + return hoistStatics(Connect, WrappedComponent); + }; +} diff --git a/src/components/createAll.js b/src/components/createAll.js deleted file mode 100644 index e83b7c9f6..000000000 --- a/src/components/createAll.js +++ /dev/null @@ -1,9 +0,0 @@ -import createProvider from './createProvider'; -import createConnect from './createConnect'; - -export default function createAll(React) { - const Provider = createProvider(React); - const connect = createConnect(React); - - return { Provider, connect }; -} diff --git a/src/components/createConnect.js b/src/components/createConnect.js deleted file mode 100644 index 5a7b9de41..000000000 --- a/src/components/createConnect.js +++ /dev/null @@ -1,238 +0,0 @@ -import createStoreShape from '../utils/createStoreShape'; -import shallowEqual from '../utils/shallowEqual'; -import isPlainObject from '../utils/isPlainObject'; -import wrapActionCreators from '../utils/wrapActionCreators'; -import hoistStatics from 'hoist-non-react-statics'; -import invariant from 'invariant'; - -const defaultMapStateToProps = () => ({}); -const defaultMapDispatchToProps = dispatch => ({ dispatch }); -const defaultMergeProps = (stateProps, dispatchProps, parentProps) => ({ - ...parentProps, - ...stateProps, - ...dispatchProps -}); - -function getDisplayName(Component) { - return Component.displayName || Component.name || 'Component'; -} - -// Helps track hot reloading. -let nextVersion = 0; - -export default function createConnect(React) { - const { Component, PropTypes } = React; - const storeShape = createStoreShape(PropTypes); - - return function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) { - const shouldSubscribe = Boolean(mapStateToProps); - const finalMapStateToProps = mapStateToProps || defaultMapStateToProps; - const finalMapDispatchToProps = isPlainObject(mapDispatchToProps) ? - wrapActionCreators(mapDispatchToProps) : - mapDispatchToProps || defaultMapDispatchToProps; - const finalMergeProps = mergeProps || defaultMergeProps; - const shouldUpdateStateProps = finalMapStateToProps.length > 1; - const shouldUpdateDispatchProps = finalMapDispatchToProps.length > 1; - const { pure = true } = options; - - // Helps track hot reloading. - const version = nextVersion++; - - function computeStateProps(store, props) { - const state = store.getState(); - const stateProps = shouldUpdateStateProps ? - finalMapStateToProps(state, props) : - finalMapStateToProps(state); - - invariant( - isPlainObject(stateProps), - '`mapStateToProps` must return an object. Instead received %s.', - stateProps - ); - return stateProps; - } - - function computeDispatchProps(store, props) { - const { dispatch } = store; - const dispatchProps = shouldUpdateDispatchProps ? - finalMapDispatchToProps(dispatch, props) : - finalMapDispatchToProps(dispatch); - - invariant( - isPlainObject(dispatchProps), - '`mapDispatchToProps` must return an object. Instead received %s.', - dispatchProps - ); - return dispatchProps; - } - - function computeNextState(stateProps, dispatchProps, parentProps) { - const mergedProps = finalMergeProps(stateProps, dispatchProps, parentProps); - invariant( - isPlainObject(mergedProps), - '`mergeProps` must return an object. Instead received %s.', - mergedProps - ); - return mergedProps; - } - - return function wrapWithConnect(WrappedComponent) { - class Connect extends Component { - - shouldComponentUpdate(nextProps, nextState) { - if (!pure) { - this.updateStateProps(nextProps); - this.updateDispatchProps(nextProps); - this.updateState(nextProps); - return true; - } - - const storeChanged = nextState.storeState !== this.state.storeState; - const propsChanged = !shallowEqual(nextProps, this.props); - let mapStateProducedChange = false; - let dispatchPropsChanged = false; - - if (storeChanged || (propsChanged && shouldUpdateStateProps)) { - mapStateProducedChange = this.updateStateProps(nextProps); - } - - if (propsChanged && shouldUpdateDispatchProps) { - dispatchPropsChanged = this.updateDispatchProps(nextProps); - } - - if (propsChanged || mapStateProducedChange || dispatchPropsChanged) { - this.updateState(nextProps); - return true; - } - - return false; - } - - constructor(props, context) { - super(props, context); - this.version = version; - this.store = props.store || context.store; - - invariant(this.store, - `Could not find "store" in either the context or ` + - `props of "${this.constructor.displayName}". ` + - `Either wrap the root component in a , ` + - `or explicitly pass "store" as a prop to "${this.constructor.displayName}".` - ); - - this.stateProps = computeStateProps(this.store, props); - this.dispatchProps = computeDispatchProps(this.store, props); - this.state = { storeState: null }; - this.updateState(); - } - - computeNextState(props = this.props) { - return computeNextState( - this.stateProps, - this.dispatchProps, - props - ); - } - - updateStateProps(props = this.props) { - const nextStateProps = computeStateProps(this.store, props); - if (shallowEqual(nextStateProps, this.stateProps)) { - return false; - } - - this.stateProps = nextStateProps; - return true; - } - - updateDispatchProps(props = this.props) { - const nextDispatchProps = computeDispatchProps(this.store, props); - if (shallowEqual(nextDispatchProps, this.dispatchProps)) { - return false; - } - - this.dispatchProps = nextDispatchProps; - return true; - } - - updateState(props = this.props) { - this.nextState = this.computeNextState(props); - } - - isSubscribed() { - return typeof this.unsubscribe === 'function'; - } - - trySubscribe() { - if (shouldSubscribe && !this.unsubscribe) { - this.unsubscribe = this.store.subscribe(::this.handleChange); - this.handleChange(); - } - } - - tryUnsubscribe() { - if (this.unsubscribe) { - this.unsubscribe(); - this.unsubscribe = null; - } - } - - componentDidMount() { - this.trySubscribe(); - } - - componentWillUnmount() { - this.tryUnsubscribe(); - } - - handleChange() { - if (!this.unsubscribe) { - return; - } - - this.setState({ - storeState: this.store.getState() - }); - } - - getWrappedInstance() { - return this.refs.wrappedInstance; - } - - render() { - return ( - - ); - } - } - - Connect.displayName = `Connect(${getDisplayName(WrappedComponent)})`; - Connect.WrappedComponent = WrappedComponent; - Connect.contextTypes = { - store: storeShape - }; - Connect.propTypes = { - store: storeShape - }; - - if (process.env.NODE_ENV !== 'production') { - Connect.prototype.componentWillUpdate = function componentWillUpdate() { - if (this.version === version) { - return; - } - - // We are hot reloading! - this.version = version; - - // Update the state and bindings. - this.trySubscribe(); - this.updateStateProps(); - this.updateDispatchProps(); - this.updateState(); - }; - } - - return hoistStatics(Connect, WrappedComponent); - }; - }; -} diff --git a/src/components/createProvider.js b/src/components/createProvider.js deleted file mode 100644 index 88486ff0c..000000000 --- a/src/components/createProvider.js +++ /dev/null @@ -1,107 +0,0 @@ -import createStoreShape from '../utils/createStoreShape'; - -function isUsingOwnerContext(React) { - const { version } = React; - if (typeof version !== 'string') { - return true; - } - - const sections = version.split('.'); - const major = parseInt(sections[0], 10); - const minor = parseInt(sections[1], 10); - - return major === 0 && minor === 13; -} - -export default function createProvider(React) { - const { Component, PropTypes, Children } = React; - const storeShape = createStoreShape(PropTypes); - const requireFunctionChild = isUsingOwnerContext(React); - - let didWarnAboutChild = false; - function warnAboutFunctionChild() { - if (didWarnAboutChild || requireFunctionChild) { - return; - } - - didWarnAboutChild = true; - console.error( // eslint-disable-line no-console - 'With React 0.14 and later versions, you no longer need to ' + - 'wrap child into a function.' - ); - } - function warnAboutElementChild() { - if (didWarnAboutChild || !requireFunctionChild) { - return; - } - - didWarnAboutChild = true; - console.error( // eslint-disable-line no-console - 'With React 0.13, you need to ' + - 'wrap child into a function. ' + - 'This restriction will be removed with React 0.14.' - ); - } - - let didWarnAboutReceivingStore = false; - function warnAboutReceivingStore() { - if (didWarnAboutReceivingStore) { - return; - } - - didWarnAboutReceivingStore = true; - console.error( // eslint-disable-line no-console - ' does not support changing `store` on the fly. ' + - 'It is most likely that you see this error because you updated to ' + - 'Redux 2.x and React Redux 2.x which no longer hot reload reducers ' + - 'automatically. See https://github.com/rackt/react-redux/releases/' + - 'tag/v2.0.0 for the migration instructions.' - ); - } - - class Provider extends Component { - getChildContext() { - return { store: this.store }; - } - - constructor(props, context) { - super(props, context); - this.store = props.store; - } - - componentWillReceiveProps(nextProps) { - const { store } = this; - const { store: nextStore } = nextProps; - - if (store !== nextStore) { - warnAboutReceivingStore(); - } - } - - render() { - let { children } = this.props; - - if (typeof children === 'function') { - warnAboutFunctionChild(); - children = children(); - } else { - warnAboutElementChild(); - } - - return Children.only(children); - } - } - - Provider.childContextTypes = { - store: storeShape.isRequired - }; - Provider.propTypes = { - store: storeShape.isRequired, - children: (requireFunctionChild ? - PropTypes.func : - PropTypes.element - ).isRequired - }; - - return Provider; -} diff --git a/src/index.js b/src/index.js index f6a257dd4..5fb3e1b61 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,2 @@ -import React from 'react'; -import createAll from './components/createAll'; - -export const { Provider, connect } = createAll(React); +export { default as Provider } from './components/Provider'; +export { default as connect } from './components/connect'; diff --git a/src/native.js b/src/native.js deleted file mode 100644 index 24a1d67cd..000000000 --- a/src/native.js +++ /dev/null @@ -1,4 +0,0 @@ -import React from 'react-native'; -import createAll from './components/createAll'; - -export const { Provider, connect } = createAll(React); diff --git a/src/utils/createStoreShape.js b/src/utils/createStoreShape.js deleted file mode 100644 index 851e7ce89..000000000 --- a/src/utils/createStoreShape.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function createStoreShape(PropTypes) { - return PropTypes.shape({ - subscribe: PropTypes.func.isRequired, - dispatch: PropTypes.func.isRequired, - getState: PropTypes.func.isRequired - }); -} diff --git a/src/utils/storeShape.js b/src/utils/storeShape.js new file mode 100644 index 000000000..040ebf167 --- /dev/null +++ b/src/utils/storeShape.js @@ -0,0 +1,7 @@ +import { PropTypes } from 'react'; + +export default PropTypes.shape({ + subscribe: PropTypes.func.isRequired, + dispatch: PropTypes.func.isRequired, + getState: PropTypes.func.isRequired +}); diff --git a/test/components/Provider.spec.js b/test/components/Provider.spec.js index ba3a3cd26..f0614e787 100644 --- a/test/components/Provider.spec.js +++ b/test/components/Provider.spec.js @@ -4,7 +4,6 @@ import React, { PropTypes, Component } from 'react'; import TestUtils from 'react-addons-test-utils'; import { createStore } from 'redux'; import { Provider } from '../../src/index'; -import createProvider from '../../src/components/createProvider'; describe('React', () => { describe('Provider', () => { @@ -20,123 +19,34 @@ describe('React', () => { } } - it('should not warn when using child-as-a-function before React 0.14', () => { - const store = createStore(() => ({})); - ['0.13.0-beta', '0.13.0', '0.13.3'].forEach(version => { - const LocalProvider = createProvider({ ...React, version }); - - let spy = expect.spyOn(console, 'error'); - const tree = TestUtils.renderIntoDocument( - - {() => } - - ); - spy.destroy(); - expect(spy.calls.length).toBe(0); - - spy = expect.spyOn(console, 'error'); - tree.forceUpdate(); - spy.destroy(); - expect(spy.calls.length).toBe(0); - }); - }); - - it('should warn once when using a single element before React 0.14', () => { - const store = createStore(() => ({})); - ['0.13.0-beta', '0.13.0', '0.13.3', undefined].forEach(version => { - const LocalProvider = createProvider({ ...React, version }); - // Trick React into checking propTypes every time: - LocalProvider.displayName = Math.random().toString(); - - let spy = expect.spyOn(console, 'error'); - const tree = TestUtils.renderIntoDocument( - - - - ); - spy.destroy(); - - expect(spy.calls.length).toBe(2); - expect(spy.calls[0].arguments[0]).toMatch( - /Invalid prop `children` of type `object` supplied to .*, expected `function`./ - ); - expect(spy.calls[1].arguments[0]).toMatch( - /With React 0.13, you need to wrap child into a function. This restriction will be removed with React 0.14./ - ); - - spy = expect.spyOn(console, 'error'); - tree.forceUpdate(); - spy.destroy(); - expect(spy.calls.length).toBe(0); - }); - }); - - it('should warn once when using child-as-a-function after React 0.14', () => { - const store = createStore(() => ({})); - ['0.14.0-beta3', '0.14.0', '0.14.2', '0.15.0-beta', '1.0.0-beta', '1.0.0'].forEach(version => { - const LocalProvider = createProvider({ ...React, version }); - // Trick React into checking propTypes every time: - LocalProvider.displayName = Math.random().toString(); - - let spy = expect.spyOn(console, 'error'); - const tree = TestUtils.renderIntoDocument( - - {() => } - - ); - spy.destroy(); - - expect(spy.calls.length).toBe(2); - expect(spy.calls[0].arguments[0]).toMatch( - /Invalid prop `children` supplied to .*, expected a single ReactElement./ - ); - expect(spy.calls[1].arguments[0]).toMatch( - /With React 0.14 and later versions, you no longer need to wrap child into a function./ - ); - - spy = expect.spyOn(console, 'error'); - tree.forceUpdate(); - spy.destroy(); - expect(spy.calls.length).toBe(0); - }); - }); - it('should enforce a single child', () => { const store = createStore(() => ({})); - expect(() => TestUtils.renderIntoDocument( - -
- - )).toNotThrow(); - - expect(() => TestUtils.renderIntoDocument( - - - )).toThrow(/exactly one child/); - - expect(() => TestUtils.renderIntoDocument( - -
-
- - )).toThrow(/exactly one child/); - }); - - it('should enforce a single child when using function-as-a-child', () => { - const store = createStore(() => ({})); - - expect(() => TestUtils.renderIntoDocument( - - {() =>
} - - )).toNotThrow(); - - expect(() => TestUtils.renderIntoDocument( - - {() => {}} - - )).toThrow(/exactly one child/); + // Ignore propTypes warnings + const propTypes = Provider.propTypes; + Provider.propTypes = {}; + + try { + expect(() => TestUtils.renderIntoDocument( + +
+ + )).toNotThrow(); + + expect(() => TestUtils.renderIntoDocument( + + + )).toThrow(/exactly one child/); + + expect(() => TestUtils.renderIntoDocument( + +
+
+ + )).toThrow(/exactly one child/); + } finally { + Provider.propTypes = propTypes; + } }); it('should add the store to the child context', () => { @@ -155,22 +65,6 @@ describe('React', () => { expect(child.context.store).toBe(store); }); - it('should add the store to the child context with function-as-a-child', () => { - const store = createStore(() => ({})); - - const spy = expect.spyOn(console, 'error'); - const tree = TestUtils.renderIntoDocument( - - {() => } - - ); - spy.destroy(); - expect(spy.calls.length).toBe(0); - - const child = TestUtils.findRenderedComponentWithType(tree, Child); - expect(child.context.store).toBe(store); - }); - it('should warn once when receiving a new store in props', () => { const store1 = createStore((state = 10) => state + 1); const store2 = createStore((state = 10) => state * 2); diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 9c8af2797..101190477 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -1276,7 +1276,7 @@ describe('React', () => { // setState calls DOM handlers are batched const container = TestUtils.findRenderedComponentWithType(tree, Container); - const node = ReactDOM.findDOMNode(container.getWrappedInstance().refs.button); + const node = container.getWrappedInstance().refs.button; TestUtils.Simulate.click(node); expect(childMapStateInvokes).toBe(4); diff --git a/webpack.config.base.js b/webpack.config.base.js index 0b7cd4867..a7130d0b0 100644 --- a/webpack.config.base.js +++ b/webpack.config.base.js @@ -19,7 +19,6 @@ var reduxExternal = { module.exports = { externals: { 'react': reactExternal, - 'react-native': reactExternal, 'redux': reduxExternal }, module: { From 2d3d0beade55477b3af65534ceb793db18b25705 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 15 Oct 2015 14:18:50 +0300 Subject: [PATCH 2/3] Make refs opt-in. Fixes #141 --- src/components/connect.js | 12 ++++--- test/components/connect.spec.js | 56 +++++++++++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/src/components/connect.js b/src/components/connect.js index 8ce0dc2ee..2d767c741 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -30,7 +30,7 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, const finalMergeProps = mergeProps || defaultMergeProps; const shouldUpdateStateProps = finalMapStateToProps.length > 1; const shouldUpdateDispatchProps = finalMapDispatchToProps.length > 1; - const { pure = true } = options; + const { pure = true, withRef = false } = options; // Helps track hot reloading. const version = nextVersion++; @@ -75,7 +75,6 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, return function wrapWithConnect(WrappedComponent) { class Connect extends Component { - shouldComponentUpdate(nextProps, nextState) { if (!pure) { this.updateStateProps(nextProps); @@ -192,13 +191,18 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, } getWrappedInstance() { + invariant(withRef, + `To access the wrapped instance, you need to specify ` + + `{ withRef: true } as the fourth argument of the connect() call.` + ); + return this.refs.wrappedInstance; } render() { + const ref = withRef ? 'wrappedInstance' : null; return ( - + ); } } diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 101190477..bf62d6543 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -85,7 +85,7 @@ describe('React', () => { ).toNotThrow(); }); - it('should subscribe to the store changes', () => { + it('should subscribe class components to the store changes', () => { const store = createStore(stringBuilder); @connect(state => ({ string: state }) ) @@ -109,6 +109,32 @@ describe('React', () => { expect(stub.props.string).toBe('ab'); }); + it('should subscribe pure function components to the store changes', () => { + const store = createStore(stringBuilder); + + let Container = connect( + state => ({ string: state }) + )(function Container(props) { + return ; + }); + + const spy = expect.spyOn(console, 'error'); + const tree = TestUtils.renderIntoDocument( + + + + ); + spy.destroy(); + expect(spy.calls.length).toBe(0); + + const stub = TestUtils.findRenderedComponentWithType(tree, Passthrough); + expect(stub.props.string).toBe(''); + store.dispatch({ type: 'APPEND', body: 'a'}); + expect(stub.props.string).toBe('a'); + store.dispatch({ type: 'APPEND', body: 'b'}); + expect(stub.props.string).toBe('ab'); + }); + it('should handle dispatches before componentDidMount', () => { const store = createStore(stringBuilder); @@ -1084,6 +1110,30 @@ describe('React', () => { ); }); + it('should throw when trying to access the wrapped instance if withRef is not specified', () => { + const store = createStore(() => ({})); + + class Container extends Component { + render() { + return ; + } + } + + const decorator = connect(state => state); + const Decorated = decorator(Container); + + const tree = TestUtils.renderIntoDocument( + + + + ); + + const decorated = TestUtils.findRenderedComponentWithType(tree, Decorated); + expect(() => decorated.getWrappedInstance()).toThrow( + /To access the wrapped instance, you need to specify \{ withRef: true \} as the fourth argument of the connect\(\) call\./ + ); + }); + it('should return the instance of the wrapped component for use in calling child methods', () => { const store = createStore(() => ({})); @@ -1101,7 +1151,7 @@ describe('React', () => { } } - const decorator = connect(state => state); + const decorator = connect(state => state, null, null, { withRef: true }); const Decorated = decorator(Container); const tree = TestUtils.renderIntoDocument( @@ -1231,7 +1281,7 @@ describe('React', () => { store.dispatch({ type: 'APPEND', body: 'a'}); let childMapStateInvokes = 0; - @connect(state => ({ state })) + @connect(state => ({ state }), null, null, { withRef: true }) class Container extends Component { emitChange() { From 11dee1f1023e2b4df7c8597672ed4b8490240ea8 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 15 Oct 2015 15:09:21 +0300 Subject: [PATCH 3/3] Update the docs for React 0.14 --- CHANGELOG.md | 2 +- README.md | 5 ++--- docs/api.md | 44 ++++++++++++++++++++++++++++------------- docs/quick-start.md | 12 ++++++----- docs/troubleshooting.md | 13 ++++++------ 5 files changed, 46 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebb8429f2..df3556609 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1 @@ -All notable changes are described on the [Releases](https://github.com/gaearon/react-redux/releases) page. +All notable changes are described on the [Releases](https://github.com/rackt/react-redux/releases) page. diff --git a/README.md b/README.md index 9f776f53b..1668c9ee4 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,11 @@ Performant and flexible. npm install --save react-redux ``` -React Redux requires **React 0.13 or later.** +React Redux requires **React 0.14 or later.** ## React Native -What you get from `react-redux` is for React. -For React Native, import from `react-redux/native` instead. +Until [React Native is compatible with React 0.14](https://github.com/facebook/react-native/issues/2985), you’ll need to keep using [React Redux 3.x branch and documentation](https://github.com/rackt/react-redux/tree/v3.1.0). ## Documentation diff --git a/docs/api.md b/docs/api.md index 7667c0599..2670794c5 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2,21 +2,23 @@ ### `` -Makes the Redux store available to the `connect()` calls in the component hierarchy below. Normally, you can’t use `connect()` without wrapping the root component in ``. (If you *really* need to, you can manually pass `store` as a prop to every `connect()`ed component, but we only recommend to do this for stubbing `store` in unit tests, or in non-fully-React codebases. Normally, you should just use ``.) +Makes the Redux store available to the `connect()` calls in the component hierarchy below. Normally, you can’t use `connect()` without wrapping the root component in ``. + +If you *really* need to, you can manually pass `store` as a prop to every `connect()`ed component, but we only recommend to do this for stubbing `store` in unit tests, or in non-fully-React codebases. Normally, you should just use ``. #### Props -* `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. +* `store` (*[Redux Store](http://rackt.github.io/redux/docs/api/Store.html)*): The single Redux store in your application. +* `children` (*ReactElement*) The root of your component hierarchy. #### Example ##### Vanilla React ```js -React.render( +ReactDOM.render( - {() => } + , rootEl ); @@ -26,9 +28,10 @@ React.render( ```js Router.run(routes, Router.HistoryLocation, (Handler, routerState) => { // note "routerState" here - React.render( + ReactDOM.render( - {() => } // note "routerState" here: important to pass it down + {/* note "routerState" here: important to pass it down */} + , document.getElementById('root') ); @@ -38,9 +41,9 @@ Router.run(routes, Router.HistoryLocation, (Handler, routerState) => { // note " ##### React Router 1.0 ```js -React.render( +ReactDOM.render( - {() => ...} + ... , targetEl ); @@ -57,26 +60,39 @@ Instead, it *returns* a new, connected component class, for you to use. * [`mapStateToProps(state, [ownProps]): stateProps`] \(*Function*): If specified, the component will subscribe to Redux store updates. Any time it updates, `mapStateToProps` 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. If `ownProps` is specified as a second argument, its value will be the properties passed to your component, and `mapStateToProps` will be re-invoked whenever the component receives new props. -* [`mapDispatchToProps(dispatch, [ownProps]): dispatchProps`] \(*Object* or *Function*): If an object is passed, each function inside it will be assumed to be a Redux action creator. 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 the [`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. If `ownProps` is specified as a second argument, its value will be the properties passed to your component, and `mapDispatchToProps` will be re-invoked whenever the component receives new props. +* [`mapDispatchToProps(dispatch, [ownProps]): dispatchProps`] \(*Object* or *Function*): If an object is passed, each function inside it will be assumed to be a Redux action creator. 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 the [`bindActionCreators()`](http://rackt.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. If `ownProps` is specified as a second argument, its value will be the properties passed to your component, and `mapDispatchToProps` will be re-invoked whenever the component receives new props. * [`mergeProps(stateProps, dispatchProps, ownProps): props`] \(*Function*): If specified, it is passed the result of `mapStateToProps()`, `mapDispatchToProps()`, 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, `Object.assign({}, ownProps, stateProps, dispatchProps)` is used by default. * [`options`] *(Object)* If specified, further customizes the behavior of the connector. - * [`pure`] *(Boolean)*: If true, implements `shouldComponentUpdate` and shallowly compares the result of `mergeProps`, preventing unnecessary updates, assuming that the component is a “pure” component and does not rely on any input or state other than its props and the selected Redux store’s state. *Defaults to `true`.* + * [`pure = true`] *(Boolean)*: If true, implements `shouldComponentUpdate` and shallowly compares the result of `mergeProps`, preventing unnecessary updates, assuming that the component is a “pure” component and does not rely on any input or state other than its props and the selected Redux store’s state. *Defaults to `true`.* + * [`withRef = false`] *(Boolean)*: If true, stores a ref to the wrapped component instance and makes it available via `getWrappedInstance()` method. *Defaults to `false`.* #### Returns A React component class that injects state and action creators into your component according to the specified options. +##### Static Properties + +* `WrappedComponent` *(Component)*: The original component class passed to `connect()`. + +##### Static Methods + +All the original static methods of the component are hoisted. + +##### Instance Methods + +###### `getWrappedInstance(): ReactComponent` + +Returns the wrapped component instance. Only available if you pass `{ withRef: true }` as part of the `connect()`’s fourth `options` argument. + #### Remarks * It needs to be invoked two times. The first time with its arguments described above, and a second time, with the component: `connect(mapStateToProps, mapDispatchToProps, mergeProps)(MyComponent)`. * It does not modify the passed React component. It returns a new, connected component, that you should use instead. -* The `mapStateToProps` 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.** You may also pass `store` as a prop to the `connect()`ed component, but it's not recommended, because it's just too much trouble. Only do this for non-fully-React codebases or to stub the store in a unit test. +* The `mapStateToProps` 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/rackt/reselect) to efficiently compose selectors and [compute derived data](http://rackt.github.io/redux/docs/recipes/ComputingDerivedData.html). #### Examples diff --git a/docs/quick-start.md b/docs/quick-start.md index de0c15797..02be890f4 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -126,9 +126,10 @@ This is the most basic usage, but `connect()` supports many other patterns: just Finally, how do we actually hook it up to the 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. -The trick is to wrap the whole view hierarchy into a `{() => ... }` where `Provider` is imported from `react-redux`. One gotcha is that **the child of `Provider` must be a function**. This is to work around an issue about how context (undocumented feature we have to rely on to pass Redux data to components below) works in React 0.13. In React 0.14, you will be able to put your view hierarchy in `` without wrapping it into a function. +The trick is to wrap the whole view hierarchy into a `` from React Redux. ```js +import ReactDOM from 'react-dom'; import { Component } from 'react'; import { Provider } from 'react-redux'; @@ -140,9 +141,10 @@ class App extends Component { const targetEl = document.getElementById('root'); -React.render(( +ReactDOM.render( - {() => } - -), targetEl); + + , + targetEl +); ``` diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index c0743e18b..2eca50b4d 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -18,9 +18,10 @@ Root view: ```js Router.run(routes, Router.HistoryLocation, (Handler, routerState) => { // note "routerState" here - React.render( + ReactDOM.render( - {() => } // note "routerState" here + {/* note "routerState" here */} + , document.getElementById('root') ); @@ -41,13 +42,13 @@ You can also upgrade to React Router 1.0 which shouldn’t have this problem. (L ### My views aren’t updating when something changes outside of Redux -If your views depend on global state or [React “context”](www.youtube.com/watch?v=H7vlH-wntD4), you might find that views decorated with `connect()` will fail to update. +If your views depend on global state or [React “context”](http://facebook.github.io/react/docs/context.html), you might find that views decorated with `connect()` will fail to update. >This is because `connect()` implements [shouldComponentUpdate](https://facebook.github.io/react/docs/component-specs.html#updating-shouldcomponentupdate) by default, assuming that your component will produce the same results given the same props and state. This is a similar concept to React’s [PureRenderMixin](https://facebook.github.io/react/docs/pure-render-mixin.html). The _best_ solution to this is to make sure that your components are pure and pass any external state to them via props. This will ensure that your views do not re-render unless they actually need to re-render and will greatly speed up your application. -If that's not practical for whatever reason (for example, if you’re using a library that depends heavily on React context), you may pass the `pure: false` option to `connect()`: +If that’s not practical for whatever reason (for example, if you’re using a library that depends heavily on React context), you may pass the `pure: false` option to `connect()`: ``` function mapStateToProps(state) { @@ -67,10 +68,8 @@ If you have context issues, 1. [Make sure you don’t have a duplicate instance of React](https://medium.com/@dan_abramov/two-weird-tricks-that-fix-react-7cf9bbdef375) on the page. 2. Make sure you didn’t forget to wrap your root component in [``](#provider-store). -3. If you use React Router, something like `{() => router}` won’t work. Due to the way context works in React 0.13, it’s important that the `` children are *created* inside that function. Just referencing an outside variable doesn’t do the trick. Instead of `{() => router}`, write `{() => createRouter()}` where `createRouter()` is a function that actually *creates* (and returns) the router. +3. Make sure you’re running the latest versions of React and React Redux. ### Invariant Violation: addComponentAsRefTo(...): Only a ReactOwner can have refs. This usually means that you’re trying to add a ref to a component that doesn’t have an owner If you’re using React for web, this usually means you have a [duplicate React](https://medium.com/@dan_abramov/two-weird-tricks-that-fix-react-7cf9bbdef375). Follow the linked instructions to fix this. - -If you’re using React Native, make sure you’re importing `react-redux/native` both for `` and any `connect()` call. Importing from `react-redux` will not work on React Native.