From 68a4f2b78671329e10bfca87f8f1c82cc8690459 Mon Sep 17 00:00:00 2001 From: Tim Dorr Date: Mon, 15 Aug 2016 11:51:27 -0400 Subject: [PATCH 001/107] Remove Style section Obviously that didn't happen... --- CONTRIBUTING.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 342e92d5d..66c07e803 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,10 +54,6 @@ npm run lint Please open an issue with a proposal for a new feature or refactoring before starting on the work. We don't want you to waste your efforts on a pull request that we won't want to accept. -###Style - -[reactjs](https://github.com/reactjs) is trying to keep a standard style across its various projects, which can be found over in [eslint-config-reactjs](https://github.com/reactjs/eslint-config-reactjs). If you have a style change proposal, it should first be proposed there. If accepted, we will be happy to accept a PR to implement it here. - ## Submitting Changes * Open a new issue in the [Issue tracker](https://github.com/reactjs/react-redux/issues). From 0fb9f7f17abd9a541250fbd21bf36ee64ef475f9 Mon Sep 17 00:00:00 2001 From: Benoit Benezech Date: Mon, 7 Nov 2016 19:18:12 +0100 Subject: [PATCH 002/107] add typings --- index.d.ts | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 4 ++- 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 index.d.ts diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 000000000..9c1da0490 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,70 @@ +import { ComponentClass, Component, StatelessComponent } from 'react'; +import { Store, Dispatch, ActionCreator } from 'redux'; + +interface ComponentDecorator { + (component: StatelessComponent|ComponentClass): ComponentClass; +} + +/** + * Following 5 functions cover all possible ways connect could be invoked + * + * - State: Redux state interface (the same one used by Store) + * - TOwnProps: Props passed to the wrapping component + * - TStateProps: Result of MapStateToProps + * - TDispatchProps: Result of MapDispatchToProps + */ +function connect(): ComponentDecorator<{ dispatch: Dispatch } & TOwnProps, TOwnProps>; + +function connect( + mapStateToProps: FuncOrSelf>, +): ComponentDecorator } & TOwnProps, TOwnProps>; + +function connect( + mapStateToProps: FuncOrSelf>, + mapDispatchToProps: FuncOrSelf | MapDispatchToPropsObject & TDispatchProps> +): ComponentDecorator; + +function connect( + mapStateToProps: null, + mapDispatchToProps: FuncOrSelf | MapDispatchToPropsObject & TDispatchProps> +): ComponentDecorator; + +function connect( + mapStateToProps: FuncOrSelf>, + mapDispatchToProps: FuncOrSelf| MapDispatchToPropsObject & TDispatchProps>, + mergeProps: MergeProps, + options?: Options +): ComponentDecorator; + +interface MapDispatchToPropsObject { + [name: string]: ActionCreator; +} + +interface MapStateToProps { + (state: State, ownProps: TOwnProps): TStateProps; +} + +interface MapDispatchToPropsFunction { + (dispatch: Dispatch, ownProps: TOwnProps): TDispatchProps; +} + +interface MergeProps { + (stateProps: TStateProps, dispatchProps: TDispatchProps, ownProps: TOwnProps): TMergeProps; +} + +interface Options { + pure?: boolean; + withRef?: boolean; +} + +type FuncOrSelf = T | (() => T); + +/** + * Typescript does not support generic components in tsx yet in an intuïtive way which is the reason we avoid a + * generic parameter in Store here by using any as the type + */ +export interface ProviderProps { + store: Store; +} + +export class Provider extends Component { } diff --git a/package.json b/package.json index 576522b83..af832b523 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "4.4.5", "description": "Official React bindings for Redux", "main": "./lib/index.js", + "typings": "./index.d.ts", "scripts": { "build:lib": "babel src --out-dir lib", "build:umd": "cross-env NODE_ENV=development webpack src/index.js dist/react-redux.js", @@ -22,7 +23,8 @@ "files": [ "dist", "lib", - "src" + "src", + "index.d.ts" ], "keywords": [ "react", From 3229d733196459faa3782d14fd5b1d9be41fa599 Mon Sep 17 00:00:00 2001 From: Benoit Benezech Date: Mon, 7 Nov 2016 19:18:12 +0100 Subject: [PATCH 003/107] add typings --- index.d.ts | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 4 ++- 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 index.d.ts diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 000000000..9c1da0490 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,70 @@ +import { ComponentClass, Component, StatelessComponent } from 'react'; +import { Store, Dispatch, ActionCreator } from 'redux'; + +interface ComponentDecorator { + (component: StatelessComponent|ComponentClass): ComponentClass; +} + +/** + * Following 5 functions cover all possible ways connect could be invoked + * + * - State: Redux state interface (the same one used by Store) + * - TOwnProps: Props passed to the wrapping component + * - TStateProps: Result of MapStateToProps + * - TDispatchProps: Result of MapDispatchToProps + */ +function connect(): ComponentDecorator<{ dispatch: Dispatch } & TOwnProps, TOwnProps>; + +function connect( + mapStateToProps: FuncOrSelf>, +): ComponentDecorator } & TOwnProps, TOwnProps>; + +function connect( + mapStateToProps: FuncOrSelf>, + mapDispatchToProps: FuncOrSelf | MapDispatchToPropsObject & TDispatchProps> +): ComponentDecorator; + +function connect( + mapStateToProps: null, + mapDispatchToProps: FuncOrSelf | MapDispatchToPropsObject & TDispatchProps> +): ComponentDecorator; + +function connect( + mapStateToProps: FuncOrSelf>, + mapDispatchToProps: FuncOrSelf| MapDispatchToPropsObject & TDispatchProps>, + mergeProps: MergeProps, + options?: Options +): ComponentDecorator; + +interface MapDispatchToPropsObject { + [name: string]: ActionCreator; +} + +interface MapStateToProps { + (state: State, ownProps: TOwnProps): TStateProps; +} + +interface MapDispatchToPropsFunction { + (dispatch: Dispatch, ownProps: TOwnProps): TDispatchProps; +} + +interface MergeProps { + (stateProps: TStateProps, dispatchProps: TDispatchProps, ownProps: TOwnProps): TMergeProps; +} + +interface Options { + pure?: boolean; + withRef?: boolean; +} + +type FuncOrSelf = T | (() => T); + +/** + * Typescript does not support generic components in tsx yet in an intuïtive way which is the reason we avoid a + * generic parameter in Store here by using any as the type + */ +export interface ProviderProps { + store: Store; +} + +export class Provider extends Component { } diff --git a/package.json b/package.json index c15dc040d..6f2bd20e0 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "./lib/index.js", "module": "es/index.js", "jsnext:main": "es/index.js", + "typings": "./index.d.ts", "scripts": { "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib", "build:es": "cross-env BABEL_ENV=es babel src --out-dir es", @@ -27,7 +28,8 @@ "dist", "lib", "src", - "es" + "es", + "index.d.ts" ], "keywords": [ "react", From 14acd153554c1734a42f5cebdf998e82c257f065 Mon Sep 17 00:00:00 2001 From: Benoit Benezech Date: Thu, 17 Nov 2016 16:12:48 +0100 Subject: [PATCH 004/107] remove separated null constrained signature --- index.d.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/index.d.ts b/index.d.ts index 9c1da0490..0d6fdf9cb 100644 --- a/index.d.ts +++ b/index.d.ts @@ -20,17 +20,12 @@ function connect( ): ComponentDecorator } & TOwnProps, TOwnProps>; function connect( - mapStateToProps: FuncOrSelf>, + mapStateToProps: FuncOrSelf>|null, mapDispatchToProps: FuncOrSelf | MapDispatchToPropsObject & TDispatchProps> ): ComponentDecorator; -function connect( - mapStateToProps: null, - mapDispatchToProps: FuncOrSelf | MapDispatchToPropsObject & TDispatchProps> -): ComponentDecorator; - function connect( - mapStateToProps: FuncOrSelf>, + mapStateToProps: FuncOrSelf>|null, mapDispatchToProps: FuncOrSelf| MapDispatchToPropsObject & TDispatchProps>, mergeProps: MergeProps, options?: Options From c0e9c2698c51b564914728659e3aeaa818459443 Mon Sep 17 00:00:00 2001 From: Daniel Lytkin Date: Mon, 21 Nov 2016 17:42:40 +0700 Subject: [PATCH 005/107] export `connect` from TypeScript definitions file; add tests for definitions --- .travis.yml | 1 + index.d.ts | 8 +- package.json | 4 + test/typescript/test.tsx | 321 ++++++++++++++++++++++++++++++++++ test/typescript/tsconfig.json | 7 + 5 files changed, 337 insertions(+), 4 deletions(-) create mode 100644 test/typescript/test.tsx create mode 100644 test/typescript/tsconfig.json diff --git a/.travis.yml b/.travis.yml index 31bd8886a..e0ab47475 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,5 +4,6 @@ node_js: script: - npm run lint - npm run test:cov + - npm run test:typescript after_success: - npm run coverage diff --git a/index.d.ts b/index.d.ts index 0d6fdf9cb..18022a884 100644 --- a/index.d.ts +++ b/index.d.ts @@ -13,18 +13,18 @@ interface ComponentDecorator { * - TStateProps: Result of MapStateToProps * - TDispatchProps: Result of MapDispatchToProps */ -function connect(): ComponentDecorator<{ dispatch: Dispatch } & TOwnProps, TOwnProps>; +export function connect(): ComponentDecorator<{ dispatch: Dispatch } & TOwnProps, TOwnProps>; -function connect( +export function connect( mapStateToProps: FuncOrSelf>, ): ComponentDecorator } & TOwnProps, TOwnProps>; -function connect( +export function connect( mapStateToProps: FuncOrSelf>|null, mapDispatchToProps: FuncOrSelf | MapDispatchToPropsObject & TDispatchProps> ): ComponentDecorator; -function connect( +export function connect( mapStateToProps: FuncOrSelf>|null, mapDispatchToProps: FuncOrSelf| MapDispatchToPropsObject & TDispatchProps>, mergeProps: MergeProps, diff --git a/package.json b/package.json index 6f2bd20e0..aba0a7a42 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "test": "cross-env BABEL_ENV=commonjs NODE_ENV=test mocha --compilers js:babel-register --recursive --require ./test/setup.js", "test:watch": "npm test -- --watch", "test:cov": "cross-env NODE_ENV=test nyc npm test", + "test:typescript": "typings-tester --dir test/typescript", "coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov" }, "repository": { @@ -49,6 +50,7 @@ }, "homepage": "https://github.com/gaearon/react-redux", "devDependencies": { + "@types/react": "^0.14.49", "babel-cli": "^6.3.17", "babel-core": "^6.3.26", "babel-eslint": "^6.1.2", @@ -95,6 +97,8 @@ "react-dom": "^0.14.0", "redux": "^3.0.0", "rimraf": "^2.3.4", + "typescript": "^2.0.10", + "typings-tester": "^0.2.0", "webpack": "^1.11.0" }, "dependencies": { diff --git a/test/typescript/test.tsx b/test/typescript/test.tsx new file mode 100644 index 000000000..52f4b4b15 --- /dev/null +++ b/test/typescript/test.tsx @@ -0,0 +1,321 @@ +import {Dispatch, Store} from "redux"; +import {connect, Provider} from "../../index"; + + +function testNoArgs() { + const Connected = connect()(props => { + // typings:expect-error + props.foo; + + return + + + ) + } + } + + @connect((state, parentProps) => { + childMapStateInvokes++ + // The state from parent props should always be consistent with the current state + expect(state).toEqual(parentProps.parentState) + return {} + }) + class ChildContainer extends Component { + render() { + return
+ } + } + + const tree = TestUtils.renderIntoDocument( + + + + ) + + expect(childMapStateInvokes).toBe(1) + + // The store state stays consistent when setState calls are batched + store.dispatch({ type: 'APPEND', body: 'c' }) + expect(childMapStateInvokes).toBe(2) + + // setState calls DOM handlers are batched + const container = TestUtils.findRenderedComponentWithType(tree, Container) + const node = container.getWrappedInstance().refs.button + TestUtils.Simulate.click(node) + expect(childMapStateInvokes).toBe(3) + + // Provider uses unstable_batchedUpdates() under the hood + store.dispatch({ type: 'APPEND', body: 'd' }) + expect(childMapStateInvokes).toBe(4) + }) }) diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 5e494a951..38e4f95ae 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -1538,14 +1538,8 @@ describe('React', () => { TestUtils.Simulate.click(node) expect(childMapStateInvokes).toBe(3) - // In future all setState calls will be batched[1]. Uncomment when it - // happens. For now redux-batched-updates middleware can be used as - // workaround this. - // - // [1]: https://twitter.com/sebmarkbage/status/642366976824864768 - // - // store.dispatch({ type: 'APPEND', body: 'd' }) - // expect(childMapStateInvokes).toBe(4) + store.dispatch({ type: 'APPEND', body: 'd' }) + expect(childMapStateInvokes).toBe(4) }) it('should not render the wrapped component when mapState does not produce change', () => { From 03013b5af445d25f341482a2b4a2fb0e373d2a6a Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sat, 16 Jul 2016 14:23:04 -0400 Subject: [PATCH 078/107] Added passing test from #395 --- test/components/connect.spec.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 38e4f95ae..0254657bb 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -1862,5 +1862,18 @@ describe('React', () => { ReactDOM.unmountComponentAtNode(div) }) + + it('should allow custom displayName', () => { + // TODO remove __ENABLE_SECRET_EXPERIMENTAL_FEATURES_DO_NOT_USE_OR_YOU_WILL_BE_FIRED once approved + @connect(null, null, null, { getDisplayName: name => `Custom(${name})`, __ENABLE_SECRET_EXPERIMENTAL_FEATURES_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: true }) + class MyComponent extends React.Component { + render() { + return
+ } + } + + expect(MyComponent.displayName).toEqual('Custom(MyComponent)') + }) + }) }) From 0bfd1547363b575a36418c6dea9d1cc7fc00c265 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sat, 16 Jul 2016 14:24:24 -0400 Subject: [PATCH 079/107] Add passing test from #429 --- test/components/connect.spec.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 0254657bb..3a63fa023 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -1875,5 +1875,28 @@ describe('React', () => { expect(MyComponent.displayName).toEqual('Custom(MyComponent)') }) + it('should update impure components whenever the state of the store changes', () => { + const store = createStore(() => ({})) + let renderCount = 0 + + @connect(() => ({}), null, null, { pure: false }) + class ImpureComponent extends React.Component { + render() { + ++renderCount + return
+ } + } + + TestUtils.renderIntoDocument( + + + + ) + + const rendersBeforeStateChange = renderCount + store.dispatch({ type: 'ACTION' }) + expect(renderCount).toBe(rendersBeforeStateChange + 1) + }) }) + }) From 278ae8f50e0578bfb6286264fc0dc177fbacd648 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sat, 16 Jul 2016 14:28:29 -0400 Subject: [PATCH 080/107] Add code + test for #436 --- src/components/connectAdvanced.js | 6 ++++++ test/components/connect.spec.js | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index c34702d05..6736e7a9d 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -64,6 +64,12 @@ export default function connectAdvanced( } return function wrapWithConnect(WrappedComponent) { + invariant( + typeof WrappedComponent == 'function', + `You must pass a component to the function returned by ` + + `connect. Instead received ${WrappedComponent}` + ) + const wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name || 'Component' diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 3a63fa023..3742afd79 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -1017,6 +1017,12 @@ describe('React', () => { expect(stub.props.passVal).toBe('otherval') }) + it('should throw an error if a component is not passed to the function returned by connect', () => { + expect(connect()).toThrow( + /You must pass a component to the function/ + ) + }) + it('should throw an error if mapState, mapDispatch, or mergeProps returns anything but a plain object', () => { const store = createStore(() => ({})) From 749962ac7c9c614323f2dca019fe9e8966dfd799 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sat, 16 Jul 2016 14:53:28 -0400 Subject: [PATCH 081/107] Extract buildConnectOptions out of connect()... will make testing easier --- src/connect/connect.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/connect/connect.js b/src/connect/connect.js index acdb933b6..9c4611983 100644 --- a/src/connect/connect.js +++ b/src/connect/connect.js @@ -20,7 +20,16 @@ import defaultSelectorFactory from './selectorFactory' The resulting final props selector is called by the Connect component instance whenever it receives new props or store state. */ -export default function connect( + +function match(arg, factories) { + for (let i = factories.length - 1; i >= 0; i--) { + const result = factories[i](arg) + if (result) return result + } + return undefined +} + +export function buildConnectOptions( mapStateToProps, mapDispatchToProps, mergeProps, @@ -46,7 +55,7 @@ export default function connect( const initMapDispatchToProps = match(mapDispatchToProps, mapDispatchToPropsFactories) const initMergeProps = match(mergeProps, mergePropsFactories) - return connectAdvanced(selectorFactory, { + return { // used in error messages methodName: 'connect', @@ -57,6 +66,7 @@ export default function connect( shouldHandleStateChanges: Boolean(mapStateToProps), // passed through to selectorFactory + selectorFactory, initMapStateToProps, initMapDispatchToProps, initMergeProps, @@ -64,13 +74,10 @@ export default function connect( // any addional options args can override defaults of connect or connectAdvanced ...options - }) + } } -function match(arg, factories) { - for (let i = factories.length - 1; i >= 0; i--) { - const result = factories[i](arg) - if (result) return result - } - return undefined +export default function connect(...args) { + const options = buildConnectOptions(...args) + return connectAdvanced(options.selectorFactory, options) } From 4bc5abba1210a4413bb051396e48f9fa92304f3e Mon Sep 17 00:00:00 2001 From: Tim Dorr Date: Tue, 16 Aug 2016 19:04:38 -0400 Subject: [PATCH 082/107] Remove eslint-config-rackt. Can't switch off babel-eslint because we're using decorators still. --- .eslintrc | 18 ++++++++++++++++-- package.json | 8 ++++---- test/components/Provider.spec.js | 2 ++ test/components/connect.spec.js | 9 ++++++--- 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/.eslintrc b/.eslintrc index de7e83749..8de866df3 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,5 +1,18 @@ { - "extends": "eslint-config-rackt", + "parser": "babel-eslint", + "extends": [ + "eslint:recommended", + "plugin:import/errors", + "plugin:react/recommended" + ], + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true, + "experimentalObjectRestSpread": true + }, + }, "env": { "browser": true, "mocha": true, @@ -9,9 +22,10 @@ "valid-jsdoc": 2, "react/jsx-uses-react": 1, "react/jsx-no-undef": 2, - "react/wrap-multilines": 2 + "react/jsx-wrap-multilines": 2 }, "plugins": [ + "import", "react" ] } diff --git a/package.json b/package.json index af832b523..dee37b082 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "devDependencies": { "babel-cli": "^6.3.17", "babel-core": "^6.3.26", - "babel-eslint": "^5.0.0-beta9", + "babel-eslint": "^6.1.2", "babel-loader": "^6.2.0", "babel-plugin-check-es2015-constants": "^6.3.13", "babel-plugin-syntax-jsx": "^6.3.13", @@ -74,9 +74,9 @@ "babel-register": "^6.3.13", "cross-env": "^1.0.7", "es3ify": "^0.2.0", - "eslint": "^1.7.1", - "eslint-config-rackt": "1.1.0", - "eslint-plugin-react": "^3.6.3", + "eslint": "^3.3.1", + "eslint-plugin-import": "^1.13.0", + "eslint-plugin-react": "^6.1.1", "expect": "^1.8.0", "glob": "^6.0.4", "isparta": "4.0.0", diff --git a/test/components/Provider.spec.js b/test/components/Provider.spec.js index 4d4a17df1..04ca82929 100644 --- a/test/components/Provider.spec.js +++ b/test/components/Provider.spec.js @@ -1,3 +1,5 @@ +/*eslint-disable react/prop-types*/ + import expect from 'expect' import React, { PropTypes, Component } from 'react' import TestUtils from 'react-addons-test-utils' diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 3742afd79..eadd2e927 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -1,3 +1,5 @@ +/*eslint-disable react/prop-types*/ + import expect from 'expect' import React, { createClass, Children, PropTypes, Component } from 'react' import ReactDOM from 'react-dom' @@ -351,9 +353,10 @@ describe('React', () => { componentDidMount() { // Simulate deep object mutation - this.state.bar.baz = 'through' + const bar = this.state.bar + bar.baz = 'through' this.setState({ - bar: this.state.bar + bar }) } @@ -1022,7 +1025,7 @@ describe('React', () => { /You must pass a component to the function/ ) }) - + it('should throw an error if mapState, mapDispatch, or mergeProps returns anything but a plain object', () => { const store = createStore(() => ({})) From c260a9b92840d4a2c09d264d67a3b6d44d70a9e4 Mon Sep 17 00:00:00 2001 From: Tim Dorr Date: Tue, 16 Aug 2016 19:37:49 -0400 Subject: [PATCH 083/107] isparta is dead. And use Codecov. --- .babelrc | 9 +++++++-- .gitignore | 1 + .travis.yml | 4 +++- codecov.yml | 1 + package.json | 15 +++++++++++---- 5 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 codecov.yml diff --git a/.babelrc b/.babelrc index 735385d1b..67435a531 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,5 @@ { - plugins: [ + "plugins": [ "transform-decorators-legacy", ["transform-es2015-template-literals", { "loose": true }], "transform-es2015-literals", @@ -22,5 +22,10 @@ "transform-object-rest-spread", "transform-react-jsx", "syntax-jsx" - ] + ], + "env": { + "test": { + "plugins": ["istanbul"] + } + } } diff --git a/.gitignore b/.gitignore index dbb9d4c83..f53a08f0e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ npm-debug.log .DS_Store dist lib +.nyc_output coverage diff --git a/.travis.yml b/.travis.yml index fdcb9167e..31bd8886a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,4 +3,6 @@ node_js: - "5" script: - npm run lint - - npm test + - npm run test:cov +after_success: + - npm run coverage diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..69cb76019 --- /dev/null +++ b/codecov.yml @@ -0,0 +1 @@ +comment: false diff --git a/package.json b/package.json index dee37b082..d390b5d39 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,10 @@ "clean": "rimraf lib dist coverage", "lint": "eslint src test", "prepublish": "npm run clean && npm run build", - "test": "mocha --compilers js:babel-register --recursive --require ./test/setup.js", + "test": "cross-env NODE_ENV=test mocha --compilers js:babel-register --recursive --require ./test/setup.js", "test:watch": "npm test -- --watch", - "test:cov": "babel-node ./node_modules/isparta/bin/isparta cover ./node_modules/mocha/bin/_mocha -- --recursive" + "test:cov": "cross-env NODE_ENV=test nyc npm test", + "coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov" }, "repository": { "type": "git", @@ -49,6 +50,7 @@ "babel-eslint": "^6.1.2", "babel-loader": "^6.2.0", "babel-plugin-check-es2015-constants": "^6.3.13", + "babel-plugin-istanbul": "^2.0.0", "babel-plugin-syntax-jsx": "^6.3.13", "babel-plugin-transform-decorators-legacy": "^1.2.0", "babel-plugin-transform-es2015-arrow-functions": "^6.3.13", @@ -72,6 +74,7 @@ "babel-plugin-transform-react-display-name": "^6.4.0", "babel-plugin-transform-react-jsx": "^6.4.0", "babel-register": "^6.3.13", + "codecov": "^1.0.1", "cross-env": "^1.0.7", "es3ify": "^0.2.0", "eslint": "^3.3.1", @@ -79,10 +82,10 @@ "eslint-plugin-react": "^6.1.1", "expect": "^1.8.0", "glob": "^6.0.4", - "isparta": "4.0.0", - "istanbul": "^0.3.17", + "istanbul": "^0.4.4", "jsdom": "~5.4.3", "mocha": "^2.2.5", + "nyc": "^8.1.0", "react": "^0.14.0", "react-addons-test-utils": "^0.14.0", "react-dom": "^0.14.0", @@ -104,5 +107,9 @@ "transform": [ "loose-envify" ] + }, + "nyc": { + "sourceMap": false, + "instrument": false } } From bdeb3261b1372ee0d222b605bb560d52cb219709 Mon Sep 17 00:00:00 2001 From: Vinay Hiremath Date: Thu, 18 Aug 2016 01:47:48 -0700 Subject: [PATCH 084/107] bind proper store context in connectAdvanced and the Subscription util --- src/components/connectAdvanced.js | 3 ++- src/utils/Subscription.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index 6736e7a9d..cafed1658 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -159,7 +159,8 @@ export default function connectAdvanced( } initSelector() { - const { dispatch, getState } = this.store + const { dispatch } = this.store + let getState = this.store.getState.bind(this.store) const sourceSelector = selectorFactory(dispatch, selectorFactoryOptions) // wrap the selector in an object that tracks its results between runs diff --git a/src/utils/Subscription.js b/src/utils/Subscription.js index b26615ed2..b3b63ec4d 100644 --- a/src/utils/Subscription.js +++ b/src/utils/Subscription.js @@ -5,7 +5,7 @@ export default class Subscription { constructor(store, parentSub) { this.subscribe = parentSub ? parentSub.addNestedSub.bind(parentSub) - : store.subscribe + : store.subscribe.bind(store) this.unsubscribe = null this.nextListeners = this.currentListeners = [] From 5db1d443e79976ab5f82cce54c22e6c135bf4da6 Mon Sep 17 00:00:00 2001 From: Vinay Hiremath Date: Thu, 18 Aug 2016 15:55:26 -0700 Subject: [PATCH 085/107] add store context-preservation tests --- src/components/connectAdvanced.js | 6 +++- test/components/connect.spec.js | 48 +++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index cafed1658..7ad013f23 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -106,6 +106,10 @@ export default function connectAdvanced( `or explicitly pass "${storeKey}" as a prop to "${displayName}".` ) + // make sure `getState` is properly bound in order to avoid breaking + // custom store implementations that rely on the store's context + this.getState = this.store.getState.bind(this.store); + this.initSelector() this.initSubscription() } @@ -160,7 +164,7 @@ export default function connectAdvanced( initSelector() { const { dispatch } = this.store - let getState = this.store.getState.bind(this.store) + const { getState } = this; const sourceSelector = selectorFactory(dispatch, selectorFactoryOptions) // wrap the selector in an object that tracks its results between runs diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index eadd2e927..b7f8e2a16 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -25,6 +25,30 @@ describe('React', () => { } } + class ContextBoundStore { + constructor(reducer) { + this.reducer = reducer + this.listeners = [] + this.state = undefined + this.dispatch({}) + } + + getState() { + return this.state + } + + subscribe(listener) { + this.listeners.push(listener) + return (() => this.listeners.filter(l => l !== listener)) + } + + dispatch(action) { + this.state = this.reducer(this.getState(), action) + this.listeners.forEach(l => l()) + return action + } + } + ProviderMock.childContextTypes = { store: PropTypes.object.isRequired } @@ -134,6 +158,30 @@ describe('React', () => { expect(stub.props.string).toBe('ab') }) + it('should retain the store\'s context', () => { + const store = new ContextBoundStore(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') + }) + it('should handle dispatches before componentDidMount', () => { const store = createStore(stringBuilder) From fdd4a8203282eae26b8d07110c89150328df559b Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Tue, 23 Aug 2016 20:49:02 -0400 Subject: [PATCH 086/107] tests and fixes #457 --- src/utils/Subscription.js | 69 +++++++++++++++++----------- test/components/connect.spec.js | 80 +++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 25 deletions(-) diff --git a/src/utils/Subscription.js b/src/utils/Subscription.js index b3b63ec4d..ff4a063c1 100644 --- a/src/utils/Subscription.js +++ b/src/utils/Subscription.js @@ -1,6 +1,44 @@ // encapsulates the subscription logic for connecting a component to the redux store, as // well as nesting subscriptions of descendant components, so that we can ensure the // ancestor components re-render before descendants + +function initListeners() { + let count = 0 + let current = [] + let next = [] + + return { + clear() { + count = 0 + next = null + current = null + }, + + notify() { + current = next + for (let i = 0; i < count; i++) { + current[i]() + } + }, + + subscribe(listener) { + let isSubscribed = true + if (next === current) next = current.slice() + next.push(listener) + count++ + + return function unsubscribe() { + if (!isSubscribed || count === 0) return + isSubscribed = false + + if (next === current) next = current.slice() + next.splice(next.indexOf(listener), 1) + count-- + } + } + } +} + export default class Subscription { constructor(store, parentSub) { this.subscribe = parentSub @@ -8,38 +46,16 @@ export default class Subscription { : store.subscribe.bind(store) this.unsubscribe = null - this.nextListeners = this.currentListeners = [] - } - - ensureCanMutateNextListeners() { - if (this.nextListeners === this.currentListeners) { - this.nextListeners = this.currentListeners.slice() - } + this.listeners = initListeners() } addNestedSub(listener) { this.trySubscribe() - - let isSubscribed = true - this.ensureCanMutateNextListeners() - this.nextListeners.push(listener) - - return function unsubscribe() { - if (!isSubscribed) return - isSubscribed = false - - this.ensureCanMutateNextListeners() - const index = this.nextListeners.indexOf(listener) - this.nextListeners.splice(index, 1) - } + return this.listeners.subscribe(listener) } notifyNestedSubs() { - const listeners = this.currentListeners = this.nextListeners - const length = listeners.length - for (let i = 0; i < length; i++) { - listeners[i]() - } + this.listeners.notify() } isSubscribed() { @@ -55,7 +71,10 @@ export default class Subscription { tryUnsubscribe() { if (this.unsubscribe) { this.unsubscribe() + this.listeners.clear() } this.unsubscribe = null + this.subscribe = null + this.listeners = { notify() {} } } } diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index b7f8e2a16..627f7ff75 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -910,6 +910,86 @@ describe('React', () => { expect(mapStateToPropsCalls).toBe(1) }) + it('should not attempt to set state after unmounting nested components', () => { + const store = createStore(() => ({})) + let mapStateToPropsCalls = 0 + + let linkA, linkB + + let App = ({ children, setLocation }) => { + const onClick = to => event => { + event.preventDefault() + setLocation(to) + } + /* eslint-disable react/jsx-no-bind */ + return ( + + ) + /* eslint-enable react/jsx-no-bind */ + } + App = connect(() => ({}))(App) + + + let A = () => (

A

) + A = connect(() => ({ calls: ++mapStateToPropsCalls }))(A) + + + const B = () => (

B

) + + + class RouterMock extends React.Component { + constructor(...args) { + super(...args) + this.state = { location: { pathname: 'a' } } + this.setLocation = this.setLocation.bind(this) + } + + setLocation(pathname) { + this.setState({ location: { pathname } }) + store.dispatch({ type: 'TEST' }) + } + + getChildComponent(location) { + switch (location) { + case 'a': return + case 'b': return + default: throw new Error('Unknown location: ' + location) + } + } + + render() { + return ( + {this.getChildComponent(this.state.location.pathname)} + ) + } + } + + + const div = document.createElement('div') + document.body.appendChild(div) + ReactDOM.render( + ( + + ), + div + ) + + const spy = expect.spyOn(console, 'error') + + linkA.click() + linkB.click() + linkB.click() + + spy.destroy() + document.body.removeChild(div) + expect(mapStateToPropsCalls).toBe(3) + expect(spy.calls.length).toBe(0) + }) + it('should not attempt to set state when dispatching in componentWillUnmount', () => { const store = createStore(stringBuilder) let mapStateToPropsCalls = 0 From 5cb358ca15a584714b8485068ceab10ece7588dc Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Tue, 23 Aug 2016 21:07:53 -0400 Subject: [PATCH 087/107] refactors out count variable in Subscription.js --- src/utils/Subscription.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/utils/Subscription.js b/src/utils/Subscription.js index ff4a063c1..d7854b9a1 100644 --- a/src/utils/Subscription.js +++ b/src/utils/Subscription.js @@ -3,20 +3,18 @@ // ancestor components re-render before descendants function initListeners() { - let count = 0 let current = [] let next = [] return { clear() { - count = 0 next = null current = null }, notify() { current = next - for (let i = 0; i < count; i++) { + for (let i = 0; i < current.length; i++) { current[i]() } }, @@ -25,15 +23,13 @@ function initListeners() { let isSubscribed = true if (next === current) next = current.slice() next.push(listener) - count++ return function unsubscribe() { - if (!isSubscribed || count === 0) return + if (!isSubscribed || !current) return isSubscribed = false if (next === current) next = current.slice() next.splice(next.indexOf(listener), 1) - count-- } } } From 3b60a7f5a2f73bbbc347b4373e42588a900990e9 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Thu, 25 Aug 2016 08:07:27 -0400 Subject: [PATCH 088/107] refactors subscription to be slightly clearer --- src/utils/Subscription.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/utils/Subscription.js b/src/utils/Subscription.js index d7854b9a1..98daa442a 100644 --- a/src/utils/Subscription.js +++ b/src/utils/Subscription.js @@ -2,14 +2,18 @@ // well as nesting subscriptions of descendant components, so that we can ensure the // ancestor components re-render before descendants -function initListeners() { +const CLEARED = null + +function createListenerCollection() { + // the current/next pattern is copied from redux's createStore code. + // TODO: refactor+expose that code to be reusable here? let current = [] let next = [] return { clear() { - next = null - current = null + next = CLEARED + current = CLEARED }, notify() { @@ -25,7 +29,7 @@ function initListeners() { next.push(listener) return function unsubscribe() { - if (!isSubscribed || !current) return + if (!isSubscribed || current === CLEARED) return isSubscribed = false if (next === current) next = current.slice() @@ -42,7 +46,7 @@ export default class Subscription { : store.subscribe.bind(store) this.unsubscribe = null - this.listeners = initListeners() + this.listeners = createListenerCollection() } addNestedSub(listener) { From c00f42e33c510a5a20e6459342fad90ca72f98a5 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Sat, 27 Aug 2016 11:40:51 -0400 Subject: [PATCH 089/107] exposes new features to the API --- src/connect/connect.js | 9 --------- src/index.js | 3 ++- test/components/connect.spec.js | 3 +-- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/connect/connect.js b/src/connect/connect.js index 9c4611983..a151f54ac 100644 --- a/src/connect/connect.js +++ b/src/connect/connect.js @@ -39,18 +39,9 @@ export function buildConnectOptions( mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory, pure = true, - __ENABLE_SECRET_EXPERIMENTAL_FEATURES_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = false, ...options } = {} ) { - if (!__ENABLE_SECRET_EXPERIMENTAL_FEATURES_DO_NOT_USE_OR_YOU_WILL_BE_FIRED) { - mapStateToPropsFactories = defaultMapStateToPropsFactories - mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories - mergePropsFactories = defaultMergePropsFactories - selectorFactory = defaultSelectorFactory - options = { withRef: options.withRef } - } - const initMapStateToProps = match(mapStateToProps, mapStateToPropsFactories) const initMapDispatchToProps = match(mapDispatchToProps, mapDispatchToPropsFactories) const initMergeProps = match(mergeProps, mergePropsFactories) diff --git a/src/index.js b/src/index.js index 2384a4428..9ce59e7c4 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,5 @@ import Provider from './components/Provider' +import connectAdvanced from './components/connectAdvanced' import connect from './connect/connect' -export { Provider, connect } +export { Provider, connectAdvanced, connect } diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 627f7ff75..c79a8aa48 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -2001,8 +2001,7 @@ describe('React', () => { }) it('should allow custom displayName', () => { - // TODO remove __ENABLE_SECRET_EXPERIMENTAL_FEATURES_DO_NOT_USE_OR_YOU_WILL_BE_FIRED once approved - @connect(null, null, null, { getDisplayName: name => `Custom(${name})`, __ENABLE_SECRET_EXPERIMENTAL_FEATURES_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: true }) + @connect(null, null, null, { getDisplayName: name => `Custom(${name})` }) class MyComponent extends React.Component { render() { return
From c08eb7f5f27352bacdcd714cda770d4c345157f7 Mon Sep 17 00:00:00 2001 From: Denis Bardadym Date: Tue, 27 Sep 2016 19:46:13 +0300 Subject: [PATCH 090/107] Add es2015 modules export from redux project (#501) * Remove Style section Obviously that didn't happen... * Add es2015 modules export from redux project --- .babelrc | 11 ++++++++++- .gitignore | 1 + build/use-lodash-es.js | 10 ++++++++++ package.json | 17 +++++++++++------ 4 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 build/use-lodash-es.js diff --git a/.babelrc b/.babelrc index 67435a531..8c6f5b857 100644 --- a/.babelrc +++ b/.babelrc @@ -18,7 +18,6 @@ "transform-es2015-parameters", ["transform-es2015-destructuring", { "loose": true }], "transform-es2015-block-scoping", - ["transform-es2015-modules-commonjs", { "loose": true }], "transform-object-rest-spread", "transform-react-jsx", "syntax-jsx" @@ -26,6 +25,16 @@ "env": { "test": { "plugins": ["istanbul"] + }, + "commonjs": { + "plugins": [ + ["transform-es2015-modules-commonjs", { "loose": true }] + ] + }, + "es": { + "plugins": [ + "./build/use-lodash-es" + ] } } } diff --git a/.gitignore b/.gitignore index f53a08f0e..2cd2cfe8d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist lib .nyc_output coverage +es diff --git a/build/use-lodash-es.js b/build/use-lodash-es.js new file mode 100644 index 000000000..ba4f2f52e --- /dev/null +++ b/build/use-lodash-es.js @@ -0,0 +1,10 @@ +module.exports = function () { + return { + visitor: { + ImportDeclaration(path) { + var source = path.node.source + source.value = source.value.replace(/^lodash($|\/)/, 'lodash-es$1') + } + } + } +} diff --git a/package.json b/package.json index d390b5d39..780eee785 100644 --- a/package.json +++ b/package.json @@ -3,16 +3,19 @@ "version": "4.4.5", "description": "Official React bindings for Redux", "main": "./lib/index.js", + "module": "es/index.js", + "jsnext:main": "es/index.js", "typings": "./index.d.ts", "scripts": { - "build:lib": "babel src --out-dir lib", - "build:umd": "cross-env NODE_ENV=development webpack src/index.js dist/react-redux.js", - "build:umd:min": "cross-env NODE_ENV=production webpack src/index.js dist/react-redux.min.js", - "build": "npm run build:lib && npm run build:umd && npm run build:umd:min && node ./prepublish", - "clean": "rimraf lib dist coverage", + "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib", + "build:es": "cross-env BABEL_ENV=es babel src --out-dir es", + "build:umd": "cross-env BABEL_ENV=commonjs NODE_ENV=development webpack src/index.js dist/redux.js", + "build:umd:min": "cross-env BABEL_ENV=commonjs NODE_ENV=production webpack src/index.js dist/redux.min.js", + "build": "npm run build:commonjs && npm run build:es && npm run build:umd && npm run build:umd:min", + "clean": "rimraf lib dist es coverage", "lint": "eslint src test", "prepublish": "npm run clean && npm run build", - "test": "cross-env NODE_ENV=test mocha --compilers js:babel-register --recursive --require ./test/setup.js", + "test": "cross-env BABEL_ENV=commonjs NODE_ENV=test mocha --compilers js:babel-register --recursive --require ./test/setup.js", "test:watch": "npm test -- --watch", "test:cov": "cross-env NODE_ENV=test nyc npm test", "coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov" @@ -25,6 +28,7 @@ "dist", "lib", "src", + "es" "index.d.ts" ], "keywords": [ @@ -97,6 +101,7 @@ "hoist-non-react-statics": "^1.0.3", "invariant": "^2.0.0", "lodash": "^4.2.0", + "lodash-es": "^4.2.0", "loose-envify": "^1.1.0" }, "peerDependencies": { From dfa32d73a8cc5f1fb1dc9ba24696a259f049244b Mon Sep 17 00:00:00 2001 From: Ron Cohen Date: Wed, 28 Sep 2016 15:44:17 +0200 Subject: [PATCH 091/107] Set displayName explicitly on Provider (#506) --- src/components/Provider.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Provider.js b/src/components/Provider.js index 7c9ba926d..6b079d5ec 100644 --- a/src/components/Provider.js +++ b/src/components/Provider.js @@ -51,3 +51,4 @@ Provider.propTypes = { Provider.childContextTypes = { store: storeShape.isRequired } +Provider.displayName = 'Provider' From d186b1f57adf6278cb4bc79c6f3aa2c238bebc1b Mon Sep 17 00:00:00 2001 From: ynonp Date: Sun, 2 Oct 2016 20:01:49 +0300 Subject: [PATCH 092/107] warn on duplicate props (#508) --- src/connect/mergeProps.js | 18 +++++++++++++++++- test/components/connect.spec.js | 26 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/connect/mergeProps.js b/src/connect/mergeProps.js index f2a42c42b..26f9c49d4 100644 --- a/src/connect/mergeProps.js +++ b/src/connect/mergeProps.js @@ -1,8 +1,24 @@ import shallowEqual from '../utils/shallowEqual' import verifyPlainObject from '../utils/verifyPlainObject' +import warning from '../utils/warning' export function defaultMergeProps(stateProps, dispatchProps, ownProps) { - return { ...ownProps, ...stateProps, ...dispatchProps } + if (process.env.NODE_ENV !== 'production') { + const stateKeys = Object.keys(stateProps) + + for ( let key of stateKeys ) { + if (typeof ownProps[key] !== 'undefined') { + warning(false, `Duplicate key ${key} sent from both parent and state`) + break + } + } + } + + return { + ...ownProps, + ...stateProps, + ...dispatchProps + } } export function wrapMergePropsFunc(mergeProps) { diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index c79a8aa48..05f1eaa23 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -79,6 +79,32 @@ describe('React', () => { expect(container.context.store).toBe(store) }) + + it('should warn if same key is used in state and props', () => { + const store = createStore(() => ({ + abc: 'bar' + })) + + @connect(({ abc }) => ({ abc })) + class Container extends Component { + render() { + return + } + } + + const errorSpy = expect.spyOn(console, 'error') + + TestUtils.renderIntoDocument( + + + + ) + errorSpy.destroy() + expect(errorSpy).toHaveBeenCalled() + }) + + + it('should pass state and props to the given component', () => { const store = createStore(() => ({ foo: 'bar', From f05828a20409330cc1fee7b559416fe2e699d9f0 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Tue, 4 Oct 2016 20:28:46 -0400 Subject: [PATCH 093/107] Update API docs for v5 (#480) * updates docs for changes in v5 (WORK IN PROGRESS) * update docs for changes in v5 * removes unused code from example * adds a tags to api.md for direct linking --- docs/api.md | 132 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 108 insertions(+), 24 deletions(-) diff --git a/docs/api.md b/docs/api.md index a054f3fe8..4e5a04193 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,5 +1,6 @@ ## API +
### `` 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 ``. @@ -40,28 +41,38 @@ ReactDOM.render( ) ``` + + ### `connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])` -Connects a React component to a Redux store. +Connects a React component to a Redux store. `connect` is a facade around `connectAdvanced`, providing a convenient API for the most common use cases. -It does not modify the component class passed to it. -Instead, it *returns* a new, connected component class, for you to use. +It does not modify the component class passed to it; instead, it *returns* a new, connected component class for you to use. + #### Arguments * [`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 props passed to your component, and `mapStateToProps` will be additionally re-invoked whenever the component receives new props (e.g. if props received from a parent component have shallowly changed, and you use the ownProps argument, mapStateToProps is re-evaluated). >Note: in advanced scenarios where you need more control over the rendering performance, `mapStateToProps()` can also return a function. In this case, *that* function will be used as `mapStateToProps()` for a particular component instance. This allows you to do per-instance memoization. You can refer to [#279](https://github.com/reactjs/react-redux/pull/279) and the tests it adds for more details. Most apps never need this. + >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/reactjs/reselect) to efficiently compose selectors and [compute derived data](http://redux.js.org/docs/recipes/ComputingDerivedData.html). + * [`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 with every action creator wrapped into a `dispatch` call so they may be invoked directly, 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://reactjs.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 props passed to your component, and `mapDispatchToProps` will be re-invoked whenever the component receives new props. >Note: in advanced scenarios where you need more control over the rendering performance, `mapDispatchToProps()` can also return a function. In this case, *that* function will be used as `mapDispatchToProps()` for a particular component instance. This allows you to do per-instance memoization. You can refer to [#279](https://github.com/reactjs/react-redux/pull/279) and the tests it adds for more details. Most apps never need this. * [`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 = 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`.* +* [`options`] *(Object)* If specified, further customizes the behavior of the connector. In addition to the options passable to `connectAdvanced()` (see those below), `connect()` accepts these additional options: + * [`pure`] *(Boolean)*: If true, `connect()` will avoid re-renders and calls to `mapStateToProps`, `mapDispatchToProps`, and `mergeProps` if the relevant state/props objects remain equal based on their respective equality checks. Assumes that the wrapped 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. Default value: `true` + * [`areStatesEqual`] *(Function)*: When pure, compares incoming store state to its previous value. Default value: `strictEqual (===)` + * [`areOwnPropsEqual`] *(Function)*: When pure, compares incoming props to its previous value. Default value: `shallowEqual` + * [`areStatePropsEqual`] *(Function)*: When pure, compares the result of `mapStateToProps` to its previous value. Default value: `shallowEqual` + * [`areMergedPropsEqual`] *(Function)*: When pure, compares the result of `mergeProps` to its previous value. Default value: `shallowEqual` + + +##### The arity of mapStateToProps and mapDispatchToProps determines whether they receive ownProps > Note: `ownProps` **is not passed** to `mapStateToProps` and `mapDispatchToProps` if formal definition of the function contains one mandatory parameter (function has length 1). For example, function defined like below won't receive `ownProps` as the second argument. ```javascript @@ -96,33 +107,26 @@ const mapStateToProps = (...args) => { } ``` + +##### Optimizing connect when options.pure is true -#### 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. +When `options.pure` is true, `connect` performs several equality checks that are used to avoid unncessary calls to `mapStateToProps`, `mapDispatchToProps`, `mergeProps`, and ultimately to `render`. These include `areStatesEqual`, `areOwnPropsEqual`, `areStatePropsEqual`, and `areMergedPropsEqual`. While the defaults are probably appropriate 99% of the time, you may wish to override them with custom implementations for performance or other reasons. Here are several examples: -##### Instance Methods +* You may wish to override `areStatesEqual` if your `mapStateToProps` function is computationally expensive and is also only concerned with a small slice of your state. For example: `areStatesEqual: (prev, next) => prev.entities.todos === next.entities.todos`; this would effectively ignore state changes for everything but that slice of state. -###### `getWrappedInstance(): ReactComponent` +* You may wish to override `areStatesEqual` to always return false (`areStatesEqual: () => false`) if you have impure reducers that mutate your store state. (This would likely impact the other equality checks is well, depending on your `mapStateToProps` function.) -Returns the wrapped component instance. Only available if you pass `{ withRef: true }` as part of the `connect()`’s fourth `options` argument. +* You may wish to override `areOwnPropsEqual` as a way to whitelist incoming props. You'd also have to implement `mapStateToProps`, `mapDispatchToProps` and `mergeProps` to also whitelist props. (It may be simpler to achieve this other ways, for example by using [recompose's mapProps](https://github.com/acdlite/recompose/blob/master/docs/API.md#mapprops).) -#### Remarks +* You may wish to override `areStatePropsEqual` to use `strictEqual` if your `mapStateToProps` uses a memoized selector that will only return a new object if a relevant prop has changed. This would be a very slight performance improvement, since would avoid extra equality checks on individual props each time `mapStateToProps` is called. -* 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)`. +* You may wish to override `areMergedPropsEqual` to implement a `deepEqual` if your selectors produce complex props. ex: nested objects, new arrays, etc. (The deep equal check should be faster than just re-rendering.) -* It does not modify the passed React component. It returns a new, connected component, that you should use instead. +#### Returns -* 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/reactjs/reselect) to efficiently compose selectors and [compute derived data](http://redux.js.org/docs/recipes/ComputingDerivedData.html). +A higher-order React component class that passes state and action creators into your component derived from the supplied arguments. This is created by `connectAdvanced`, and details of this higher-order component are covered there. + #### Examples ##### Inject just `dispatch` and don't listen to store @@ -294,3 +298,83 @@ function mergeProps(stateProps, dispatchProps, ownProps) { export default connect(mapStateToProps, actionCreators, mergeProps)(TodoApp) ``` + + +### `connectAdvanced(selectorFactory, [connectOptions])` + +Connects a React component to a Redux store. It is the base for `connect()` but is less opinionated about how to combine `state`, `props`, and `dispatch` into your final props. It makes no assumptions about defaults or memoization of results, leaving those responsibilities to the caller. + +It does not modify the component class passed to it; instead, it *returns* a new, connected component class for you to use. + + +#### Arguments + +* `selectorFactory(dispatch, factoryOptions): selector(state, ownProps): props` \(*Function*): Intializes a selector function (during each instance's constructor). That selector function is called any time the connector component needs to compute new props, as a result of a store state change or receiving new props. The result of `selector` is expected to be a plain object, which is passed as the props to the wrapped component. If a consecutive call to `selector` returns the same object (`===`) as its previous call, the component will not be re-rendered. It's the responsibility of `selector` to return that previous object when appropriate. + +* [`connectOptions`] *(Object)* If specified, further customizes the behavior of the connector. + + * [`getDisplayName`] *(Function)*: computes the connector component's displayName property relative to that of the wrapped component. Usually overridden by wrapper functions. Default value: `name => 'ConnectAdvanced('+name+')'` + + * [`methodName`] *(String)*: shown in error messages. Usually overridden by wrapper functions. Default value: `'connectAdvanced'` + + * [`renderCountProp`] *(String)*: if defined, a property named this value will be added to the props passed to the wrapped component. Its value will be the number of times the component has been rendered, which can be useful for tracking down unnecessary re-renders. Default value: `undefined` + + * [`shouldHandleStateChanges`] *(Boolean)*: controls whether the connector component subscribes to redux store state changes. If set to false, it will only re-render on `componentWillReceiveProps`. Default value: `true` + + * [`storeKey`] *(String)*: the key of props/context to get the store. You probably only need this if you are in the inadvisable position of having multiple stores. Default value: `'store'` + + * [`withRef`] *(Boolean)*: If true, stores a ref to the wrapped component instance and makes it available via `getWrappedInstance()` method. Default value: `false` + + * Addionally, any extra options passed via `connectOptions` will be passed through to your `selectorFactory` in the `factoryOptions` argument. + + +#### Returns + +A higher-order React component class that builds props from the store state and passes them to the wrapped component. A higher-order component is a function which accepts a component argument and returns a new component. + +##### Static Properties + +* `WrappedComponent` *(Component)*: The original component class passed to `connectAdvanced(...)(Component)`. + +##### 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 `options` argument. + +#### Remarks + +* Since it returns a higher-order component, it needs to be invoked two times. The first time with its arguments as described above, and a second time, with the component: `connectAdvanced(selectorFactory)(MyComponent)`. + +* It does not modify the passed React component. It returns a new, connected component, that you should use instead. + + +#### Examples + +##### Inject `todos` of a specific user depending on props, and inject `props.userId` into the action +```js +import * as actionCreators from './actionCreators' +import { bindActionCreators } from 'redux' + +function selectorFactory(dispatch) { + let state = {} + let ownProps = {} + let result = {} + const actions = bindActionCreators(actionCreators, dispatch) + const addTodo = (text) => actions.addTodo(ownProps.userId, text) + return (nextState, nextOwnProps) => { + const todos = nextState.todos[nextProps.userId] + const nextResult = { ...nextOwnProps, todos, addTodo } + state = nextState + ownProps = nextOwnProps + if (!shallowEqual(result, nextResult)) result = nextResult + return result + } +} +export default connectAdvanced(selectorFactory)(TodoApp) +``` + From 92e55b85c53a103200dc2b79ae8345074cb82384 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Tue, 4 Oct 2016 20:31:45 -0400 Subject: [PATCH 094/107] groups factories options and move *areEqual defaults into connect (#477) * groups factories options and move *areEqual defaults into connect * refactors connect so that new "factories" extension points aren't in main API --- src/connect/connect.js | 83 ++++++++++++++++++++-------------- src/connect/mergeProps.js | 3 +- src/connect/selectorFactory.js | 9 +--- 3 files changed, 50 insertions(+), 45 deletions(-) diff --git a/src/connect/connect.js b/src/connect/connect.js index a151f54ac..cb82945a6 100644 --- a/src/connect/connect.js +++ b/src/connect/connect.js @@ -1,4 +1,5 @@ import connectAdvanced from '../components/connectAdvanced' +import shallowEqual from '../utils/shallowEqual' import defaultMapDispatchToPropsFactories from './mapDispatchToProps' import defaultMapStateToPropsFactories from './mapStateToProps' import defaultMergePropsFactories from './mergeProps' @@ -29,46 +30,58 @@ function match(arg, factories) { return undefined } -export function buildConnectOptions( - mapStateToProps, - mapDispatchToProps, - mergeProps, - { - mapStateToPropsFactories = defaultMapStateToPropsFactories, - mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, - mergePropsFactories = defaultMergePropsFactories, - selectorFactory = defaultSelectorFactory, - pure = true, - ...options - } = {} -) { - const initMapStateToProps = match(mapStateToProps, mapStateToPropsFactories) - const initMapDispatchToProps = match(mapDispatchToProps, mapDispatchToPropsFactories) - const initMergeProps = match(mergeProps, mergePropsFactories) +function strictEqual(a, b) { return a === b } - return { - // used in error messages - methodName: 'connect', +// createConnect with default args builds the 'official' connect behavior. Calling it with +// different options opens up some testing and extensibility scenarios +export function createConnect({ + connectHOC = connectAdvanced, + mapStateToPropsFactories = defaultMapStateToPropsFactories, + mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, + mergePropsFactories = defaultMergePropsFactories, + selectorFactory = defaultSelectorFactory +} = {}) { + return function connect( + mapStateToProps, + mapDispatchToProps, + mergeProps, + { + pure = true, + areStatesEqual = strictEqual, + areOwnPropsEqual = shallowEqual, + areStatePropsEqual = shallowEqual, + areMergedPropsEqual = shallowEqual, + ...extraOptions + } = {} + ) { + const initMapStateToProps = match(mapStateToProps, mapStateToPropsFactories) + const initMapDispatchToProps = match(mapDispatchToProps, mapDispatchToPropsFactories) + const initMergeProps = match(mergeProps, mergePropsFactories) - // used to compute Connect's displayName from the wrapped component's displayName. - getDisplayName: name => `Connect(${name})`, + return connectHOC(selectorFactory, { + // used in error messages + methodName: 'connect', - // if mapStateToProps is falsy, the Connect component doesn't subscribe to store state changes - shouldHandleStateChanges: Boolean(mapStateToProps), + // used to compute Connect's displayName from the wrapped component's displayName. + getDisplayName: name => `Connect(${name})`, - // passed through to selectorFactory - selectorFactory, - initMapStateToProps, - initMapDispatchToProps, - initMergeProps, - pure, + // if mapStateToProps is falsy, the Connect component doesn't subscribe to store state changes + shouldHandleStateChanges: Boolean(mapStateToProps), - // any addional options args can override defaults of connect or connectAdvanced - ...options + // passed through to selectorFactory + initMapStateToProps, + initMapDispatchToProps, + initMergeProps, + pure, + areStatesEqual, + areOwnPropsEqual, + areStatePropsEqual, + areMergedPropsEqual, + + // any extra options args can override defaults of connect or connectAdvanced + ...extraOptions + }) } } -export default function connect(...args) { - const options = buildConnectOptions(...args) - return connectAdvanced(options.selectorFactory, options) -} +export default createConnect() diff --git a/src/connect/mergeProps.js b/src/connect/mergeProps.js index 26f9c49d4..0a9827328 100644 --- a/src/connect/mergeProps.js +++ b/src/connect/mergeProps.js @@ -1,4 +1,3 @@ -import shallowEqual from '../utils/shallowEqual' import verifyPlainObject from '../utils/verifyPlainObject' import warning from '../utils/warning' @@ -23,7 +22,7 @@ export function defaultMergeProps(stateProps, dispatchProps, ownProps) { export function wrapMergePropsFunc(mergeProps) { return function initMergePropsProxy( - dispatch, { displayName, pure, areMergedPropsEqual = shallowEqual } + dispatch, { displayName, pure, areMergedPropsEqual } ) { let hasRunOnce = false let mergedProps diff --git a/src/connect/selectorFactory.js b/src/connect/selectorFactory.js index 47593aa9b..4479431e0 100644 --- a/src/connect/selectorFactory.js +++ b/src/connect/selectorFactory.js @@ -1,5 +1,4 @@ import verifySubselectors from './verifySubselectors' -import shallowEqual from '../utils/shallowEqual' export function impureFinalPropsSelectorFactory( mapStateToProps, @@ -16,18 +15,12 @@ export function impureFinalPropsSelectorFactory( } } -function strictEqual(a, b) { return a === b } - export function pureFinalPropsSelectorFactory( mapStateToProps, mapDispatchToProps, mergeProps, dispatch, - { - areStatesEqual = strictEqual, - areOwnPropsEqual = shallowEqual, - areStatePropsEqual = shallowEqual - } + { areStatesEqual, areOwnPropsEqual, areStatePropsEqual } ) { let hasRunAtLeastOnce = false let state From 774f8a1febb31819a188d88d3f8841b6d7e93e0a Mon Sep 17 00:00:00 2001 From: Tim Dorr Date: Tue, 4 Oct 2016 20:41:19 -0400 Subject: [PATCH 095/107] Whoops, didn't bump the version in git... --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 780eee785..506a82828 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-redux", - "version": "4.4.5", + "version": "5.0.0-beta.1", "description": "Official React bindings for Redux", "main": "./lib/index.js", "module": "es/index.js", From 0e14a0510671af7bfdb99c9a11bcb4727e0eb894 Mon Sep 17 00:00:00 2001 From: Tim Dorr Date: Tue, 4 Oct 2016 20:41:48 -0400 Subject: [PATCH 096/107] 5.0.0-beta.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 506a82828..75cf8b388 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-redux", - "version": "5.0.0-beta.1", + "version": "5.0.0-beta.2", "description": "Official React bindings for Redux", "main": "./lib/index.js", "module": "es/index.js", From aa28a3f54b00fd5d441429e960984afea0861f6b Mon Sep 17 00:00:00 2001 From: Tim Dorr Date: Wed, 5 Oct 2016 10:02:19 -0400 Subject: [PATCH 097/107] Revert "warn on duplicate props" (#511) --- src/connect/mergeProps.js | 18 +----------------- test/components/connect.spec.js | 26 -------------------------- 2 files changed, 1 insertion(+), 43 deletions(-) diff --git a/src/connect/mergeProps.js b/src/connect/mergeProps.js index 0a9827328..9ef1c8ee5 100644 --- a/src/connect/mergeProps.js +++ b/src/connect/mergeProps.js @@ -1,23 +1,7 @@ import verifyPlainObject from '../utils/verifyPlainObject' -import warning from '../utils/warning' export function defaultMergeProps(stateProps, dispatchProps, ownProps) { - if (process.env.NODE_ENV !== 'production') { - const stateKeys = Object.keys(stateProps) - - for ( let key of stateKeys ) { - if (typeof ownProps[key] !== 'undefined') { - warning(false, `Duplicate key ${key} sent from both parent and state`) - break - } - } - } - - return { - ...ownProps, - ...stateProps, - ...dispatchProps - } + return { ...ownProps, ...stateProps, ...dispatchProps } } export function wrapMergePropsFunc(mergeProps) { diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 05f1eaa23..c79a8aa48 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -79,32 +79,6 @@ describe('React', () => { expect(container.context.store).toBe(store) }) - - it('should warn if same key is used in state and props', () => { - const store = createStore(() => ({ - abc: 'bar' - })) - - @connect(({ abc }) => ({ abc })) - class Container extends Component { - render() { - return - } - } - - const errorSpy = expect.spyOn(console, 'error') - - TestUtils.renderIntoDocument( - - - - ) - errorSpy.destroy() - expect(errorSpy).toHaveBeenCalled() - }) - - - it('should pass state and props to the given component', () => { const store = createStore(() => ({ foo: 'bar', From 4462a201e1b77dcc340927d8661408c7811d989d Mon Sep 17 00:00:00 2001 From: Tim Dorr Date: Wed, 5 Oct 2016 10:02:48 -0400 Subject: [PATCH 098/107] 5.0.0-beta.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 75cf8b388..84862352e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-redux", - "version": "5.0.0-beta.2", + "version": "5.0.0-beta.3", "description": "Official React bindings for Redux", "main": "./lib/index.js", "module": "es/index.js", From 5074c819f5ae5882e91b11e222ba14e9bf1484b3 Mon Sep 17 00:00:00 2001 From: Jim Bolla Date: Tue, 11 Oct 2016 19:59:03 -0400 Subject: [PATCH 099/107] Supress eslint error "react/display-name" at intentional omission. --- test/components/connect.spec.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index c79a8aa48..578f9b1fa 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -1369,11 +1369,15 @@ describe('React', () => { ).displayName).toBe('Connect(Bar)') expect(connect(state => state)( + // eslint: In this case, we don't want to specify a displayName because we're testing what + // happens when one isn't defined. + /* eslint-disable react/display-name */ createClass({ render() { return
} }) + /* eslint-enable react/display-name */ ).displayName).toBe('Connect(Component)') }) From ad7a329259afec74f931ebb955c3ae5957a38c93 Mon Sep 17 00:00:00 2001 From: Scott Kyle Date: Thu, 13 Oct 2016 19:15:54 -0700 Subject: [PATCH 100/107] Fix error from clear() called during notify() (#517) The `current` variable would be set to `null` when `clear()` was called, which would cause an exception if there was more than one listener. The new test would fail before this change. --- src/utils/Subscription.js | 6 ++--- test/components/connect.spec.js | 46 +++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/utils/Subscription.js b/src/utils/Subscription.js index 98daa442a..4b3331ccf 100644 --- a/src/utils/Subscription.js +++ b/src/utils/Subscription.js @@ -17,9 +17,9 @@ function createListenerCollection() { }, notify() { - current = next - for (let i = 0; i < current.length; i++) { - current[i]() + const listeners = current = next + for (let i = 0; i < listeners.length; i++) { + listeners[i]() } }, diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 578f9b1fa..2b63bdb2f 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -910,6 +910,52 @@ describe('React', () => { expect(mapStateToPropsCalls).toBe(1) }) + it('should not attempt to notify unmounted child of state change', () => { + const store = createStore(stringBuilder) + + @connect((state) => ({ hide: state === 'AB' })) + class App extends Component { + render() { + return this.props.hide ? null : + } + } + + @connect(() => ({})) + class Container extends Component { + render() { + return ( + + ) + } + } + + @connect((state) => ({ state })) + class Child extends Component { + componentWillReceiveProps(nextProps) { + if (nextProps.state === 'A') { + store.dispatch({ type: 'APPEND', body: 'B' }); + } + } + render() { + return null; + } + } + + const div = document.createElement('div') + ReactDOM.render( + + + , + div + ) + + try { + store.dispatch({ type: 'APPEND', body: 'A' }) + } finally { + ReactDOM.unmountComponentAtNode(div) + } + }) + it('should not attempt to set state after unmounting nested components', () => { const store = createStore(() => ({})) let mapStateToPropsCalls = 0 From b3352bca6c833da38eaccc5bc0e1d26facbe6ce1 Mon Sep 17 00:00:00 2001 From: Benoit Benezech Date: Mon, 7 Nov 2016 19:18:12 +0100 Subject: [PATCH 101/107] add typings --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 84862352e..6f2bd20e0 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "dist", "lib", "src", - "es" + "es", "index.d.ts" ], "keywords": [ From 01d164c7d06770e8dbbd1eaabd5d520ade21c308 Mon Sep 17 00:00:00 2001 From: Benoit Benezech Date: Thu, 17 Nov 2016 16:12:48 +0100 Subject: [PATCH 102/107] remove separated null constrained signature --- index.d.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/index.d.ts b/index.d.ts index 9c1da0490..0d6fdf9cb 100644 --- a/index.d.ts +++ b/index.d.ts @@ -20,17 +20,12 @@ function connect( ): ComponentDecorator } & TOwnProps, TOwnProps>; function connect( - mapStateToProps: FuncOrSelf>, + mapStateToProps: FuncOrSelf>|null, mapDispatchToProps: FuncOrSelf | MapDispatchToPropsObject & TDispatchProps> ): ComponentDecorator; -function connect( - mapStateToProps: null, - mapDispatchToProps: FuncOrSelf | MapDispatchToPropsObject & TDispatchProps> -): ComponentDecorator; - function connect( - mapStateToProps: FuncOrSelf>, + mapStateToProps: FuncOrSelf>|null, mapDispatchToProps: FuncOrSelf| MapDispatchToPropsObject & TDispatchProps>, mergeProps: MergeProps, options?: Options From f4670f02d4e254190eb1a2c4feb36d9dea5edd60 Mon Sep 17 00:00:00 2001 From: Daniel Lytkin Date: Mon, 21 Nov 2016 17:42:40 +0700 Subject: [PATCH 103/107] export `connect` from TypeScript definitions file; add tests for definitions --- .travis.yml | 1 + index.d.ts | 8 +- package.json | 4 + test/typescript/test.tsx | 321 ++++++++++++++++++++++++++++++++++ test/typescript/tsconfig.json | 7 + 5 files changed, 337 insertions(+), 4 deletions(-) create mode 100644 test/typescript/test.tsx create mode 100644 test/typescript/tsconfig.json diff --git a/.travis.yml b/.travis.yml index 31bd8886a..e0ab47475 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,5 +4,6 @@ node_js: script: - npm run lint - npm run test:cov + - npm run test:typescript after_success: - npm run coverage diff --git a/index.d.ts b/index.d.ts index 0d6fdf9cb..18022a884 100644 --- a/index.d.ts +++ b/index.d.ts @@ -13,18 +13,18 @@ interface ComponentDecorator { * - TStateProps: Result of MapStateToProps * - TDispatchProps: Result of MapDispatchToProps */ -function connect(): ComponentDecorator<{ dispatch: Dispatch } & TOwnProps, TOwnProps>; +export function connect(): ComponentDecorator<{ dispatch: Dispatch } & TOwnProps, TOwnProps>; -function connect( +export function connect( mapStateToProps: FuncOrSelf>, ): ComponentDecorator } & TOwnProps, TOwnProps>; -function connect( +export function connect( mapStateToProps: FuncOrSelf>|null, mapDispatchToProps: FuncOrSelf | MapDispatchToPropsObject & TDispatchProps> ): ComponentDecorator; -function connect( +export function connect( mapStateToProps: FuncOrSelf>|null, mapDispatchToProps: FuncOrSelf| MapDispatchToPropsObject & TDispatchProps>, mergeProps: MergeProps, diff --git a/package.json b/package.json index 6f2bd20e0..aba0a7a42 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "test": "cross-env BABEL_ENV=commonjs NODE_ENV=test mocha --compilers js:babel-register --recursive --require ./test/setup.js", "test:watch": "npm test -- --watch", "test:cov": "cross-env NODE_ENV=test nyc npm test", + "test:typescript": "typings-tester --dir test/typescript", "coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov" }, "repository": { @@ -49,6 +50,7 @@ }, "homepage": "https://github.com/gaearon/react-redux", "devDependencies": { + "@types/react": "^0.14.49", "babel-cli": "^6.3.17", "babel-core": "^6.3.26", "babel-eslint": "^6.1.2", @@ -95,6 +97,8 @@ "react-dom": "^0.14.0", "redux": "^3.0.0", "rimraf": "^2.3.4", + "typescript": "^2.0.10", + "typings-tester": "^0.2.0", "webpack": "^1.11.0" }, "dependencies": { diff --git a/test/typescript/test.tsx b/test/typescript/test.tsx new file mode 100644 index 000000000..52f4b4b15 --- /dev/null +++ b/test/typescript/test.tsx @@ -0,0 +1,321 @@ +import {Dispatch, Store} from "redux"; +import {connect, Provider} from "../../index"; + + +function testNoArgs() { + const Connected = connect()(props => { + // typings:expect-error + props.foo; + + return