├── .babelrc ├── .github └── workflows │ ├── release.yml │ └── tests.yml ├── .gitignore ├── README.md ├── commitlint.config.js ├── docs └── rules │ ├── connect-prefer-minimum-two-arguments.md │ ├── connect-prefer-named-arguments.md │ ├── mapDispatchToProps-prefer-parameters-names.md │ ├── mapDispatchToProps-prefer-shorthand.md │ ├── mapDispatchToProps-returns-object.md │ ├── mapStateToProps-no-store.md │ ├── mapStateToProps-prefer-hoisted.md │ ├── mapStateToProps-prefer-parameters-names.md │ ├── mapStateToProps-prefer-selectors.md │ ├── no-unused-prop-types.md │ ├── prefer-separate-component-file.md │ └── useSelector-prefer-selectors.md ├── eslint.config.js ├── index.js ├── lib ├── filterReports.js ├── isReactReduxConnect.js ├── rules │ ├── connect-prefer-minimum-two-arguments.js │ ├── connect-prefer-named-arguments.js │ ├── mapDispatchToProps-prefer-parameters-names.js │ ├── mapDispatchToProps-prefer-shorthand.js │ ├── mapDispatchToProps-returns-object.js │ ├── mapStateToProps-no-store.js │ ├── mapStateToProps-prefer-hoisted.js │ ├── mapStateToProps-prefer-parameters-names.js │ ├── mapStateToProps-prefer-selectors.js │ ├── no-unused-prop-types.js │ ├── prefer-separate-component-file.js │ └── useSelector-prefer-selectors.js └── utils.js ├── package-lock.json ├── package.json └── tests ├── code-sanity-samples.js ├── index.js ├── lib └── rules │ ├── connect-prefer-minimum-two-arguments.js │ ├── connect-prefer-named-arguments.js │ ├── mapDispatchToProps-prefer-parameters-names.js │ ├── mapDispatchToProps-prefer-shorthand.js │ ├── mapDispatchToProps-returns-object.js │ ├── mapStateToProps-no-store.js │ ├── mapStateToProps-prefer-hoisted.js │ ├── mapStateToProps-prefer-parameters-names.js │ ├── mapStateToProps-prefer-selectors.js │ ├── no-unused-prop-types.js │ ├── prefer-separate-component-file.js │ └── useSelector-prefer-selectors.js └── util.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-object-rest-spread" 4 | ], 5 | "presets": [ 6 | [ 7 | "env", 8 | { 9 | "targets": { 10 | "node": 4 11 | } 12 | } 13 | ] 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | with: 14 | fetch-depth: 0 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: 20 19 | - name: Install dependencies 20 | run: npm ci 21 | - name: Release 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 25 | run: npx semantic-release 26 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [18.x, 20.x] 17 | eslint-version: [8.x, 9.x] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - name: Install ESLint ${{ matrix.eslint-version }} 26 | run: npm install eslint@${{ matrix.eslint-version }} 27 | - run: npm install --force 28 | - run: npm test 29 | env: 30 | CI: true 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslint-plugin-react-redux 2 | 3 | Enforcing best practices for react-redux 4 | 5 | ## Installation 6 | 7 | You'll first need to install [ESLint](http://eslint.org): 8 | 9 | ``` 10 | $ npm i eslint --save-dev 11 | ``` 12 | 13 | Next, install `eslint-plugin-react-redux`: 14 | 15 | ``` 16 | $ npm install eslint-plugin-react-redux --save-dev 17 | ``` 18 | 19 | **Note:** If you installed ESLint globally (using the `-g` flag) then you must also install `eslint-plugin-react-redux` globally. 20 | 21 | ## Usage 22 | 23 | Add `react-redux` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix: 24 | 25 | ```json 26 | { 27 | "plugins": [ 28 | "react-redux" 29 | ], 30 | "extends": [ 31 | "plugin:react-redux/recommended" 32 | ] 33 | } 34 | ``` 35 | 36 | 37 | To configure individual rules: 38 | 39 | ```json 40 | { 41 | "rules": { 42 | "react-redux/connect-prefer-named-arguments": 2 43 | } 44 | } 45 | ``` 46 | 47 | ## Supported Rules 48 | 49 | * [react-redux/connect-prefer-minimum-two-arguments](docs/rules/connect-prefer-minimum-two-arguments.md) Enforces that connect function has at least 2 arguments. 50 | * [react-redux/connect-prefer-named-arguments](docs/rules/connect-prefer-named-arguments.md) Enforces that all connect arguments have recommended names. 51 | * [react-redux/mapDispatchToProps-returns-object](docs/rules/mapDispatchToProps-returns-object.md) Enforces that mapDispatchToProps returns an object. 52 | * [react-redux/mapDispatchToProps-prefer-shorthand](docs/rules/mapDispatchToProps-prefer-shorthand.md) Enforces that all mapDispatchToProps use a shorthand method to wrap actions in dispatch calls whenever possible. 53 | * [react-redux/mapDispatchToProps-prefer-parameters-names](docs/rules/mapDispatchToProps-prefer-parameters-names.md) Enforces that all mapDispatchToProps parameters have specific names. 54 | * [react-redux/mapStateToProps-no-store](docs/rules/mapStateToProps-no-store.md) Prohibits binding a whole store object to a component. 55 | * [react-redux/mapStateToProps-prefer-hoisted](docs/rules/mapStateToProps-prefer-hoisted.md) Flags generation of copies of same-by-value but different-by-reference props. 56 | * [react-redux/mapStateToProps-prefer-parameters-names](docs/rules/mapStateToProps-prefer-parameters-names.md) Enforces that all mapStateToProps parameters have specific names. 57 | * [react-redux/mapStateToProps-prefer-selectors](docs/rules/mapStateToProps-prefer-selectors.md) Enforces that all mapStateToProps properties use selector functions. 58 | * [react-redux/useSelector-prefer-selectors](docs/rules/useSelector-prefer-selectors.md) Enforces that all useSelector properties use selector functions. 59 | * [react-redux/no-unused-prop-types](docs/rules/no-unused-prop-types.md) Extension of a react's no-unused-prop-types rule filtering out false positive used in redux context. 60 | * [react-redux/prefer-separate-component-file](docs/rules/prefer-separate-component-file.md) Enforces that all connected components are defined in a separate file. 61 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /docs/rules/connect-prefer-minimum-two-arguments.md: -------------------------------------------------------------------------------- 1 | # Enforces that connect function is provided with at least 2 arguments. (react-redux/connect-prefer-minimum-two-arguments) 2 | 3 | [react-redux mapStateToProps](https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options) 4 | 5 | > If you do not supply your own mapDispatchToProps function or object full of action creators, the default mapDispatchToProps implementation just injects dispatch into your component’s props. 6 | 7 | This rule enforces that the second argument is provided explicitly. 8 | 9 | ## Rule details 10 | 11 | The following pattern is considered incorrect: 12 | 13 | ```js 14 | connect(mapStateToProps)(Component) 15 | ``` 16 | 17 | The following patterns are considered correct: 18 | 19 | ```js 20 | connect(mapStateToProps, mapDispatchToProps, mergeProps)(Component) 21 | ``` 22 | 23 | ```js 24 | connect(mapStateToProps, mapDispatchToProps)(Component) 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/rules/connect-prefer-named-arguments.md: -------------------------------------------------------------------------------- 1 | # Enforces that all connect arguments have recommended names. (react-redux/connect-prefer-named-arguments) 2 | 3 | [react-redux connect](https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options) function has 4 optional arguments: 4 | * mapStateToProps 5 | * mapDispatchToProps 6 | * mergeProps 7 | * options 8 | 9 | This rule enforces that all of the provided parameters should follow the above naming conventions. 10 | 11 | ## Rule details 12 | 13 | The following patterns are considered incorrect: 14 | 15 | ```js 16 | connect(mapStateToProps, actionCreators)(TodoApp) 17 | ``` 18 | 19 | ```js 20 | connect(state => state)(TodoApp) 21 | ``` 22 | 23 | The following patterns are considered correct: 24 | 25 | ```js 26 | connect(mapStateToProps, mapDispatchToProps, mergeProps)(TodoApp) 27 | ``` 28 | 29 | ```js 30 | connect()(TodoApp) 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/rules/mapDispatchToProps-prefer-parameters-names.md: -------------------------------------------------------------------------------- 1 | # Enforces that all mapDispatchToProps parameters have specific names. (react-redux/mapDispatchToProps-prefer-parameters-names) 2 | 3 | [react-redux mapStateToProps](https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options) function has 2 optional arguments: 4 | * state 5 | * ownProps 6 | 7 | This rule enforces that all of the provided parameters should follow the above naming conventions. 8 | 9 | ## Rule details 10 | 11 | The following pattern is considered incorrect: 12 | 13 | ```js 14 | const mapDispatchToProps = (anyOtherName) => {} 15 | ``` 16 | 17 | ```js 18 | connect((state) => state, (anyOtherName) => {})(App) 19 | ``` 20 | 21 | The following patterns are considered correct: 22 | 23 | ```js 24 | const mapDispatchToProps = () => {} 25 | ``` 26 | 27 | ```js 28 | const mapDispatchToProps = (dispatch, ownProps) => {} 29 | ``` 30 | 31 | ```js 32 | const mapDispatchToProps = (dispatch, {prop1, prop2}) => {} 33 | ``` 34 | 35 | ```js 36 | const mapDispatchToProps = (dispatch) => {} 37 | ``` 38 | 39 | ```js 40 | connect((state) => state, (dispatch, ownProps, moreArgs) => {})(App) 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/rules/mapDispatchToProps-prefer-shorthand.md: -------------------------------------------------------------------------------- 1 | # Enforces that mapDispatchToProps uses a shorthand method to wrap actions in dispatch calls whenever possible. (react-redux/mapDispatchToProps-prefer-shorthand) 2 | 3 | >...`connect` supports an “object shorthand” form for the `mapDispatchToProps` argument: if you pass an object full of action creators instead of a function, `connect` will automatically call bindActionCreators for you internally. We recommend always using the “object shorthand” form of `mapDispatchToProps`, unless you have a specific reason to customize the dispatching behavior. 4 | 5 | [source](https://github.com/reduxjs/react-redux/blob/master/docs/using-react-redux/connect-dispatching-actions-with-mapDispatchToProps.md#defining-mapdispatchtoprops-as-an-object) 6 | 7 | ## Rule details 8 | 9 | The following patterns are considered incorrect: 10 | 11 | ```js 12 | const mapDispatchToProps = (dispatch) => ({ 13 | action: () => dispatch(action()) 14 | }) 15 | export default connect(null, mapDispatchToProps)(Component) 16 | ``` 17 | 18 | ```js 19 | const mapDispatchToProps = (dispatch) => ({ 20 | action: () => dispatch(action()), 21 | action1: (arg1, arg2) => dispatch(action(arg1, arg2)) 22 | }) 23 | export default connect(null, mapDispatchToProps)(Component) 24 | ``` 25 | 26 | The following patterns are considered correct: 27 | 28 | 29 | ```js 30 | export default connect(null, { action })(Component) 31 | ``` 32 | 33 | ```js 34 | const mapDispatchToProps = { action } 35 | export default connect(null, mapDispatchToProps)(Component) 36 | ``` 37 | 38 | ```js 39 | const mapDispatchToProps = (dispatch) => ({ 40 | action: () => dispatch(actionHelper(true)) 41 | }) 42 | export default connect(null, mapDispatchToProps)(Component) 43 | ``` 44 | 45 | ```js 46 | const mapDispatchToProps = (dispatch) => ({ 47 | action: () => dispatch(action()), 48 | action1: (arg1, arg2) => dispatch(action(arg1 + arg2)) 49 | }) 50 | export default connect(null, mapDispatchToProps)(Component) 51 | ``` 52 | -------------------------------------------------------------------------------- /docs/rules/mapDispatchToProps-returns-object.md: -------------------------------------------------------------------------------- 1 | # Enforces that mapDispatchToProps returns an object. (react-redux/mapDispatchToProps-returns-object) 2 | 3 | Enforces that the mapDispatchToProps is an object or a function returning an object. 4 | 5 | *Note: All of the caught cases would have caused a runtime [warning](https://github.com/reactjs/react-redux/blob/master/src/utils/verifyPlainObject.js) by react-redux * 6 | 7 | ## Rule details 8 | 9 | The following pattern is considered incorrect: 10 | 11 | ```js 12 | const mapDispatchToProps = (dispatch) => dispatch(action()) 13 | ``` 14 | 15 | ```js 16 | connect((state) => state, (dispatch) => dispatch(action()))(App) 17 | ``` 18 | 19 | ```js 20 | const mapDispatchToProps = () => {} 21 | ``` 22 | 23 | The following patterns are considered correct: 24 | 25 | 26 | ```js 27 | const mapDispatchToProps = {} 28 | ``` 29 | 30 | ```js 31 | const mapDispatchToProps = null; 32 | ``` 33 | 34 | ```js 35 | const mapDispatchToProps = {anAction: anAction} 36 | ``` 37 | 38 | ```js 39 | const mapDispatchToProps = (dispatch) => ({anAction: dispatch(anAction())}) 40 | ``` 41 | 42 | ## Not supported use cases. 43 | 44 | #### mapDispatchToProps is a function but actions are not bound to dispatch 45 | 46 | >If a function is passed, it will be given dispatch as the first parameter. It’s up to you to return an object that somehow uses dispatch to bind action creators in your own way. 47 | 48 | Below use case is likely not what you want but will not be enforced by this rule nor runtime react-redux check: 49 | 50 | ```js 51 | const mapDispatchToProps = () => ({ 52 | action 53 | }); 54 | ``` 55 | 56 | In this scenario action wouldn't be wrapped in dispatch and thus wouldn't be triggered. 57 | 58 | Most likely it needs to be rewritten as: 59 | 60 | ```js 61 | const mapDispatchToProps = { 62 | action 63 | }; 64 | ``` 65 | or 66 | 67 | ```js 68 | const mapDispatchToProps = (dispatch) => ({ 69 | action: () => dispatch(action()) 70 | }); 71 | ``` 72 | 73 | or 74 | 75 | ```js 76 | const mapDispatchToProps = (dispatch) => ({ 77 | action: () => bindActionCreators(action, dispatch) 78 | }); 79 | ``` 80 | 81 | #### mapDispatchToProps is equal to or returns a variable 82 | 83 | Note that if mapDispatchToProps is assigned a value of a variable there is no way for lint to know if the variable resolves to an object. 84 | 85 | So both of below use cases will be considered correct by the rule event though the second one is technically incorrect. 86 | 87 | This one would be caught by a react-redux check. 88 | 89 | ```js 90 | const actionsMap = { 91 | action1, 92 | action2, 93 | }; 94 | const mapDispatchToProps = actionsMap; 95 | ``` 96 | 97 | ```js 98 | const mapDispatchToProps = aSingleAction; 99 | ``` 100 | -------------------------------------------------------------------------------- /docs/rules/mapStateToProps-no-store.md: -------------------------------------------------------------------------------- 1 | # Enforces that mapStateToProps does not bind complete store to a component. (react-redux/mapStateToProps-no-store) 2 | 3 | Passing whole state to a component is a bad practice, triggering unnecessary re-renders. Additionally bad is passing around a mutable object that your component critically depends on preventing mutations to. 4 | Instead one should specify the properties actually used by a component. 5 | 6 | ## Rule details 7 | 8 | The following patterns are considered incorrect: 9 | 10 | ```js 11 | const mapStateToProps = (state) => state 12 | ``` 13 | 14 | ```js 15 | const mapStateToProps = state => { 16 | return {state: state} 17 | } 18 | ``` 19 | 20 | ```js 21 | const mapStateToProps = state => ({...state}); 22 | ``` 23 | 24 | ```js 25 | connect((state) => state, null)(App) 26 | ``` 27 | 28 | The following patterns are correct: 29 | 30 | ```js 31 | const mapStateToProps = () => {} 32 | ``` 33 | 34 | ```js 35 | const mapStateToProps = (state) => {isActive: state.isActive} 36 | ``` 37 | 38 | ```js 39 | const mapStateToProps = ({isActive}) => {isActive} 40 | ``` 41 | 42 | ```js 43 | connect((state) => ({isActive: state.isActive}), null)(App) 44 | ``` 45 | 46 | ## Not supported use cases. 47 | 48 | Please note that the following use case, although common, is not supported due to the nature of static code analysis. 49 | 50 | The following would not warn: 51 | 52 | ```js 53 | const getProps = (state) => state; 54 | const mapStateToProps = (state) => getProps(state); 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/rules/mapStateToProps-prefer-hoisted.md: -------------------------------------------------------------------------------- 1 | # Flags generation of copies of same-by-value but different-by-reference props (react-redux/mapStateToProps-prefer-hoisted) 2 | 3 | Primitives props like strings and numbers are compared by their value, while objects like arrays, dates, and plain objects are compared by their reference. 4 | 5 | In case when mapStateToProps creates a new "constant" (i.e. independent of `state` and `ownProps`) object inside of it, React will trigger a re-render of connected component even if actual prop value didn't change. 6 | 7 | 8 | ## Rule details 9 | 10 | The following patterns are considered incorrect: 11 | 12 | ```js 13 | const mapStateToProps = state => { 14 | return { 15 | foo: [1, 2, 3] // this array should be defined outside of mapStateToProps 16 | }; 17 | }; 18 | ``` 19 | 20 | 21 | ```js 22 | const mapStateToProps = state => { 23 | return { 24 | foo: { // this object should be defined outside of mapStateToProps 25 | a: 1 26 | } 27 | }; 28 | }; 29 | ``` 30 | 31 | 32 | The following patterns are correct 33 | 34 | ```js 35 | const mapStateToProps = state => { 36 | return { 37 | a: 1 38 | }; 39 | }; 40 | ``` 41 | 42 | ```js 43 | const mapStateToProps = state => { 44 | const a = state.a; 45 | return { 46 | a 47 | }; 48 | }; 49 | ``` 50 | 51 | ```js 52 | const mapStateToProps = state => ({ 53 | user: state.user, 54 | // this is still a bad design because the list prop will be considered 55 | // updated on every store change but the rule will not flag this. 56 | list: [1, 2, state.count] 57 | }); 58 | ``` 59 | 60 | 61 | ## Limitations 62 | 63 | Below case wouldn't be flagged by the rule: 64 | 65 | ```js 66 | const mapStateToProps = state => { 67 | const foo = []; 68 | return { 69 | foo 70 | }; 71 | }; 72 | ``` 73 | -------------------------------------------------------------------------------- /docs/rules/mapStateToProps-prefer-parameters-names.md: -------------------------------------------------------------------------------- 1 | # Enforces that all mapStateToProps parameters have specific names. (react-redux/mapStateToProps-prefer-parameters-names) 2 | 3 | [react-redux mapStateToProps](https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options) function has 2 optional arguments: 4 | * state 5 | * ownProps 6 | 7 | This rule enforces that all of the provided parameters should follow the above naming conventions. 8 | 9 | ## Rule details 10 | 11 | The following pattern is considered incorrect: 12 | 13 | ```js 14 | const mapStateToProps = (anyOtherName) => {} 15 | ``` 16 | 17 | ```js 18 | connect(function(anyOtherName) {}, null)(App) 19 | ``` 20 | 21 | The following patterns are considered correct: 22 | 23 | ```js 24 | const mapStateToProps = (state, ownProps) => {} 25 | ``` 26 | 27 | ```js 28 | const mapStateToProps = (state) => {} 29 | ``` 30 | 31 | ```js 32 | const mapStateToProps = ({isActive}) => {isActive} 33 | ``` 34 | 35 | ```js 36 | connect((state) => state, null)(App) 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/rules/mapStateToProps-prefer-selectors.md: -------------------------------------------------------------------------------- 1 | # Enforces that all mapStateToProps properties use selector functions. (react-redux/mapStateToProps-prefer-selectors) 2 | 3 | Using selectors in `mapStateToProps` to pull data from the store or [compute derived data](https://redux.js.org/recipes/computing-derived-data#composing-selectors) allows you to uncouple your containers from the state architecture and more easily enable memoization. This rule will ensure that every prop utilizes a selector. 4 | 5 | ## Rule details 6 | 7 | The following pattern is considered incorrect: 8 | 9 | ```js 10 | const mapStateToProps = (state) => { x: state.property } 11 | ``` 12 | 13 | ```js 14 | connect(function(state) { 15 | return { 16 | y: state.other.property 17 | } 18 | }, null)(App) 19 | ``` 20 | 21 | The following patterns are considered correct: 22 | 23 | ```js 24 | const propertySelector = (state) => state.property 25 | const mapStateToProps = (state) => { x: propertySelector(state) } 26 | ``` 27 | 28 | ```js 29 | const getOtherProperty = (state) => state.other.property 30 | connect(function(state) { 31 | return { 32 | y: getOtherProperty(state) 33 | } 34 | }, null)(App) 35 | ``` 36 | 37 | ## Rule Options 38 | 39 | ```js 40 | ... 41 | "react-redux/mapStateToProps-prefer-selectors": [, { 42 | "matching": 43 | "validateParams": 44 | }] 45 | ... 46 | ``` 47 | 48 | ### `matching` 49 | If provided, validates the name of the selector functions against the RegExp pattern provided. 50 | 51 | ```js 52 | // .eslintrc 53 | { 54 | "react-redux/mapStateToProps-prefer-selectors": ["error", { matching: "^.*Selector$"}] 55 | } 56 | 57 | // container.js 58 | const mapStateToProps = (state) => { 59 | x: xSelector(state), // success 60 | y: selectY(state), // failure 61 | } 62 | ``` 63 | 64 | ```js 65 | // .eslintrc 66 | { 67 | "react-redux/mapStateToProps-prefer-selectors": ["error", { matching: "^get.*FromState$"}] 68 | } 69 | 70 | // container.js 71 | const mapStateToProps = (state) => { 72 | x: getXFromState(state), // success 73 | y: getY(state), // failure 74 | } 75 | ``` 76 | 77 | ### `validateParams` 78 | Boolean to determine if the selectors use the correct params (`(state, ownProps)`, where both params are optional). Defaults to true. 79 | 80 | ```js 81 | // .eslintrc 82 | { 83 | "react-redux/mapStateToProps-prefer-selectors": ["error", { validateParams: true }] 84 | } 85 | 86 | // container.js 87 | const mapStateToProps = (state, ownProps) => { 88 | x: xSelector(state), // success 89 | y: ySelector(state, ownProps), // sucess 90 | z: zSelector(), // success 91 | a: aSelector(ownProps, state), // failure 92 | b: bSelector(state, someOtherValue) // failure 93 | } 94 | ``` -------------------------------------------------------------------------------- /docs/rules/no-unused-prop-types.md: -------------------------------------------------------------------------------- 1 | # Extension of a react's no-unused-prop-types rule filtering out false positive used in redux context. (react-redux/no-unused-prop-types) 2 | 3 | [react/no-unused-prop-types](https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/no-unused-prop-types.md) 4 | 5 | # Rule details 6 | 7 | This rule fixes some of the false positive reported by the react rule 8 | 9 | In below example `react/no-unused-prop-types` would report `myProp PropType is defined but prop is never used` while `react-redux/no-unused-prop-types` would correctly detect the usage of this prop within `mapStateToProps`. 10 | 11 | ```js 12 | export const mapStateToProps = (state, ownProps) => ({ 13 | myData: getMyData(state, ownProps.myProp), 14 | }); 15 | 16 | export class MyComponent extends Component { 17 | render() { 18 | return
{this.props.myData}
; 19 | } 20 | } 21 | 22 | MyComponent.propTypes = { 23 | myProp: PropTypes.string.isRequired 24 | }; 25 | 26 | export default connect(mapStateToProps)(MyComponent); 27 | ``` 28 | 29 | # Implementation details and Limitations 30 | 31 | The rule actually runs `react/no-unused-prop-types` rule and then filters out the reports of props that are used within redux's `mapStateToProps` or `mapDispatchToProps`. 32 | The rule only works within a context of a single file. So it would only work properly if compoent and container (react connect fucntion) are defined within the same file. 33 | 34 | # Configuration 35 | 36 | You'd want to disable `react/no-unused-prop-types` if you using this rule. -------------------------------------------------------------------------------- /docs/rules/prefer-separate-component-file.md: -------------------------------------------------------------------------------- 1 | # Enforces that all connected components are defined in a separate file (react-redux/prefer-separate-component-file) 2 | 3 | And imports it to the container. 4 | 5 | ## Rule details 6 | 7 | The following pattern is considered incorrect: 8 | 9 | ```js 10 | const Component = () => {}; 11 | connect(mapStateToProps, null)(Component) 12 | ``` 13 | 14 | The following patterns are considered correct: 15 | 16 | ```js 17 | import Component from './component'; 18 | connect(mapStateToProps, mapDispatchToProps)(Component) 19 | ``` 20 | 21 | ```js 22 | const Component = require('./component') 23 | connect(mapStateToProps, mapDispatchToProps)(Component) 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/rules/useSelector-prefer-selectors.md: -------------------------------------------------------------------------------- 1 | # Enforces that all useSelector hooks use named selector functions. (react-redux/useSelector-prefer-selectors) 2 | 3 | Using selectors in `useSelector` to pull data from the store or [compute derived data](https://redux.js.org/recipes/computing-derived-data#composing-selectors) allows you to decouple your containers from the state architecture and more easily enable memoization. This rule will ensure that every hook utilizes a named selector. 4 | 5 | ## Rule details 6 | 7 | The following pattern is considered incorrect: 8 | 9 | ```js 10 | const property = useSelector((state) => state.property) 11 | const property = useSelector(function (state) { return state.property }) 12 | ``` 13 | 14 | The following patterns are considered correct: 15 | 16 | ```js 17 | const selector = (state) => state.property 18 | 19 | function Component() { 20 | const property = useSelector(selector) 21 | // ... 22 | } 23 | ``` 24 | 25 | ## Rule Options 26 | 27 | ```js 28 | ... 29 | "react-redux/useSelector-prefer-selectors": [, { 30 | "matching": 31 | "validateParams": 32 | }] 33 | ... 34 | ``` 35 | 36 | ### `matching` 37 | If provided, validates the name of the selector functions against the RegExp pattern provided. 38 | 39 | ```js 40 | // .eslintrc 41 | { 42 | "react-redux/useSelector-prefer-selectors": ["error", { matching: "^.*Selector$"}] 43 | } 44 | 45 | // container.js 46 | const propertyA = useSelector(aSelector) // success 47 | const propertyB = useSelector(selectB) // failure 48 | ``` 49 | 50 | ```js 51 | // .eslintrc 52 | { 53 | "react-redux/useSelector-prefer-selectors": ["error", { matching: "^get.*FromState$"}] 54 | } 55 | 56 | // container.js 57 | const propertyA = useSelector(getAFromState) // success 58 | const propertyB = useSelector(getB) // failure 59 | ``` 60 | 61 | ### `hook` 62 | 63 | Sets the name of the `useSelector` function to target. The value can also be an array of strings. Defaults to `['useSelector', 'useAppSelector']`. 64 | 65 | ```js 66 | // .eslintrc 67 | { 68 | "react-redux/useSelector-prefer-selectors": ["error", { hook: 'useAppSelector' }] 69 | } 70 | 71 | // container.js 72 | const property = useAppSelector(state => state.mydata) // failure 73 | ``` 74 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "plugins": { 3 | "react": require("eslint-plugin-react"), 4 | "jsx-a11y": require("eslint-plugin-jsx-a11y"), 5 | "import": require("eslint-plugin-import") 6 | }, 7 | "rules": { 8 | "func-names": 0, 9 | "global-require": 0, 10 | "no-undef": "error", 11 | "prefer-destructuring": 0, 12 | "strict": 0, 13 | // Include rules from airbnb configuration directly here 14 | // Make sure to copy the rules from the airbnb configuration 15 | }, 16 | "languageOptions": { 17 | "globals": { 18 | __dirname: true, 19 | console: true, 20 | describe: true, 21 | it: true, 22 | module: true, 23 | require: true, 24 | } 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const rules = { 2 | 'connect-prefer-minimum-two-arguments': require('./lib/rules/connect-prefer-minimum-two-arguments'), 3 | 'connect-prefer-named-arguments': require('./lib/rules/connect-prefer-named-arguments'), 4 | 'mapDispatchToProps-prefer-shorthand': require('./lib/rules/mapDispatchToProps-prefer-shorthand'), 5 | 'mapDispatchToProps-returns-object': require('./lib/rules/mapDispatchToProps-returns-object'), 6 | 'mapDispatchToProps-prefer-parameters-names': require('./lib/rules/mapDispatchToProps-prefer-parameters-names'), 7 | 'mapStateToProps-no-store': require('./lib/rules/mapStateToProps-no-store'), 8 | 'mapStateToProps-prefer-hoisted': require('./lib/rules/mapStateToProps-prefer-hoisted'), 9 | 'mapStateToProps-prefer-parameters-names': require('./lib/rules/mapStateToProps-prefer-parameters-names'), 10 | 'mapStateToProps-prefer-selectors': require('./lib/rules/mapStateToProps-prefer-selectors'), 11 | 'useSelector-prefer-selectors': require('./lib/rules/useSelector-prefer-selectors'), 12 | 'no-unused-prop-types': require('./lib/rules/no-unused-prop-types'), 13 | 'prefer-separate-component-file': require('./lib/rules/prefer-separate-component-file'), 14 | }; 15 | 16 | function configureAsError() { 17 | const result = {}; 18 | Object.keys(rules).forEach((key) => { 19 | result[`react-redux/${key}`] = 2; 20 | }); 21 | return result; 22 | } 23 | 24 | const activeRulesConfig = configureAsError(); 25 | 26 | module.exports = { 27 | deprecatedRules: [], 28 | rules, 29 | configs: { 30 | recommended: { 31 | plugins: ['react-redux'], 32 | rules: { 33 | 'react-redux/connect-prefer-minimum-two-arguments': 0, 34 | 'react-redux/connect-prefer-named-arguments': 2, 35 | 'react-redux/mapDispatchToProps-prefer-parameters-names': 2, 36 | 'react-redux/mapDispatchToProps-prefer-shorthand': 2, 37 | 'react-redux/mapDispatchToProps-returns-object': 2, 38 | 'react-redux/mapStateToProps-no-store': 2, 39 | 'react-redux/mapStateToProps-prefer-hoisted': 2, 40 | 'react-redux/mapStateToProps-prefer-parameters-names': 2, 41 | 'react-redux/useSelector-prefer-selectors': 2, 42 | 'react-redux/no-unused-prop-types': 2, 43 | 'react-redux/prefer-separate-component-file': 1, 44 | }, 45 | }, 46 | all: { 47 | plugins: ['react-redux'], 48 | rules: activeRulesConfig, 49 | }, 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /lib/filterReports.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ruleComposer = require('eslint-rule-composer'); 4 | 5 | /* eslint-disable */ 6 | 7 | // ---- Start copy ---- // 8 | // from https://github.com/not-an-aardvark/eslint-rule-composer/blob/master/lib/rule-composer.js#L3-L125 9 | 10 | /** 11 | * Translates a multi-argument context.report() call into a single object argument call 12 | * @param {...*} arguments A list of arguments passed to `context.report` 13 | * @returns {MessageDescriptor} A normalized object containing report information 14 | */ 15 | function normalizeMultiArgReportCall() { 16 | // If there is one argument, it is considered to be a new-style call already. 17 | if (arguments.length === 1) { 18 | return arguments[0]; 19 | } 20 | 21 | // If the second argument is a string, the arguments are interpreted as [node, message, data, fix]. 22 | if (typeof arguments[1] === 'string') { 23 | return { 24 | node: arguments[0], 25 | message: arguments[1], 26 | data: arguments[2], 27 | fix: arguments[3], 28 | }; 29 | } 30 | 31 | // Otherwise, the arguments are interpreted as [node, loc, message, data, fix]. 32 | return { 33 | node: arguments[0], 34 | loc: arguments[1], 35 | message: arguments[2], 36 | data: arguments[3], 37 | fix: arguments[4], 38 | }; 39 | } 40 | 41 | /** 42 | * Normalizes a MessageDescriptor to always have a `loc` with `start` and `end` properties 43 | * @param {MessageDescriptor} descriptor A descriptor for the report from a rule. 44 | * @returns {{start: Location, end: (Location|null)}} An updated location that infers the `start` and `end` properties 45 | * from the `node` of the original descriptor, or infers the `start` from the `loc` of the original descriptor. 46 | */ 47 | function normalizeReportLoc(descriptor) { 48 | if (descriptor.loc) { 49 | if (descriptor.loc.start) { 50 | return descriptor.loc; 51 | } 52 | return { start: descriptor.loc, end: null }; 53 | } 54 | return descriptor.node.loc; 55 | } 56 | 57 | 58 | /** 59 | * Interpolates data placeholders in report messages 60 | * @param {MessageDescriptor} descriptor The report message descriptor. 61 | * @param {Object} messageIds Message IDs from rule metadata 62 | * @returns {{message: string, data: Object}} The interpolated message and data for the descriptor 63 | */ 64 | function normalizeMessagePlaceholders(descriptor, messageIds) { 65 | const message = typeof descriptor.messageId === 'string' ? messageIds[descriptor.messageId] : descriptor.message; 66 | if (!descriptor.data) { 67 | return { 68 | message, 69 | data: typeof descriptor.messageId === 'string' ? {} : null, 70 | }; 71 | } 72 | 73 | const normalizedData = Object.create(null); 74 | const interpolatedMessage = message.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, (fullMatch, term) => { 75 | if (term in descriptor.data) { 76 | normalizedData[term] = descriptor.data[term]; 77 | return descriptor.data[term]; 78 | } 79 | 80 | return fullMatch; 81 | }); 82 | 83 | return { 84 | message: interpolatedMessage, 85 | data: Object.freeze(normalizedData), 86 | }; 87 | } 88 | 89 | function getRuleMeta(rule) { 90 | return typeof rule === 'object' && rule.meta && typeof rule.meta === 'object' 91 | ? rule.meta 92 | : {}; 93 | } 94 | 95 | function getMessageIds(rule) { 96 | const meta = getRuleMeta(rule); 97 | return meta.messages && typeof rule.meta.messages === 'object' 98 | ? meta.messages 99 | : {}; 100 | } 101 | 102 | function getReportNormalizer(rule) { 103 | const messageIds = getMessageIds(rule); 104 | 105 | return function normalizeReport() { 106 | const descriptor = normalizeMultiArgReportCall(...arguments); 107 | const interpolatedMessageAndData = normalizeMessagePlaceholders(descriptor, messageIds); 108 | 109 | return { 110 | node: descriptor.node, 111 | message: interpolatedMessageAndData.message, 112 | messageId: typeof descriptor.messageId === 'string' ? descriptor.messageId : null, 113 | data: typeof descriptor.messageId === 'string' ? interpolatedMessageAndData.data : null, 114 | loc: normalizeReportLoc(descriptor), 115 | fix: descriptor.fix, 116 | }; 117 | }; 118 | } 119 | 120 | function getRuleCreateFunc(rule) { 121 | return typeof rule === 'function' ? rule : rule.create; 122 | } 123 | 124 | function removeMessageIfMessageIdPresent(reportDescriptor) { 125 | const newDescriptor = Object.assign({}, reportDescriptor); 126 | 127 | if (typeof reportDescriptor.messageId === 'string' && typeof reportDescriptor.message === 'string') { 128 | delete newDescriptor.message; 129 | } 130 | 131 | return newDescriptor; 132 | } 133 | 134 | // ---- End copy ---- // 135 | 136 | const filterReports = (rule, getPropNameFromReactRuleMessage, getPropNameFromReduxRuleMessage) => Object.freeze({ 137 | create(context) { 138 | const removeProps = []; 139 | return getRuleCreateFunc(rule)(Object.freeze(Object.create( 140 | context, 141 | { 142 | report: { 143 | enumerable: true, 144 | value() { 145 | const reportDescriptor = getReportNormalizer(rule)(...arguments); 146 | if (reportDescriptor.message.indexOf('exclude:') > -1) { 147 | removeProps.push(getPropNameFromReduxRuleMessage(reportDescriptor.message)); 148 | } else { 149 | const propName = getPropNameFromReactRuleMessage(reportDescriptor.message); 150 | if (removeProps.indexOf(propName) === -1) { 151 | context.report(removeMessageIfMessageIdPresent(reportDescriptor)); 152 | } 153 | } 154 | 155 | }, 156 | }, 157 | } 158 | ))); 159 | }, 160 | schema: rule.schema, 161 | meta: getRuleMeta(rule), 162 | }); 163 | 164 | module.exports = (rules, getPropNameFromReactRuleMessage, getPropNameFromReduxRuleMessage) => filterReports(ruleComposer.joinReports(rules), getPropNameFromReactRuleMessage, getPropNameFromReduxRuleMessage); 165 | -------------------------------------------------------------------------------- /lib/isReactReduxConnect.js: -------------------------------------------------------------------------------- 1 | module.exports = function (node, context) { 2 | if (node.callee.type === 'Identifier' && node.callee.name === 'connect') { 3 | const sourceCode = context.getSourceCode(); 4 | const scope = sourceCode.getScope(node); 5 | const variable = scope.variables.find(v => v.name === 'connect'); 6 | if (variable && variable.defs.length > 0) { 7 | const def = variable.defs[0]; 8 | if ( 9 | (def.node.type === 'ImportSpecifier' && def.parent.source.value === 'react-redux') || 10 | (def.node.type === 'VariableDeclarator' && def.node.init && def.node.init.callee && def.node.init.callee.name === 'require' && def.node.init.arguments[0].value === 'react-redux') 11 | ) { 12 | return true; 13 | } 14 | } 15 | } 16 | return false; 17 | }; 18 | -------------------------------------------------------------------------------- /lib/rules/connect-prefer-minimum-two-arguments.js: -------------------------------------------------------------------------------- 1 | const isReactReduxConnect = require('../isReactReduxConnect'); 2 | 3 | const create = function (context) { 4 | const report = function (node) { 5 | context.report({ 6 | message: 'Connect function should have at least 2 arguments.', 7 | node, 8 | }); 9 | }; 10 | 11 | return { 12 | CallExpression(node) { 13 | if (isReactReduxConnect(node, context)) { 14 | if (node.arguments.length < 2) { 15 | report(node); 16 | } 17 | } 18 | }, 19 | }; 20 | }; 21 | 22 | module.exports = { 23 | create, 24 | }; 25 | -------------------------------------------------------------------------------- /lib/rules/connect-prefer-named-arguments.js: -------------------------------------------------------------------------------- 1 | const isReactReduxConnect = require('../isReactReduxConnect'); 2 | 3 | const argumentNames = [ 4 | 'mapStateToProps', 5 | 'mapDispatchToProps', 6 | 'mergeProps', 7 | 'options', 8 | ]; 9 | 10 | const create = function (context) { 11 | const report = function (node, i) { 12 | context.report({ 13 | message: `Connect function argument #${i + 1} should be named ${argumentNames[i]}`, 14 | node, 15 | }); 16 | }; 17 | 18 | return { 19 | CallExpression(node) { 20 | if (isReactReduxConnect(node, context)) { 21 | node.arguments.forEach((argument, i) => { 22 | if (argument.raw && argument.raw !== 'null') { 23 | report(node, i); 24 | } else if ( 25 | !argument.raw 26 | && argumentNames[i] 27 | && (!argument.name || argument.name !== argumentNames[i])) { 28 | report(node, i); 29 | } 30 | }); 31 | } 32 | }, 33 | }; 34 | }; 35 | 36 | module.exports = { 37 | create, 38 | }; 39 | -------------------------------------------------------------------------------- /lib/rules/mapDispatchToProps-prefer-parameters-names.js: -------------------------------------------------------------------------------- 1 | const isReactReduxConnect = require('../isReactReduxConnect'); 2 | 3 | const argumentNames = [ 4 | 'dispatch', 5 | 'ownProps', 6 | ]; 7 | 8 | const report = function (context, node, i) { 9 | context.report({ 10 | message: `mapDispatchToProps function parameter #${i} should be named ${argumentNames[i]}`, 11 | node, 12 | }); 13 | }; 14 | 15 | const check = function (context, params) { 16 | params.forEach((param, i) => { 17 | if (argumentNames[i] && param.type !== 'ObjectPattern' && argumentNames[i] !== param.name) { 18 | report(context, param, i); 19 | } 20 | }); 21 | }; 22 | 23 | const create = function (context) { 24 | return { 25 | VariableDeclaration(node) { 26 | node.declarations.forEach((decl) => { 27 | if (decl.id && decl.id.name === 'mapDispatchToProps') { 28 | if (decl.init && ( 29 | decl.init.type === 'ArrowFunctionExpression' || 30 | decl.init.type === 'FunctionExpression' 31 | )) { 32 | check(context, decl.init.params); 33 | } 34 | } 35 | }); 36 | }, 37 | FunctionDeclaration(node) { 38 | if (node.id && node.id.name === 'mapDispatchToProps') { 39 | check(context, node.params); 40 | } 41 | }, 42 | CallExpression(node) { 43 | if (isReactReduxConnect(node, context)) { 44 | const mapDispatchToProps = node.arguments && node.arguments[1]; 45 | if (mapDispatchToProps && ( 46 | mapDispatchToProps.type === 'ArrowFunctionExpression' || 47 | mapDispatchToProps.type === 'FunctionExpression') 48 | ) { 49 | check(context, mapDispatchToProps.params); 50 | } 51 | } 52 | }, 53 | }; 54 | }; 55 | 56 | module.exports = { 57 | create, 58 | }; 59 | -------------------------------------------------------------------------------- /lib/rules/mapDispatchToProps-prefer-shorthand.js: -------------------------------------------------------------------------------- 1 | const isReactReduxConnect = require('../isReactReduxConnect'); 2 | const utils = require('../utils'); 3 | 4 | const report = function (context, node) { 5 | context.report({ 6 | message: 'mapDispatchToProps should use a shorthand dispatch wrapping instead', 7 | node, 8 | }); 9 | }; 10 | 11 | const getParamsString = (params, context) => { 12 | const sourceCode = context.sourceCode ?? context.getSourceCode(); 13 | return params.map(param => sourceCode.getText(param)).join(',') 14 | } 15 | 16 | 17 | const propertyCanUseShortHandButDoesnt = (context, prop, dispatchName) => { 18 | const propName = prop.key && prop.key.name; 19 | const sourceCodeImpl = context.sourceCode ?? context.getSourceCode(); 20 | const sourceCode = sourceCodeImpl.getText(prop.value).replace(/(\r\n|\n|\r|\t| |;)/gm, ''); 21 | if (prop.value && prop.value.type === 'ArrowFunctionExpression') { 22 | const fncDef = prop.value; 23 | const paramString = getParamsString(fncDef.params, context); 24 | const actionNode = prop.value.body && prop.value.body.arguments && prop.value.body.arguments[0]; 25 | const nameFromSourceCode = actionNode && actionNode.callee && actionNode.callee.name; 26 | if (sourceCode === `(${paramString})=>${dispatchName}(${nameFromSourceCode}(${paramString}))`) { 27 | return true; 28 | } 29 | } else if (prop.value && prop.value.type === 'FunctionExpression') { 30 | const fncDef = prop.value; 31 | const paramString = getParamsString(fncDef.params, context); 32 | if (sourceCode === `function(${paramString}){return${dispatchName}(${propName}(${paramString}))}` 33 | ) { 34 | return true; 35 | } 36 | } 37 | return false; 38 | }; 39 | 40 | const checkReturnNode = function (context, returnNode, dispatchName) { 41 | if (returnNode.properties.every(prop => 42 | propertyCanUseShortHandButDoesnt(context, prop, dispatchName)) 43 | ) { 44 | report(context, returnNode); 45 | } 46 | }; 47 | 48 | const create = function (context) { 49 | return { 50 | VariableDeclaration(node) { 51 | node.declarations.forEach((decl) => { 52 | if (decl.id && decl.id.name === 'mapDispatchToProps') { 53 | if (decl.init && ( 54 | decl.init.type === 'ArrowFunctionExpression' || 55 | decl.init.type === 'FunctionExpression' 56 | )) { 57 | const returnNode = utils.getReturnNode(decl.init); 58 | if (returnNode && returnNode.type === 'ObjectExpression') { 59 | checkReturnNode(context, returnNode, 'dispatch'); 60 | } 61 | } 62 | } 63 | }); 64 | }, 65 | FunctionDeclaration(node) { 66 | if (node.id && node.id.name === 'mapDispatchToProps') { 67 | const returnNode = utils.getReturnNode(node.body); 68 | if (returnNode && returnNode.type === 'ObjectExpression') { 69 | checkReturnNode(context, returnNode, 'dispatch'); 70 | } 71 | } 72 | }, 73 | CallExpression(node) { 74 | if (isReactReduxConnect(node, context)) { 75 | const mapDispatchToProps = node.arguments && node.arguments[1]; 76 | if (mapDispatchToProps && ( 77 | mapDispatchToProps.type === 'ArrowFunctionExpression' || 78 | mapDispatchToProps.type === 'FunctionExpression') 79 | ) { 80 | const returnNode = utils.getReturnNode(mapDispatchToProps); 81 | if (returnNode && returnNode.type === 'ObjectExpression') { 82 | checkReturnNode(context, returnNode, 'dispatch'); 83 | } 84 | } 85 | } 86 | }, 87 | }; 88 | }; 89 | 90 | module.exports = { 91 | create, 92 | }; 93 | -------------------------------------------------------------------------------- /lib/rules/mapDispatchToProps-returns-object.js: -------------------------------------------------------------------------------- 1 | const isReactReduxConnect = require('../isReactReduxConnect'); 2 | const utils = require('../utils'); 3 | 4 | const report = function (context, node) { 5 | context.report({ 6 | message: 'mapDispatchToProps should return object', 7 | node, 8 | }); 9 | }; 10 | 11 | const create = function (context) { 12 | return { 13 | VariableDeclaration(node) { 14 | node.declarations.forEach((decl) => { 15 | if (decl.id && decl.id.name === 'mapDispatchToProps') { 16 | if (decl.init && ( 17 | decl.init.type === 'ArrowFunctionExpression' || 18 | decl.init.type === 'FunctionExpression' 19 | )) { 20 | const returnNode = utils.getReturnNode(decl.init); 21 | if (!utils.isObject(returnNode)) { 22 | report(context, node); 23 | } 24 | } 25 | } 26 | }); 27 | }, 28 | FunctionDeclaration(node) { 29 | if (node.id && node.id.name === 'mapDispatchToProps') { 30 | const returnNode = utils.getReturnNode(node.body); 31 | if (!utils.isObject(returnNode)) { 32 | report(context, node); 33 | } 34 | } 35 | }, 36 | CallExpression(node) { 37 | if (isReactReduxConnect(node, context)) { 38 | const mapDispatchToProps = node.arguments && node.arguments[1]; 39 | if (mapDispatchToProps && ( 40 | mapDispatchToProps.type === 'ArrowFunctionExpression' || 41 | mapDispatchToProps.type === 'FunctionExpression') 42 | ) { 43 | const returnNode = utils.getReturnNode(mapDispatchToProps); 44 | if (!utils.isObject(returnNode)) { 45 | report(context, node); 46 | } 47 | } 48 | } 49 | }, 50 | }; 51 | }; 52 | 53 | module.exports = { 54 | create, 55 | }; 56 | -------------------------------------------------------------------------------- /lib/rules/mapStateToProps-no-store.js: -------------------------------------------------------------------------------- 1 | const utils = require('../utils'); 2 | const isReactReduxConnect = require('../isReactReduxConnect'); 3 | 4 | const report = function (context, node) { 5 | context.report({ 6 | message: 'mapStateToProps should not return complete store object', 7 | node, 8 | }); 9 | }; 10 | 11 | // first param name or false for destructuring assignment; 12 | const getFirstParamName = (node) => { 13 | const firstParam = node.params && node.params[0]; 14 | return firstParam && firstParam.type === 'Identifier' && firstParam.name; 15 | }; 16 | 17 | const propertyIsStore = (prop, storeName) => { 18 | if (prop.type === 'Property' && prop.value && prop.value.name === storeName) { 19 | // state 20 | return true; 21 | } else if ( 22 | // ...state 23 | prop.type === 'SpreadElement' 24 | && prop.argument && prop.argument.type === 'Identifier' 25 | && prop.argument.name === storeName 26 | ) { 27 | return true; 28 | } 29 | return false; 30 | }; 31 | 32 | const checkFunction = function (context, body, firstParamName) { 33 | const returnNode = utils.getReturnNode(body); 34 | // return state; 35 | if (returnNode && returnNode.type === 'Identifier' && returnNode.name === firstParamName) { 36 | report(context, body); 37 | } 38 | // return {store: state}; 39 | if (returnNode && returnNode.type === 'ObjectExpression' && 40 | returnNode.properties.reduce((acc, cv) => 41 | (acc || propertyIsStore(cv, firstParamName)), false) 42 | ) { 43 | report(context, body); 44 | } 45 | }; 46 | 47 | const create = function (context) { 48 | return { 49 | VariableDeclaration(node) { 50 | node.declarations.forEach((decl) => { 51 | if (decl.id && decl.id.name === 'mapStateToProps') { 52 | const body = decl.init.body; 53 | const firstParamName = getFirstParamName(decl.init); 54 | if (firstParamName) { 55 | checkFunction(context, body, firstParamName); 56 | } 57 | } 58 | }); 59 | }, 60 | FunctionDeclaration(node) { 61 | if (node.id && node.id.name === 'mapStateToProps') { 62 | const firstParamName = getFirstParamName(node); 63 | if (firstParamName) { 64 | checkFunction(context, node.body, firstParamName); 65 | } 66 | } 67 | }, 68 | CallExpression(node) { 69 | if (isReactReduxConnect(node, context)) { 70 | const mapStateToProps = node.arguments && node.arguments[0]; 71 | if (mapStateToProps && mapStateToProps.body) { 72 | const firstParamName = getFirstParamName(mapStateToProps); 73 | if (firstParamName) { 74 | checkFunction(context, mapStateToProps.body, firstParamName); 75 | } 76 | } 77 | } 78 | }, 79 | }; 80 | }; 81 | 82 | module.exports = { 83 | create, 84 | }; 85 | -------------------------------------------------------------------------------- /lib/rules/mapStateToProps-prefer-hoisted.js: -------------------------------------------------------------------------------- 1 | const utils = require('../utils'); 2 | const isReactReduxConnect = require('../isReactReduxConnect'); 3 | 4 | const report = function (context, node) { 5 | context.report({ 6 | message: 'constant arrays and objects should be initialized outside of mapStateToProps', 7 | node, 8 | }); 9 | }; 10 | 11 | const isConstArrayOrObj = (node, nested) => { 12 | if (node && node.type === 'ObjectExpression') { 13 | return node.properties.reduce((acc, prop) => 14 | (acc && isConstArrayOrObj(prop.value, (nested + 1))), true); 15 | } 16 | if (node && node.type === 'ArrayExpression') { 17 | return node.elements.reduce((acc, el) => 18 | (acc && isConstArrayOrObj(el, (nested + 1))), true); 19 | } 20 | if (node && node.type === 'Literal' && nested > 0) { 21 | return true; 22 | } 23 | return false; 24 | }; 25 | 26 | const checkProp = (node, context) => { 27 | if (isConstArrayOrObj(node, 0)) { 28 | report(context, node); 29 | } 30 | }; 31 | 32 | const checkFunction = function (context, body) { 33 | const returnNode = utils.getReturnNode(body); 34 | if (returnNode && returnNode.type === 'ObjectExpression') { 35 | returnNode.properties.forEach(prop => checkProp(prop.value, context)); 36 | } 37 | }; 38 | 39 | const create = function (context) { 40 | return { 41 | VariableDeclaration(node) { 42 | node.declarations.forEach((decl) => { 43 | if (decl.id && decl.id.name === 'mapStateToProps') { 44 | const body = decl.init.body; 45 | checkFunction(context, body); 46 | } 47 | }); 48 | }, 49 | FunctionDeclaration(node) { 50 | if (node.id && node.id.name === 'mapStateToProps') { 51 | checkFunction(context, node.body); 52 | } 53 | }, 54 | CallExpression(node) { 55 | if (isReactReduxConnect(node, context)) { 56 | const mapStateToProps = node.arguments && node.arguments[0]; 57 | if (mapStateToProps && mapStateToProps.body) { 58 | checkFunction(context, mapStateToProps.body); 59 | } 60 | } 61 | }, 62 | }; 63 | }; 64 | 65 | module.exports = { 66 | create, 67 | }; 68 | -------------------------------------------------------------------------------- /lib/rules/mapStateToProps-prefer-parameters-names.js: -------------------------------------------------------------------------------- 1 | const isReactReduxConnect = require('../isReactReduxConnect'); 2 | 3 | const argumentNames = [ 4 | 'state', 5 | 'ownProps', 6 | ]; 7 | 8 | const report = function (context, node, i) { 9 | context.report({ 10 | message: `mapStateToProps function parameter #${i} should be named ${argumentNames[i]}`, 11 | node, 12 | }); 13 | }; 14 | 15 | const check = function (context, params) { 16 | params.forEach((param, i) => { 17 | if (argumentNames[i] && param.type !== 'ObjectPattern' && argumentNames[i] !== param.name) { 18 | report(context, param, i); 19 | } 20 | }); 21 | }; 22 | 23 | const create = function (context) { 24 | return { 25 | VariableDeclaration(node) { 26 | node.declarations.forEach((decl) => { 27 | if (decl.id && decl.id.name === 'mapStateToProps') { 28 | if (decl.init && ( 29 | decl.init.type === 'ArrowFunctionExpression' || 30 | decl.init.type === 'FunctionExpression' 31 | )) { 32 | check(context, decl.init.params); 33 | } 34 | } 35 | }); 36 | }, 37 | FunctionDeclaration(node) { 38 | if (node.id && node.id.name === 'mapStateToProps') { 39 | check(context, node.params); 40 | } 41 | }, 42 | CallExpression(node) { 43 | if (isReactReduxConnect(node, context)) { 44 | const mapStateToProps = node.arguments && node.arguments[0]; 45 | if (mapStateToProps && ( 46 | mapStateToProps.type === 'ArrowFunctionExpression' || 47 | mapStateToProps.type === 'FunctionExpression') 48 | ) { 49 | check(context, mapStateToProps.params); 50 | } 51 | } 52 | }, 53 | }; 54 | }; 55 | 56 | module.exports = { 57 | create, 58 | }; 59 | -------------------------------------------------------------------------------- /lib/rules/mapStateToProps-prefer-selectors.js: -------------------------------------------------------------------------------- 1 | const isReactReduxConnect = require('../isReactReduxConnect'); 2 | const utils = require('../utils'); 3 | 4 | const reportNoSelector = function (context, node, name) { 5 | context.report({ 6 | message: `mapStateToProps property "${name}" should use a selector function.`, 7 | node, 8 | }); 9 | }; 10 | 11 | const reportWrongName = function (context, node, propName, functionName, matching) { 12 | context.report({ 13 | message: `mapStateToProps "${propName}"'s selector "${functionName}" does not match "${matching}".`, 14 | node, 15 | }); 16 | }; 17 | 18 | const reportUnexpectedParam = function (context, node, propName, functionName, index) { 19 | context.report({ 20 | message: `mapStateToProps "${propName}"'s selector "${functionName}" parameter #${index} is not expected.`, 21 | node, 22 | }); 23 | }; 24 | 25 | const reportInvalidParams = function (context, node, propName, functionName, params, index) { 26 | context.report({ 27 | message: `mapStateToProps "${propName}"'s selector "${functionName}" parameter #${index} should be "${params[index].name}".`, 28 | node, 29 | }); 30 | }; 31 | 32 | const checkProperties = function (context, properties, matching, expectedParams) { 33 | properties.forEach((prop) => { 34 | if (prop.value.type !== 'CallExpression') { 35 | reportNoSelector(context, prop, prop.key.name); 36 | return; 37 | } 38 | if (matching && !prop.value.callee.name.match(new RegExp(matching))) { 39 | reportWrongName(context, prop, prop.key.name, prop.value.callee.name, matching); 40 | } 41 | if (expectedParams) { 42 | const actualParams = prop.value.arguments; 43 | const propName = prop.key.name; 44 | const functionName = prop.value.callee.name; 45 | actualParams.forEach((param, i) => { 46 | if (!expectedParams[i]) { 47 | reportUnexpectedParam(context, prop, propName, functionName, i); 48 | return; 49 | } 50 | if (param.name !== expectedParams[i].name) { 51 | reportInvalidParams(context, prop, propName, functionName, expectedParams, i); 52 | } 53 | }); 54 | } 55 | }); 56 | }; 57 | 58 | const check = function (context, node, matching, validateParams) { 59 | const returnNode = utils.getReturnNode(node); 60 | if (utils.isObject(returnNode)) { 61 | checkProperties(context, returnNode.properties, matching, validateParams && node.params); 62 | } 63 | }; 64 | 65 | const create = function (context) { 66 | const config = context.options[0] || {}; 67 | return { 68 | VariableDeclaration(node) { 69 | node.declarations.forEach((decl) => { 70 | if (decl.id && decl.id.name === 'mapStateToProps') { 71 | if (decl.init && ( 72 | decl.init.type === 'ArrowFunctionExpression' || 73 | decl.init.type === 'FunctionExpression' 74 | )) { 75 | check(context, decl.init, config.matching, !(config.validateParams === false)); 76 | } 77 | } 78 | }); 79 | }, 80 | FunctionDeclaration(node) { 81 | if (node.id && node.id.name === 'mapStateToProps') { 82 | check(context, node.body, config.matching, !(config.validateParams === false)); 83 | } 84 | }, 85 | CallExpression(node) { 86 | if (isReactReduxConnect(node, context)) { 87 | const mapStateToProps = node.arguments && node.arguments[0]; 88 | if (mapStateToProps && ( 89 | mapStateToProps.type === 'ArrowFunctionExpression' || 90 | mapStateToProps.type === 'FunctionExpression') 91 | ) { 92 | check(context, mapStateToProps, config.matching, !(config.validateParams === false)); 93 | } 94 | } 95 | }, 96 | }; 97 | }; 98 | 99 | module.exports = { 100 | create, 101 | meta: { 102 | schema: { 103 | matching: { 104 | type: 'string' 105 | } 106 | } 107 | }, 108 | }; 109 | -------------------------------------------------------------------------------- /lib/rules/no-unused-prop-types.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const filterReports = require('../filterReports'); 4 | const isReactReduxConnect = require('../isReactReduxConnect'); 5 | 6 | const noUnusedPropTypesReact = require('eslint-plugin-react').rules['no-unused-prop-types']; 7 | 8 | const belongsToReduxReact = (node, objectName, destrArg, context) => { 9 | const checkProp = (secondArgument) => { 10 | const secondArgumentName = secondArgument && secondArgument.type === 'Identifier' && secondArgument.name; 11 | return (secondArgumentName === objectName // ownProps.myProp 12 | || destrArg === secondArgument // {myProp} in fn argument 13 | || (destrArg && destrArg.parent.type === 'VariableDeclarator' && destrArg.parent.init && destrArg.parent.init.name === secondArgumentName) // const {myProp} = ownProps; 14 | ); 15 | }; 16 | let isReactRedux = false; 17 | if (node.type === 'VariableDeclaration') { 18 | node.declarations.forEach((decl) => { 19 | const name = decl.id && decl.id.name; 20 | if (name === 'mapStateToProps' || name === 'mapDispatchToProps') { 21 | const secondArgument = decl.init.params && decl.init.params[1]; 22 | if (checkProp(secondArgument)) { 23 | isReactRedux = true; 24 | } 25 | } 26 | }); 27 | } else if (node.type === 'FunctionDeclaration') { 28 | const name = node.id && node.id.name; 29 | if (name === 'mapStateToProps' || name === 'mapDispatchToProps') { 30 | const secondArgument = node.params && node.params[1]; 31 | if (checkProp(secondArgument)) { 32 | isReactRedux = true; 33 | } 34 | } 35 | } else if (node.type === 'CallExpression') { 36 | if (isReactReduxConnect(node, context)) { 37 | const check = (mapToProps) => { 38 | if (mapToProps && mapToProps.body) { 39 | const secondArgument = mapToProps.params && mapToProps.params[1]; 40 | if (checkProp(secondArgument)) { 41 | isReactRedux = true; 42 | } 43 | } 44 | }; 45 | const mapStateToProps = node.arguments && node.arguments[0]; 46 | const mapDispatchToProps = node.arguments && node.arguments[1]; 47 | if (mapStateToProps) check(mapStateToProps); 48 | if (mapDispatchToProps) check(mapDispatchToProps); 49 | } 50 | } 51 | return isReactRedux; 52 | }; 53 | 54 | 55 | const propsUsedInRedux = function (context) { 56 | return { 57 | MemberExpression(node) { 58 | const nodeName = node.object.name; 59 | const usedInReactRedux = context.getSourceCode().getAncestors(node) 60 | .some(ancestor => belongsToReduxReact(ancestor, nodeName, null, context)); 61 | if (usedInReactRedux) { 62 | context.report(node, `exclude:${node.property.name}`); 63 | } 64 | }, 65 | ObjectPattern(node) { 66 | const usedInReactRedux = context.getSourceCode().getAncestors(node) 67 | .some(ancestor => belongsToReduxReact(ancestor, null, node, context)); 68 | if (usedInReactRedux) { 69 | node.properties.forEach((prop) => { 70 | if (prop.type === 'Property' && prop.key && prop.key.name) { 71 | return context.report(node, `exclude:${prop.key.name}`); 72 | } else if (prop.type === 'ExperimentalRestProperty' && prop.argument && prop.argument.name) { 73 | return context.report(node, `exclude:${prop.argument.name}`); 74 | } 75 | return undefined; 76 | }); 77 | } 78 | }, 79 | }; 80 | }; 81 | 82 | const getPropNameFromReactRuleMessage = message => message.replace(' PropType is defined but prop is never used', '').replace("'", '').replace("'", ''); 83 | const getPropNameFromReduxRuleMessage = message => message.replace('exclude:', ''); 84 | 85 | module.exports = filterReports([ 86 | propsUsedInRedux, 87 | noUnusedPropTypesReact, 88 | ], getPropNameFromReactRuleMessage, getPropNameFromReduxRuleMessage); 89 | -------------------------------------------------------------------------------- /lib/rules/prefer-separate-component-file.js: -------------------------------------------------------------------------------- 1 | const isReactReduxConnect = require('../isReactReduxConnect'); 2 | 3 | const report = function (context, node) { 4 | context.report({ 5 | message: 'Connected component should be defined in a separate file.', 6 | node, 7 | }); 8 | }; 9 | 10 | module.exports = { 11 | create(context) { 12 | const sourceCode = context.sourceCode ?? context.getSourceCode(); 13 | return { 14 | CallExpression(node) { 15 | if (isReactReduxConnect(node, context)) { 16 | const component = 17 | node.parent && 18 | node.parent.arguments && 19 | node.parent.arguments[0]; 20 | if (component) { 21 | const vars = sourceCode.getScope(component).variables; 22 | vars.forEach((definedVar) => { 23 | if (component.name === definedVar.name) { 24 | definedVar.defs.forEach((def) => { 25 | if (!(def.type === 'ImportBinding' || sourceCode.getText(def.node).includes('require'))) { 26 | report(context, component); 27 | } 28 | }); 29 | } 30 | }); 31 | } 32 | } 33 | }, 34 | }; 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /lib/rules/useSelector-prefer-selectors.js: -------------------------------------------------------------------------------- 1 | function isUseSelector(node, hookNames) { 2 | return hookNames.includes(node.callee.name); 3 | } 4 | 5 | function reportWrongName(context, node, functionName, matching) { 6 | context.report({ 7 | message: `${node.callee.name} selector "${functionName}" does not match "${matching}".`, 8 | node, 9 | }); 10 | } 11 | 12 | function reportNoSelector(context, node) { 13 | context.report({ 14 | message: `${node.callee.name} should use a named selector function.`, 15 | node, 16 | }); 17 | } 18 | 19 | module.exports = { 20 | meta: { 21 | schema: { 22 | matching: { 23 | type: 'string' 24 | } 25 | } 26 | }, 27 | create(context) { 28 | const config = context.options[0] || {}; 29 | let hookNames = ['useSelector', 'useAppSelector']; 30 | 31 | // Ensure hookNames is an array 32 | if (config.hook) { 33 | hookNames = Array.isArray(config.hook) ? config.hook : [config.hook]; 34 | } 35 | 36 | return { 37 | CallExpression(node) { 38 | if (!isUseSelector(node, hookNames)) return; 39 | const selector = node.arguments && node.arguments[0]; 40 | if (selector && ( 41 | selector.type === 'ArrowFunctionExpression' || 42 | selector.type === 'FunctionExpression') 43 | ) { 44 | reportNoSelector(context, node); 45 | } else if ( 46 | selector && selector.type === 'Identifier' && 47 | config.matching && 48 | !selector.name.match(new RegExp(config.matching)) 49 | ) { 50 | reportWrongName(context, node, selector.name, config.matching); 51 | } 52 | }, 53 | }; 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const isObject = node => node && ( 4 | node.type === 'ObjectExpression' || node.type === 'Identifier' 5 | ); 6 | 7 | const getReturnNode = (node) => { 8 | const body = node && node.body; 9 | if (!body) { 10 | return node; 11 | } else if (isObject(body)) { 12 | return body; 13 | } else if (body.type === 'BlockStatement') { 14 | return getReturnNode(body); 15 | } 16 | for (let i = body.length - 1; i >= 0; i -= 1) { 17 | if (body[i].type === 'ReturnStatement') { 18 | const arg = body[i].argument; 19 | if (arg && (arg.type === 'ArrowFunctionExpression' || arg.type === 'FunctionExpression')) { 20 | return getReturnNode(arg); 21 | } 22 | return arg; 23 | } 24 | } 25 | return null; 26 | }; 27 | 28 | module.exports = { 29 | getReturnNode, 30 | isObject, 31 | }; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-react-redux", 3 | "version": "0.0.0-development", 4 | "description": "Enforcing best practices for react-redux", 5 | "keywords": [ 6 | "eslint", 7 | "eslintplugin", 8 | "eslint-plugin", 9 | "react-redux" 10 | ], 11 | "author": "diana.suvorova@gmail.com", 12 | "main": "index.js", 13 | "scripts": { 14 | "lint": "eslint ./lib ./tests", 15 | "test": "npm run lint && mocha tests --recursive", 16 | "semantic-release": "semantic-release", 17 | "commitmsg": "npm run test && commitlint -e $GIT_PARAMS" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/DianaSuvorova/eslint-plugin-react-redux" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.17.0", 25 | "@commitlint/cli": "^6.0.2", 26 | "@commitlint/config-conventional": "^6.0.2", 27 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 28 | "babel-register": "^6.26.0", 29 | "eslint": "^8 || ^9", 30 | "eslint-config-standard": "^11.0.0-beta.0", 31 | "eslint-plugin-import": "^2.25.4", 32 | "eslint-plugin-jsx-a11y": "^6.5.1", 33 | "eslint-plugin-node": "^5.2.1", 34 | "eslint-plugin-promise": "^3.6.0", 35 | "eslint-plugin-standard": "^3.0.1", 36 | "husky": "^7.0.4", 37 | "mocha": "^9.2.0", 38 | "semantic-release": "^17.2.3" 39 | }, 40 | "peerDependencies": { 41 | "eslint-plugin-react": "^7.35.0", 42 | "eslint": "^7 || ^8 || ^9.7" 43 | }, 44 | "engines": { 45 | "node": ">=18.0.0" 46 | }, 47 | "license": "ISC", 48 | "directories": { 49 | "test": "tests" 50 | }, 51 | "dependencies": { 52 | "eslint-plugin-react": "^7.35.0", 53 | "eslint-rule-composer": "^0.3.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/code-sanity-samples.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | 'expect(() => useSelector()).toThrow();', 3 | `const rows = []; 4 | function mapStateToProps(state, ownProps) { 5 | for (const { value } of rows) { 6 | } 7 | } 8 | `, 9 | `const mapStateToProps = () => { 10 | return; 11 | };`, 12 | ]; 13 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | const plugin = require('..'); 2 | 3 | const assert = require('assert'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | 7 | const ruleFiles = fs.readdirSync(path.resolve(__dirname, '../lib/rules/')) 8 | .map(f => path.basename(f, '.js')); 9 | 10 | describe('all rule files should be exported by the plugin', () => { 11 | ruleFiles.forEach((ruleName) => { 12 | it(`should export ${ruleName}`, () => { 13 | if (ruleName !== 'no-unused-prop-types') { 14 | assert.equal( 15 | plugin.rules[ruleName], 16 | // eslint-disable-next-line 17 | require(path.join('../lib/rules', ruleName)) 18 | ); 19 | } 20 | }); 21 | }); 22 | }); 23 | 24 | describe('configurations', () => { 25 | it('should export a \'recommended\' configuration', () => { 26 | assert(plugin.configs.recommended); 27 | assert(plugin.configs.recommended.plugins.includes('react-redux')); 28 | Object.keys(plugin.configs.recommended.rules).forEach((configName) => { 29 | assert.equal(configName.indexOf('react-redux/'), 0); 30 | const ruleName = configName.substring('react-redux/'.length); 31 | assert(plugin.rules[ruleName]); 32 | }); 33 | }); 34 | it('should export a \'all\' configuration', () => { 35 | assert(plugin.configs.all); 36 | assert(plugin.configs.all.plugins.includes('react-redux')); 37 | Object.keys(plugin.configs.all.rules).forEach((configName) => { 38 | assert.equal(configName.indexOf('react-redux/'), 0); 39 | assert.equal(plugin.configs.all.rules[configName], 2); 40 | }); 41 | ruleFiles.forEach((ruleName) => { 42 | if (ruleName !== 'no-unused-prop-types') { 43 | const inDeprecatedRules = Boolean(plugin.deprecatedRules[ruleName]); 44 | const inAllConfig = Boolean(plugin.configs.all.rules[`react-redux/${ruleName}`]); 45 | assert(inDeprecatedRules || inAllConfig); 46 | } 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/lib/rules/connect-prefer-minimum-two-arguments.js: -------------------------------------------------------------------------------- 1 | const rule = require('../../../lib/rules/connect-prefer-minimum-two-arguments'); 2 | const RuleTester = require('eslint').RuleTester; 3 | const codeSamples = require('../../code-sanity-samples'); 4 | const formatOptions = require('../../util'); 5 | 6 | const parserOptions = formatOptions({ 7 | ecmaVersion: 2018, 8 | sourceType: 'module', 9 | }); 10 | 11 | const ruleTester = new RuleTester( parserOptions ); 12 | 13 | ruleTester.run('connect-prefer-minimum-two-arguments', rule, { 14 | valid: [ 15 | ...codeSamples, 16 | `import { connect } from 'react-redux'; connect(mapStateToProps, mapDispatchToProps, mergeProps, options)(Component)`, 17 | `import { connect } from 'react-redux'; connect(mapStateToProps, mapDispatchToProps)(Component)`, 18 | `import { connect } from 'react-redux'; connect({prop1, prop2}, {action1, action2})(Component)`, 19 | ], 20 | invalid: [{ 21 | code: `import { connect } from 'react-redux'; connect(mapStateToProps)(Component)`, 22 | errors: [ 23 | { 24 | message: 'Connect function should have at least 2 arguments.', 25 | }, 26 | ], 27 | }], 28 | }); 29 | -------------------------------------------------------------------------------- /tests/lib/rules/connect-prefer-named-arguments.js: -------------------------------------------------------------------------------- 1 | const rule = require('../../../lib/rules/connect-prefer-named-arguments'); 2 | const RuleTester = require('eslint').RuleTester; 3 | const codeSamples = require('../../code-sanity-samples'); 4 | const formatOptions = require('../../util'); 5 | 6 | const parserOptions = formatOptions({ 7 | ecmaVersion: 2018, 8 | sourceType: 'module', 9 | }); 10 | 11 | const ruleTester = new RuleTester( parserOptions ); 12 | 13 | ruleTester.run('connect-prefer-named-arguments', rule, { 14 | valid: [ 15 | ...codeSamples, 16 | `import { connect } from 'react-redux'; export default connect(null, mapDispatchToProps)(TodoApp)`, 17 | `import { connect } from 'react-redux'; connect(mapStateToProps, mapDispatchToProps, mergeProps, options)(Component)`, 18 | `import { connect } from 'react-redux'; connect(mapStateToProps, mapDispatchToProps)(Component)`, 19 | `import { connect } from 'react-redux'; connect()(TodoApp)`, 20 | 'connect(() => {}, () => {}, mergeProps, options)(Component)', 21 | 'connect({}, {})(Component)', 22 | 'connect(state => state)(TodoApp)', 23 | ], 24 | invalid: [{ 25 | code: `import { connect } from 'react-redux'; connect(() => {}, () => {}, mergeProps, options)(Component)`, 26 | errors: [ 27 | { 28 | message: 'Connect function argument #1 should be named mapStateToProps', 29 | }, { 30 | message: 'Connect function argument #2 should be named mapDispatchToProps', 31 | }, 32 | ], 33 | }, { 34 | code: `import { connect } from 'react-redux'; connect({}, {})(Component)`, 35 | errors: [ 36 | { 37 | message: 'Connect function argument #1 should be named mapStateToProps', 38 | }, { 39 | message: 'Connect function argument #2 should be named mapDispatchToProps', 40 | }, 41 | ], 42 | }, { 43 | code: `import { connect } from 'react-redux'; connect(state => state)(TodoApp)`, 44 | errors: [ 45 | { 46 | message: 'Connect function argument #1 should be named mapStateToProps', 47 | }, 48 | ], 49 | }], 50 | }); 51 | -------------------------------------------------------------------------------- /tests/lib/rules/mapDispatchToProps-prefer-parameters-names.js: -------------------------------------------------------------------------------- 1 | const rule = require('../../../lib/rules/mapDispatchToProps-prefer-parameters-names'); 2 | const RuleTester = require('eslint').RuleTester; 3 | const codeSamples = require('../../code-sanity-samples'); 4 | const formatOptions = require('../../util'); 5 | 6 | const parserOptions = formatOptions({ 7 | ecmaVersion: 2018, 8 | sourceType: 'module', 9 | }); 10 | 11 | const ruleTester = new RuleTester( parserOptions ); 12 | 13 | ruleTester.run('mapDispatchToProps-prefer-parameters-names', rule, { 14 | valid: [ 15 | ...codeSamples, 16 | 'const mapDispatchToProps = (dispatch, ownProps) => {}', 17 | 'const mapDispatchToProps = (dispatch, {prop1, prop2}) => {}', 18 | 'const mapDispatchToProps = (dispatch) => {}', 19 | 'const mapDispatchToProps = (dispatch, ownProps, moreArgs) => {}', 20 | 'const mapDispatchToProps = {anAction: anAction}', 21 | `import { connect } from 'react-redux'; connect((state) => state, {anAction: anAction})(App)`, 22 | `import { connect } from 'react-redux'; connect(null, null)(App)`, 23 | `import { connect } from 'react-redux'; connect((state) => state, (dispatch, ownProps, moreArgs) => {})(App)`, 24 | `import { connect } from './path/to/connect.js'; connect('something')`, 25 | `import { connect } from './path/to/connect.js'; connect((state) => state, (anyOtherName) => {})(App)`, 26 | 'function mapDispatchToProps(dispatch, ownProps) {}', 27 | ], 28 | invalid: [{ 29 | code: 'const mapDispatchToProps = (anyOtherName) => {}', 30 | errors: [ 31 | { 32 | message: 'mapDispatchToProps function parameter #0 should be named dispatch', 33 | }, 34 | ], 35 | }, { 36 | code: 'const mapDispatchToProps = (anyName, anyOtherName) => {}', 37 | errors: [ 38 | { 39 | message: 'mapDispatchToProps function parameter #0 should be named dispatch', 40 | }, { 41 | message: 'mapDispatchToProps function parameter #1 should be named ownProps', 42 | }, 43 | ], 44 | }, { 45 | code: 'function mapDispatchToProps(anyOtherName) {}', 46 | errors: [ 47 | { 48 | message: 'mapDispatchToProps function parameter #0 should be named dispatch', 49 | }, 50 | ], 51 | }, { 52 | code: `import { connect } from 'react-redux'; connect((state) => state, (anyOtherName) => {})(App)`, 53 | errors: [ 54 | { 55 | message: 'mapDispatchToProps function parameter #0 should be named dispatch', 56 | }, 57 | ], 58 | }, { 59 | code: `const { connect } = require('react-redux'); connect((state) => state, (anyOtherName) => {})(App)`, 60 | errors: [ 61 | { 62 | message: 'mapDispatchToProps function parameter #0 should be named dispatch', 63 | }, 64 | ], 65 | }], 66 | }); 67 | -------------------------------------------------------------------------------- /tests/lib/rules/mapDispatchToProps-prefer-shorthand.js: -------------------------------------------------------------------------------- 1 | const rule = require('../../../lib/rules/mapDispatchToProps-prefer-shorthand'); 2 | const RuleTester = require('eslint').RuleTester; 3 | const codeSamples = require('../../code-sanity-samples'); 4 | const formatOptions = require('../../util'); 5 | 6 | const parserOptions = formatOptions({ 7 | ecmaVersion: 2018, 8 | sourceType: 'module', 9 | }); 10 | 11 | const ruleTester = new RuleTester( parserOptions ); 12 | 13 | ruleTester.run('mapDispatchToProps-prefer-shorthand', rule, { 14 | valid: [ 15 | ...codeSamples, 16 | 'function mapDispatchToProps () {return {action}}', 17 | `const mapDispatchToProps = dispatch => ({ 18 | onDoSomething: function() {return dispatch(toDo())}, 19 | action2: (arg1, arg2) => dispatch(action2(arg1, arg2)), 20 | });`, 21 | `const mapDispatchToProps = dispatch => ({ 22 | onDoSomething: () => dispatch(onDoSomething()), 23 | action2: (arg1, arg2) => dispatch(action2(arg1 + arg2)), 24 | });`, 25 | 'const mapDispatchToProps = {}', 26 | 'const mapDispatchToProps = null', 27 | 'const mapDispatchToProps = actionsMap', 28 | 'const mapDispatchToProps = {...actions}', 29 | 'const mapDispatchToProps = {anAction: anAction}', 30 | `import { connect } from 'react-redux'; 31 | export default connect( 32 | state => ({ 33 | productsList: state.Products.productsList, 34 | }), 35 | { fetchProducts } 36 | )(Products); 37 | `, 38 | `import { connect } from 'react-redux'; connect(null, null)(App)`, 39 | 'function mapDispatchToProps () {return aThing}', 40 | ], 41 | invalid: [{ 42 | code: `const mapDispatchToProps = dispatch => ({ 43 | onDoSomething: () => dispatch(onDoSomething()), 44 | action1: () => dispatch(action1()), 45 | action2: (arg1, arg2) => dispatch(action2(arg1, arg2)), 46 | });`, 47 | errors: [ 48 | { 49 | message: 'mapDispatchToProps should use a shorthand dispatch wrapping instead', 50 | }, 51 | ], 52 | }, { 53 | code: `const mapDispatchToProps = dispatch => ({ 54 | onDoSomething: function() {return dispatch(onDoSomething())} 55 | });`, 56 | errors: [ 57 | { 58 | message: 'mapDispatchToProps should use a shorthand dispatch wrapping instead', 59 | }, 60 | ], 61 | }, { 62 | code: `const mapDispatchToProps = function(dispatch) { 63 | return { requestFilteredItems: (client, keyword) => 64 | dispatch(requestFilteredItems(client, keyword)) 65 | }; 66 | }`, 67 | errors: [ 68 | { 69 | message: 'mapDispatchToProps should use a shorthand dispatch wrapping instead', 70 | }, 71 | ], 72 | }, { 73 | code: `const mapDispatchToProps = dispatch => ({ 74 | onDoSomething: () => dispatch(toSomethingElse()), 75 | });`, 76 | errors: [ 77 | { 78 | message: 'mapDispatchToProps should use a shorthand dispatch wrapping instead', 79 | }, 80 | ], 81 | }], 82 | }); 83 | -------------------------------------------------------------------------------- /tests/lib/rules/mapDispatchToProps-returns-object.js: -------------------------------------------------------------------------------- 1 | const rule = require('../../../lib/rules/mapDispatchToProps-returns-object'); 2 | const RuleTester = require('eslint').RuleTester; 3 | const codeSamples = require('../../code-sanity-samples'); 4 | const formatOptions = require('../../util'); 5 | 6 | const parserOptions = formatOptions({ 7 | ecmaVersion: 2018, 8 | sourceType: 'module', 9 | }); 10 | const ruleTester = new RuleTester( parserOptions ); 11 | 12 | ruleTester.run('mapDispatchToProps-returns-object', rule, { 13 | valid: [ 14 | ...codeSamples, 15 | 'const mapDispatchToProps = {}', 16 | 'const mapDispatchToProps = null', 17 | 'const mapDispatchToProps = actionsMap', 18 | 'const mapDispatchToProps = {...actions}', 19 | 'const mapDispatchToProps = {anAction: anAction}', 20 | `import { connect } from 'react-redux'; 21 | export default connect( 22 | state => ({ 23 | productsList: state.Products.productsList, 24 | }), 25 | { fetchProducts } 26 | )(Products); 27 | `, 28 | 'function mapDispatchToProps () {return {action}}', 29 | `const mapDispatchToProps = (dispatch) => ( 30 | { 31 | requestFilteredItems: (client, keyword) => { 32 | dispatch(requestFilteredItems(client, keyword)); 33 | } 34 | } 35 | ) 36 | `, 37 | `const mapDispatchToProps = dispatch => ({ 38 | onDoSomething: () => dispatch(toSomethingElse()) 39 | });`, 40 | `const mapDispatchToProps = function(dispatch) { 41 | return { requestFilteredItems: (client, keyword) => { 42 | dispatch(requestFilteredItems(client, keyword)); 43 | } 44 | } 45 | }`, 46 | `import { connect } from 'react-redux'; connect(null, null)(App)`, 47 | 'function mapDispatchToProps () {return aThing}', 48 | `function mapDispatchToProps(dispatch) { 49 | return { actions: bindActionCreators(actionCreators, dispatch) } 50 | }`, 51 | ], 52 | invalid: [{ 53 | code: 'function mapDispatchToProps () {}', 54 | errors: [ 55 | { 56 | message: 'mapDispatchToProps should return object', 57 | }, 58 | ], 59 | }, { 60 | code: 'const mapDispatchToProps = () => {}', 61 | errors: [ 62 | { 63 | message: 'mapDispatchToProps should return object', 64 | }, 65 | ], 66 | }, { 67 | code: `import { connect } from 'react-redux'; 68 | export default connect( 69 | state => ({ 70 | productsList: state.Products.productsList, 71 | }), 72 | dispatch => dispatch(action()) 73 | )(Products); 74 | `, 75 | errors: [ 76 | { 77 | message: 'mapDispatchToProps should return object', 78 | }, 79 | ], 80 | }, { 81 | code: `import { connect } from 'react-redux'; 82 | export default connect( 83 | state => ({ 84 | productsList: state.Products.productsList, 85 | }), 86 | function(dispatch){ return dispatch(action()) } 87 | )(Products); 88 | `, 89 | errors: [ 90 | { 91 | message: 'mapDispatchToProps should return object', 92 | }, 93 | ], 94 | }], 95 | }); 96 | -------------------------------------------------------------------------------- /tests/lib/rules/mapStateToProps-no-store.js: -------------------------------------------------------------------------------- 1 | 2 | const rule = require('../../../lib/rules/mapStateToProps-no-store'); 3 | const RuleTester = require('eslint').RuleTester; 4 | const codeSamples = require('../../code-sanity-samples'); 5 | const formatOptions = require('../../util'); 6 | 7 | const parserOptions = formatOptions({ 8 | ecmaVersion: 2018, 9 | sourceType: 'module', 10 | parserOptions: { 11 | ecmaFeatures: { 12 | jsx: true 13 | } 14 | } 15 | }); 16 | 17 | const ruleTester = new RuleTester( parserOptions ); 18 | 19 | ruleTester.run('mapStateToProps-no-store', rule, { 20 | valid: [ 21 | ...codeSamples, 22 | ` const mapStateToProps = state => ({ 23 | ...getSomeStateFromASelector(state), 24 | showDefaultHeader: showDefaultHeader(state), 25 | }); 26 | `, 27 | ` const mapStateToProps = state => ({ 28 | aField: getSomeStateFromASelector(state), 29 | }); 30 | `, 31 | 'export default function observeStore(store) {return store;}', 32 | `import { connect } from 'react-redux'; export default connect(() => {})(Alert)`, 33 | `import { connect } from 'react-redux'; export default connect(null, null)(Alert)`, 34 | `import { connect } from 'react-redux'; connect((state) => ({isActive: state.isActive}), null)(App)`, 35 | `import { connect } from 'react-redux'; connect(null, null)(App)`, 36 | `import { connect } from 'react-redux'; 37 | connect( 38 | (state) => { 39 | return { 40 | isActive: state.isActive 41 | } 42 | }, 43 | null 44 | )(App) 45 | `, 46 | `import { connect } from 'react-redux'; 47 | connect(function(state){ 48 | return { 49 | isActive: state.isActive 50 | } 51 | }, 52 | null 53 | )(App) 54 | `, 55 | `function mapStateToProps(state) { 56 | return {}; 57 | }`, 58 | `const mapStateToProps = function(state) { 59 | return state.isActive; 60 | }`, 61 | 'const mapStateToProps = (state, ownProps) => {}', 62 | 'const mapStateToProps = (state) => {isActive: state.isActive}', 63 | `import { connect } from 'react-redux'; 64 | const mapStateToProps = (state, ownProps) => {}; 65 | connect(mapStateToProps, null)(Alert);`, 66 | `const mapStateToProps = ({ header }) => ({ 67 | isLoggedIn: header.user && header.user.isLoggedIn, 68 | }); `, 69 | 'const mapStateToProps = ({header}, ownProps) => {header};', 70 | `import { connect } from 'react-redux'; connect(({header}, ownProps) => {header})(App);`, 71 | `import { connect } from 'react-redux'; connect(({header}, {ownProp1}) => {header, ownProp1})(App);`, 72 | ], 73 | invalid: [{ 74 | code: 'const mapStateToProps = (state) => state', 75 | errors: [ 76 | { 77 | message: 'mapStateToProps should not return complete store object', 78 | }, 79 | ], 80 | }, { 81 | code: `const mapStateToProps = state => { 82 | return {state: state} 83 | }`, 84 | errors: [ 85 | { 86 | message: 'mapStateToProps should not return complete store object', 87 | }, 88 | ], 89 | }, { 90 | code: `function mapStateToProps(state) { 91 | return state; 92 | }`, 93 | errors: [ 94 | { 95 | message: 'mapStateToProps should not return complete store object', 96 | }, 97 | ], 98 | }, { 99 | code: `import { connect } from 'react-redux'; 100 | export default connect( 101 | (state) => { 102 | return { 103 | state: state 104 | } 105 | }, 106 | (dispatch) => { 107 | return { 108 | actions: bindActionCreators(actions, dispatch) 109 | } 110 | } 111 | )(App)`, 112 | errors: [ 113 | { 114 | message: 'mapStateToProps should not return complete store object', 115 | }, 116 | ], 117 | }, { 118 | code: `import { connect } from 'react-redux'; connect((state) => state, null)(App)`, 119 | errors: [ 120 | { 121 | message: 'mapStateToProps should not return complete store object', 122 | }, 123 | ], 124 | }, { 125 | code: `import { connect } from 'react-redux'; 126 | const mapStateToProps = (state, ownProps) => state; 127 | connect(mapStateToProps, null)(Alert);`, 128 | errors: [ 129 | { 130 | message: 'mapStateToProps should not return complete store object', 131 | }, 132 | ], 133 | }, { 134 | code: 'const mapStateToProps = state => ({...state});', 135 | errors: [ 136 | { 137 | message: 'mapStateToProps should not return complete store object', 138 | }, 139 | ], 140 | }, { 141 | code: `import { connect } from 'react-redux'; connect((state) => ({...state}), null)(App)`, 142 | errors: [ 143 | { 144 | message: 'mapStateToProps should not return complete store object', 145 | }, 146 | ], 147 | }], 148 | }); 149 | -------------------------------------------------------------------------------- /tests/lib/rules/mapStateToProps-prefer-hoisted.js: -------------------------------------------------------------------------------- 1 | 2 | const rule = require('../../../lib/rules/mapStateToProps-prefer-hoisted'); 3 | const RuleTester = require('eslint').RuleTester; 4 | const codeSamples = require('../../code-sanity-samples'); 5 | const formatOptions = require('../../util'); 6 | 7 | const parserOptions = formatOptions({ 8 | ecmaVersion: 2018, 9 | sourceType: 'module', 10 | }); 11 | 12 | const errorMessage = 'constant arrays and objects should be initialized outside of mapStateToProps'; 13 | 14 | const ruleTester = new RuleTester(parserOptions); 15 | 16 | ruleTester.run('mapStateToProps-prefer-hoisted', rule, { 17 | valid: [ 18 | ...codeSamples, 19 | `function mapStateToProps(state) { 20 | return {}; 21 | }`, 22 | `const mapStateToProps = state => { 23 | return { 24 | a : 1 25 | }; 26 | };`, 27 | `const mapStateToProps = state => { 28 | const a = state.a 29 | return { 30 | a 31 | }; 32 | };`, 33 | `const mapStateToProps = state => ({ 34 | user: state.user, 35 | list: [1, 2, state.count] 36 | }); 37 | `, 38 | `const mapStateToProps = state => { 39 | return { 40 | a: 1, 41 | b: [state.b, 2] 42 | }; 43 | }; 44 | `, 45 | `const mapStateToProps = state => { 46 | const foo = 'hello'; 47 | return { 48 | a: 1, 49 | b: [foo, 2] 50 | }; 51 | }; 52 | `, 53 | `import { connect } from 'react-redux'; export default connect(null, null)(Alert)`, 54 | `import { connect } from 'react-redux'; connect((state) => ({isActive: state.isActive}), null)(App)`, 55 | `import { connect } from 'react-redux'; connect(null, null)(App)`, 56 | `import { connect } from 'react-redux'; 57 | connect( 58 | (state) => { 59 | return { 60 | isActive: state.isActive 61 | } 62 | }, 63 | null 64 | )(App) 65 | `, 66 | `import { connect } from 'react-redux'; 67 | connect(function(state){ 68 | return { 69 | isActive: state.isActive 70 | } 71 | }, 72 | null 73 | )(App) 74 | `, 75 | `const mapStateToProps = function(state) { 76 | return { 77 | a: x 78 | }; 79 | }`, 80 | 'const mapStateToProps = (state, ownProps) => {}', 81 | 'const mapStateToProps = (state) => {set: [1, 2, 3, state.a]}', 82 | `import { connect } from 'react-redux'; 83 | const mapStateToProps = (state, ownProps) => {}; 84 | connect(mapStateToProps, null)(Alert);`, 85 | `const mapStateToProps = ({ header }) => ({ 86 | isLoggedIn: header.user && header.user.isLoggedIn, 87 | }); `, 88 | 'const mapStateToProps = ({header}, ownProps) => {header};', 89 | `import { connect } from 'react-redux'; connect(({header}, ownProps) => {header})(App);`, 90 | `import { connect } from 'react-redux'; connect(({header}, {ownProp1}) => {header, ownProp1})(App);`, 91 | `const mapStateToProps = ({header}, ownProps) => { 92 | return { 93 | props: { 94 | header, 95 | } 96 | } 97 | };`, 98 | `import { connect } from 'react-redux'; 99 | const createConnectedToolbarItem = (icon, onClick) => { 100 | const mapStateToProps = { onClick } 101 | 102 | connect( 103 | null, 104 | mapStateToProps 105 | )(createToolbarItem(icon)) 106 | }`, 107 | ], 108 | invalid: [{ 109 | code: `const mapStateToProps = (state) => { 110 | return { 111 | foo: { 112 | a: 1 113 | } 114 | } 115 | }`, 116 | errors: [ 117 | { 118 | message: errorMessage, 119 | }, 120 | ], 121 | }, { 122 | code: `const mapStateToProps = state => { 123 | return { 124 | foo: [1, 2, 3] 125 | } 126 | }`, 127 | errors: [ 128 | { 129 | message: errorMessage, 130 | }, 131 | ], 132 | }, { 133 | code: `function mapStateToProps(state) { 134 | return { 135 | a: [] 136 | }; 137 | }`, 138 | errors: [ 139 | { 140 | message: errorMessage, 141 | }, 142 | ], 143 | }, { 144 | code: `import { connect } from 'react-redux'; 145 | export default connect( 146 | (state) => { 147 | return { 148 | a: { 149 | z: 1 150 | } 151 | } 152 | }, 153 | (dispatch) => { 154 | return { 155 | actions: bindActionCreators(actions, dispatch) 156 | } 157 | } 158 | )(App)`, 159 | errors: [ 160 | { 161 | message: errorMessage, 162 | }, 163 | ], 164 | }, { 165 | code: `const mapStateToProps = state => { 166 | return { 167 | a: [1, 2, 3], 168 | }; 169 | }; 170 | `, 171 | errors: [ 172 | { 173 | message: errorMessage, 174 | }, 175 | ], 176 | }, { 177 | code: `function mapStateToProps(state) { 178 | return {a : {}}; 179 | }`, 180 | errors: [ 181 | { 182 | message: errorMessage, 183 | }, 184 | ], 185 | }, { 186 | code: `function mapStateToProps(state) { 187 | return { 188 | aProp: state.aProp, 189 | aConstProp: [1, 2, 3] 190 | }; 191 | }`, 192 | errors: [ 193 | { 194 | message: errorMessage, 195 | }, 196 | ], 197 | }, 198 | ], 199 | }); 200 | -------------------------------------------------------------------------------- /tests/lib/rules/mapStateToProps-prefer-parameters-names.js: -------------------------------------------------------------------------------- 1 | const rule = require('../../../lib/rules/mapStateToProps-prefer-parameters-names'); 2 | const RuleTester = require('eslint').RuleTester; 3 | const codeSamples = require('../../code-sanity-samples'); 4 | const formatOptions = require('../../util'); 5 | 6 | const parserOptions = formatOptions({ 7 | ecmaVersion: 2018, 8 | sourceType: 'module', 9 | }); 10 | const ruleTester = new RuleTester(parserOptions); 11 | 12 | ruleTester.run('mapStateToProps-prefer-parameters-names', rule, { 13 | valid: [ 14 | ...codeSamples, 15 | 'const mapStateToProps = ({prop1, prop2}, {ownProp1, ownProp2}) => {}', 16 | 'const mapStateToProps = (state, ownProps) => {}', 17 | 'const mapStateToProps = (state) => {}', 18 | 'const mapStateToProps = (state, ownProps, moreArgs) => {}', 19 | `import { connect } from 'react-redux'; connect((state) => state, null)(App)`, 20 | 'function mapStateToProps(state, ownProps) {}', 21 | `import { connect } from 'react-redux'; connect({state}, null)(App)`, 22 | 'const mapStateToProps = {}', 23 | `import { connect } from 'react-redux'; connect(null, null)(App)`, 24 | 'const mapStateToProps = ({prop1, prop2}, ownProps) => {}', 25 | ], 26 | invalid: [{ 27 | code: 'const mapStateToProps = (anyOtherName) => {}', 28 | errors: [ 29 | { 30 | message: 'mapStateToProps function parameter #0 should be named state', 31 | }, 32 | ], 33 | }, { 34 | code: 'const mapStateToProps = (anyOtherName, anyOtherName1) => {}', 35 | errors: [ 36 | { 37 | message: 'mapStateToProps function parameter #0 should be named state', 38 | }, { 39 | message: 'mapStateToProps function parameter #1 should be named ownProps', 40 | }, 41 | ], 42 | }, { 43 | code: `import { connect } from 'react-redux'; connect(function(anyOtherName) {}, null)(App)`, 44 | errors: [ 45 | { 46 | message: 'mapStateToProps function parameter #0 should be named state', 47 | }, 48 | ], 49 | }], 50 | }); 51 | -------------------------------------------------------------------------------- /tests/lib/rules/mapStateToProps-prefer-selectors.js: -------------------------------------------------------------------------------- 1 | const rule = require('../../../lib/rules/mapStateToProps-prefer-selectors'); 2 | const RuleTester = require('eslint').RuleTester; 3 | const codeSamples = require('../../code-sanity-samples'); 4 | const formatOptions = require('../../util'); 5 | 6 | const parserOptions = formatOptions({ 7 | ecmaVersion: 2018, 8 | sourceType: 'module', 9 | }); 10 | 11 | const ruleTester = new RuleTester(parserOptions); 12 | 13 | ruleTester.run('mapStateToProps-prefer-selectors', rule, { 14 | valid: [ 15 | ...codeSamples, 16 | 'const mapStateToProps = (state) => 1', 17 | 'const mapStateToProps = (state) => ({})', 18 | 'const mapStateToProps = (state) => ({ x: xSelector(state) })', 19 | 'const mapStateToProps = (state, ownProps) => ({ x: xSelector(state, ownProps) })', 20 | 'const mapStateToProps = (state) => ({ x: xSelector(state), y: ySelector(state) })', 21 | 'const mapStateToProps = (state) => { return { x: xSelector(state) }; }', 22 | 'const mapStateToProps = (state) => { doSomethingElse(); return { x: xSelector(state) }; }', 23 | 'const mapStateToProps = function(state) { return { x: xSelector(state) }; }', 24 | 'function mapStateToProps(state) { doSomethingElse(); return { x: xSelector(state) }; }', 25 | `import { connect } from 'react-redux'; connect((state) => ({ x: xSelector(state) }), {})(Comp)`, 26 | 'const mapStateToProps = () => ({ x: xSelector() })', 27 | 'const mapStateToProps = function(state) { return { x: getX() }; }', 28 | 'const mapStateToProps = function(state) { return { x: getX(state) }; }', 29 | `import { connect } from 'react-redux'; connect((state, ownProps) => ({ x: selector() }), {})(Comp)`, 30 | `import { connect } from 'react-redux'; connect((state, ownProps) => ({ x: selector(state) }), {})(Comp)`, 31 | `import { connect } from 'react-redux'; connect((state, ownProps) => ({ x: selector(state, ownProps) }), {})(Comp)`, 32 | { 33 | code: 'const mapStateToProps = (state) => ({ x: xSelector(state) })', 34 | options: [{ 35 | matching: '^.*Selector$', 36 | }], 37 | }, 38 | { 39 | code: 'const mapStateToProps = function(state) { return { x: getX(state) }; }', 40 | options: [{ 41 | matching: '^get.*$', 42 | }], 43 | }, 44 | { 45 | code: `import { connect } from 'react-redux'; connect((state) => ({ x: selector(state) }), {})(Comp)`, 46 | options: [{ 47 | matching: '^selector$', 48 | }], 49 | }, 50 | { 51 | code: 'const mapStateToProps = (state) => ({ x: xSelector(differentParam) })', 52 | options: [{ 53 | validateParams: false, 54 | }], 55 | }, 56 | { 57 | code: 'const mapStateToProps = function(state) { return { x: getX(state, ownProps2) }; }', 58 | options: [{ 59 | validateParams: false, 60 | }], 61 | }, 62 | { 63 | code: `import { connect } from 'react-redux'; connect(() => ({ x: selector(state) }), {})(Comp)`, 64 | options: [{ 65 | validateParams: false, 66 | }], 67 | }, 68 | ], 69 | invalid: [{ 70 | code: 'const mapStateToProps = (state) => ({ x: state.b })', 71 | errors: [ 72 | { 73 | message: 'mapStateToProps property "x" should use a selector function.', 74 | }, 75 | ], 76 | }, { 77 | code: 'const mapStateToProps = (state) => ({ x: state.x, y: state.y })', 78 | errors: [ 79 | { 80 | message: 'mapStateToProps property "x" should use a selector function.', 81 | }, 82 | { 83 | message: 'mapStateToProps property "y" should use a selector function.', 84 | }, 85 | ], 86 | }, { 87 | code: 'const mapStateToProps = (state) => ({ x: state.x, y: ySelector(state) })', 88 | errors: [ 89 | { 90 | message: 'mapStateToProps property "x" should use a selector function.', 91 | }, 92 | ], 93 | }, { 94 | code: 'const mapStateToProps = (state) => { return { x: state.b }; }', 95 | errors: [ 96 | { 97 | message: 'mapStateToProps property "x" should use a selector function.', 98 | }, 99 | ], 100 | }, { 101 | code: 'const mapStateToProps = (state) => { doSomethingElse(); return { x: state.b }; }', 102 | errors: [ 103 | { 104 | message: 'mapStateToProps property "x" should use a selector function.', 105 | }, 106 | ], 107 | }, { 108 | code: 'const mapStateToProps = function(state) { return { x: state.x }; }', 109 | errors: [ 110 | { 111 | message: 'mapStateToProps property "x" should use a selector function.', 112 | }, 113 | ], 114 | }, { 115 | code: 'function mapStateToProps(state) { doSomethingElse(); return { x: state.b }; }', 116 | errors: [ 117 | { 118 | message: 'mapStateToProps property "x" should use a selector function.', 119 | }, 120 | ], 121 | }, { 122 | code: `import { connect } from 'react-redux'; connect((state) => ({ x: state.x }), {})(Comp)`, 123 | errors: [ 124 | { 125 | message: 'mapStateToProps property "x" should use a selector function.', 126 | }, 127 | ], 128 | }, { 129 | code: 'const mapStateToProps = (state) => ({ x: xSelector(state) })', 130 | options: [{ 131 | matching: '^get.*$', 132 | }], 133 | errors: [{ 134 | message: 'mapStateToProps "x"\'s selector "xSelector" does not match "^get.*$".', 135 | }], 136 | }, { 137 | code: 'const mapStateToProps = function(state) { return { x: getX(state) }; }', 138 | options: [{ 139 | matching: '^.*Selector$', 140 | }], 141 | errors: [{ 142 | message: 'mapStateToProps "x"\'s selector "getX" does not match "^.*Selector$".', 143 | }], 144 | }, { 145 | code: `import { connect } from 'react-redux'; connect((state) => ({ x: selectorr(state) }), {})(Comp)`, 146 | options: [{ 147 | matching: '^selector$', 148 | }], 149 | errors: [{ 150 | message: 'mapStateToProps "x"\'s selector "selectorr" does not match "^selector$".', 151 | }], 152 | }, { 153 | code: 'const mapStateToProps = (state) => ({ x: xSelector(state, ownProps) })', 154 | errors: [{ 155 | message: 'mapStateToProps "x"\'s selector "xSelector" parameter #1 is not expected.', 156 | }], 157 | }, { 158 | code: 'const mapStateToProps = (state, ownProps) => ({ x: xSelector(state, ownProps, someOtherValue) })', 159 | errors: [{ 160 | message: 'mapStateToProps "x"\'s selector "xSelector" parameter #2 is not expected.', 161 | }], 162 | }, { 163 | code: 'const mapStateToProps = function(state) { return { x: getX(notState) }; }', 164 | errors: [{ 165 | message: 'mapStateToProps "x"\'s selector "getX" parameter #0 should be "state".', 166 | }], 167 | }, { 168 | code: `import { connect } from 'react-redux'; connect((state, ownProps) => ({ x: getX(state, notOwnProps) }), {})(Comp)`, 169 | errors: [{ 170 | message: 'mapStateToProps "x"\'s selector "getX" parameter #1 should be "ownProps".', 171 | }], 172 | }, { 173 | code: `import { connect } from 'react-redux'; connect((state2, ownProps) => ({ x: getX(state) }), {})(Comp)`, 174 | errors: [{ 175 | message: 'mapStateToProps "x"\'s selector "getX" parameter #0 should be "state2".', 176 | }], 177 | }, { 178 | code: `import { connect } from 'react-redux'; connect((state, ownProps2) => ({ x: getX(state, ownProps) }), {})(Comp)`, 179 | errors: [{ 180 | message: 'mapStateToProps "x"\'s selector "getX" parameter #1 should be "ownProps2".', 181 | }], 182 | }], 183 | 184 | }); 185 | -------------------------------------------------------------------------------- /tests/lib/rules/no-unused-prop-types.js: -------------------------------------------------------------------------------- 1 | 2 | const rule = require('../../../lib/rules/no-unused-prop-types'); 3 | const RuleTester = require('eslint').RuleTester; 4 | const codeSamples = require('../../code-sanity-samples'); 5 | const formatOptions = require('../../util'); 6 | 7 | const parserOptions = formatOptions({ 8 | ecmaVersion: 2018, 9 | sourceType: 'module', 10 | ecmaFeatures: { 11 | jsx: true 12 | } 13 | }); 14 | 15 | console.log(parserOptions); 16 | const ruleTester = new RuleTester(parserOptions); 17 | 18 | ruleTester.run('no-unused-prop-types', rule, { 19 | valid: [ 20 | ...codeSamples, 21 | `export const mapStateToProps = (state, ownProps) => { 22 | const { myProp } = ownProps; 23 | return { myData: getMyData(state, myProp)}; 24 | } 25 | 26 | export class MyComponent extends Component { 27 | render() { 28 | return
{this.props.myData}
; 29 | } 30 | } 31 | 32 | MyComponent.propTypes = { 33 | myProp: PropTypes.string.isRequired 34 | }; 35 | 36 | import { connect } from 'react-redux'; 37 | export default connect(mapStateToProps)(MyComponent);`, 38 | 39 | `export const mapStateToProps = (state, ownProps) => { 40 | const myProp = ownProps.myProp; 41 | return { myData: getMyData(state, myProp)}; 42 | } 43 | 44 | export class MyComponent extends Component { 45 | render() { 46 | return
{this.props.myData}
; 47 | } 48 | } 49 | 50 | MyComponent.propTypes = { 51 | myProp: PropTypes.string.isRequired 52 | }; 53 | 54 | import { connect } from 'react-redux'; 55 | export default connect(mapStateToProps)(MyComponent);`, 56 | 57 | `export const mapStateToProps = (state, ownProps) => ({ 58 | myData: getMyData(state, ownProps.myProp), 59 | }); 60 | 61 | export class MyComponent extends Component { 62 | render() { 63 | return
{this.props.myData}
; 64 | } 65 | } 66 | 67 | MyComponent.propTypes = { 68 | myProp: PropTypes.string.isRequired 69 | }; 70 | 71 | import { connect } from 'react-redux'; 72 | export default connect(mapStateToProps)(MyComponent);`, 73 | 74 | `export const mapDispatchToProps = (state, ownProps) => ({ 75 | myData: getMyData(state, ownProps.myProp), 76 | }); 77 | 78 | export class MyComponent extends Component { 79 | render() { 80 | return
{this.props.myData}
; 81 | } 82 | } 83 | 84 | MyComponent.propTypes = { 85 | myProp: PropTypes.string.isRequired 86 | }; 87 | 88 | import { connect } from 'react-redux'; 89 | export default connect(mapStateToProps)(MyComponent);`, 90 | `export const mapStateToProps = (state, {myProp}) => ({ 91 | myData: getMyData(state, myProp.z), 92 | }); 93 | 94 | export class MyComponent extends Component { 95 | render() { 96 | return
{this.props.myData}
; 97 | } 98 | } 99 | 100 | MyComponent.propTypes = { 101 | myProp: PropTypes.string.isRequired 102 | }; 103 | 104 | import { connect } from 'react-redux'; 105 | export default connect(mapStateToProps)(MyComponent);`, 106 | `const selectorFoo = (state) => ({isFetching: false, name: 'Foo', isDeleting: false, deltedId: ''}); 107 | const selectorBar = (state) => ({ isFetching: false, name: 'Bar'}); 108 | export const mapStateToProps = (state) => { 109 | const { isFetching: isFetchingFoo, ...restFoo } = selectorFoo(state); 110 | const { isFetching: isFeatchingBar, ...restBar } = selectorBar(state); 111 | return { 112 | isFetchingFoo, 113 | isFetchingBar, 114 | ...restFoo, 115 | ...restBar, 116 | }; 117 | }; 118 | export class MyComponent extends Component { 119 | render() { 120 | const {isFetchingFoo, name, isFetchingBar, isDeleting, deletedId} = this.props; 121 | return ( 122 |
123 | {isFetchingFoo} 124 | {isDeleting} 125 | {isFetchingBar} 126 | {name}{deletedId} 127 |
128 | ) 129 | } 130 | }; 131 | 132 | MyComponent.propTypes = { 133 | isFetchingFoo: PropTypes.bool.isRequired, 134 | isDeleting: PropTypes.bool.isRequired, 135 | deletedId: PropTypes.number.isRequired, 136 | name: Proptypes.string.isRequired, 137 | isFetchingBar: PropTypes.bool.isRequired, 138 | }; 139 | 140 | import { connect } from 'react-redux'; 141 | export default connect(mapStateToProps)(MyComponent);`, 142 | ], 143 | invalid: [{ 144 | code: `export const mapStateToProps = (state) => ({ 145 | myData: getMyData(state), 146 | }); 147 | 148 | export class MyComponent extends Component { 149 | render() { 150 | return
{this.props.myData}
; 151 | } 152 | } 153 | 154 | MyComponent.propTypes = { 155 | myProp: PropTypes.string.isRequired 156 | }; 157 | 158 | import { connect } from 'react-redux'; 159 | export default connect(mapStateToProps)(MyComponent);`, 160 | 161 | errors: [ 162 | { 163 | message: '\'myProp\' PropType is defined but prop is never used', 164 | }, 165 | ], 166 | }, { 167 | code: `export const mapStateToProps = (state, ownProps) => ({ 168 | myData: getMyData(state, ownProps.myProp), 169 | }); 170 | 171 | export class MyComponent extends Component { 172 | render() { 173 | return
{this.props.myData}
; 174 | } 175 | } 176 | 177 | MyComponent.propTypes = { 178 | myProp: PropTypes.string.isRequired, 179 | notUsedProp: PropTypes.string.isRequired, 180 | }; 181 | 182 | import { connect } from 'react-redux'; 183 | export default connect(mapStateToProps)(MyComponent);`, 184 | 185 | errors: [ 186 | { 187 | message: '\'notUsedProp\' PropType is defined but prop is never used', 188 | }, 189 | ], 190 | }, { 191 | code: `export const mapStateToProps = ({aState}, ownProps) => ({ 192 | myData: getMyData(aState), 193 | }); 194 | 195 | export class MyComponent extends Component { 196 | render() { 197 | return
{this.props.myData}
; 198 | } 199 | } 200 | 201 | MyComponent.propTypes = { 202 | myProp: PropTypes.string.isRequired 203 | }; 204 | 205 | import { connect } from 'react-redux'; 206 | export default connect(mapStateToProps)(MyComponent);`, 207 | 208 | errors: [ 209 | { 210 | message: '\'myProp\' PropType is defined but prop is never used', 211 | }, 212 | ], 213 | }, { 214 | code: `export const mapStateToProps = (state, {myProp}) => ({ 215 | myData: getMyData(state, myProp), 216 | }); 217 | 218 | export class MyComponent extends Component { 219 | render() { 220 | return
{this.props.myData}
; 221 | } 222 | } 223 | 224 | MyComponent.propTypes = { 225 | myProp: PropTypes.string.isRequired, 226 | notUsedProp: PropTypes.string.isRequired, 227 | }; 228 | 229 | import { connect } from 'react-redux'; 230 | export default connect(mapStateToProps)(MyComponent);`, 231 | 232 | errors: [ 233 | { 234 | message: '\'notUsedProp\' PropType is defined but prop is never used', 235 | }, 236 | ], 237 | }, { 238 | code: `export const mapStateToProps = (state, ownProps) => { 239 | const { myProp } = ownProps; 240 | return { myData: getMyData(state, myProp)}; 241 | } 242 | 243 | export class MyComponent extends Component { 244 | render() { 245 | return
{this.props.myData}
; 246 | } 247 | } 248 | 249 | MyComponent.propTypes = { 250 | myProp: PropTypes.string.isRequired, 251 | notUsedProp: PropTypes.string.isRequired, 252 | }; 253 | 254 | import { connect } from 'react-redux'; 255 | export default connect(mapStateToProps)(MyComponent);`, 256 | 257 | errors: [ 258 | { 259 | message: '\'notUsedProp\' PropType is defined but prop is never used', 260 | }, 261 | ], 262 | }], 263 | }); 264 | -------------------------------------------------------------------------------- /tests/lib/rules/prefer-separate-component-file.js: -------------------------------------------------------------------------------- 1 | const rule = require('../../../lib/rules/prefer-separate-component-file'); 2 | const RuleTester = require('eslint').RuleTester; 3 | const codeSamples = require('../../code-sanity-samples'); 4 | const formatOptions = require('../../util'); 5 | 6 | const parserOptions = formatOptions({ 7 | ecmaVersion: 2018, 8 | sourceType: 'module', 9 | }); 10 | const ruleTester = new RuleTester(parserOptions); 11 | 12 | ruleTester.run('prefer-separate-component-file', rule, { 13 | valid: [ 14 | ...codeSamples, 15 | `import { connect } from 'react-redux'; 16 | import Component from './component'; 17 | connect(mapStateToProps, mapDispatchToProps)(Component)`, 18 | `import { connect } from 'react-redux'; 19 | const Component = require('./component') 20 | connect(mapStateToProps, mapDispatchToProps)(Component)`, 21 | `import { connect } from 'react-redux'; 22 | import {Component} from './component'; 23 | connect(mapStateToProps, mapDispatchToProps)(Component)`, 24 | ], 25 | invalid: [{ 26 | code: `import { connect } from 'react-redux'; 27 | const Component = () => {}; 28 | connect(mapStateToProps, null)(Component)`, 29 | errors: [ 30 | { 31 | message: 'Connected component should be defined in a separate file.', 32 | }, 33 | ], 34 | }], 35 | }); 36 | -------------------------------------------------------------------------------- /tests/lib/rules/useSelector-prefer-selectors.js: -------------------------------------------------------------------------------- 1 | const rule = require('../../../lib/rules/useSelector-prefer-selectors'); 2 | const RuleTester = require('eslint').RuleTester; 3 | const codeSamples = require('../../code-sanity-samples'); 4 | const formatOptions = require('../../util'); 5 | 6 | const parserOptions = formatOptions({ 7 | ecmaVersion: 2018, 8 | sourceType: 'module', 9 | }); 10 | const ruleTester = new RuleTester(parserOptions); 11 | 12 | ruleTester.run('useSelector-prefer-selectors', rule, { 13 | valid: [ 14 | ...codeSamples, 15 | 'const property = useSelector(xSelector)', 16 | { 17 | code: 'const property = useSelector(xSelector)', 18 | options: [{ 19 | matching: '^.*Selector$', 20 | }], 21 | }, 22 | { 23 | code: 'const property = useSelector(getX)', 24 | options: [{ 25 | matching: '^get.*$', 26 | }], 27 | }, 28 | { 29 | code: 'const property = useSelector(selector)', 30 | options: [{ 31 | matching: '^selector$', 32 | }], 33 | }, 34 | { 35 | code: 'const property = useAppSelector(selector)', 36 | options: [{ 37 | matching: '^selector$', 38 | hook: 'useAppSelector', 39 | }], 40 | }, 41 | ], 42 | invalid: [{ 43 | code: 'const property = useSelector((state) => state.x)', 44 | errors: [ 45 | { 46 | message: 'useSelector should use a named selector function.', 47 | }, 48 | ], 49 | }, { 50 | code: 'const property = useSelector(function(state) { return state.x })', 51 | errors: [{ 52 | message: 'useSelector should use a named selector function.', 53 | }], 54 | }, { 55 | code: 'const property = useSelector(xSelector)', 56 | options: [{ 57 | matching: '^get.*$', 58 | }], 59 | errors: [{ 60 | message: 'useSelector selector "xSelector" does not match "^get.*$".', 61 | }], 62 | }, { 63 | code: 'const property = useSelector(getX)', 64 | options: [{ 65 | matching: '^.*Selector$', 66 | }], 67 | errors: [{ 68 | message: 'useSelector selector "getX" does not match "^.*Selector$".', 69 | }], 70 | }, { 71 | code: 'const property = useSelector(selectorr)', 72 | options: [{ 73 | matching: '^selector$', 74 | }], 75 | errors: [{ 76 | message: 'useSelector selector "selectorr" does not match "^selector$".', 77 | }], 78 | }, { 79 | code: 'const property = useAppSelector(selectorr)', 80 | options: [{ 81 | matching: '^selector$', 82 | hook: ['useSelector', 'useAppSelector'], 83 | }], 84 | errors: [{ 85 | message: 'useAppSelector selector "selectorr" does not match "^selector$".', 86 | }], 87 | }], 88 | }); 89 | -------------------------------------------------------------------------------- /tests/util.js: -------------------------------------------------------------------------------- 1 | const semver = require('semver'); 2 | const eslintPkg = require('eslint/package.json'); 3 | const { parserOptions } = require('eslint-plugin-import/config/react'); 4 | 5 | function formatOtions(item) { 6 | if (semver.major(eslintPkg.version) < 9) { 7 | return Object.assign({}, { parserOptions: item }); 8 | } 9 | const newItem = Object.assign({}, { languageOptions: item }) 10 | if (item.ecmaFeatures) { 11 | const parserOptions = { 12 | ecmaFeatures: item.ecmaFeatures 13 | } 14 | delete newItem.languageOptions.ecmaFeatures; 15 | newItem.languageOptions = { 16 | ...newItem.languageOptions, 17 | parserOptions 18 | } 19 | 20 | 21 | } 22 | return newItem; 23 | 24 | } 25 | module.exports = formatOtions; --------------------------------------------------------------------------------