Repository: yannickcr/eslint-plugin-react
Pull request #366 (2015-12-19 01:10:40)

Add no-is-mounted rule
isMounted is an anti-pattern [0], is not available when using ES6
classes, and will eventually be officially deprecated. This commit adds
a rule that prevents the use of this method.

[0]: https://facebook.github.io/react/blog/2015/12/16/ismounted-antipattern.html

Finishes #37


Patch:
diff --git a/README.md b/README.md
index 48acbcad..f0939fab 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,7 @@ Finally, enable all of the rules that you would like to use.
     "react/jsx-max-props-per-line": 1,
     "react/jsx-no-bind": 1,
     "react/jsx-no-duplicate-props": 1,
+    "react/jsx-no-is-mounted": 1,
     "react/jsx-no-literals": 1,
     "react/jsx-no-undef": 1,
     "react/jsx-pascal-case": 1,
@@ -110,6 +111,7 @@ Finally, enable all of the rules that you would like to use.
 * [no-did-mount-set-state](docs/rules/no-did-mount-set-state.md): Prevent usage of `setState` in `componentDidMount`
 * [no-did-update-set-state](docs/rules/no-did-update-set-state.md): Prevent usage of `setState` in `componentDidUpdate`
 * [no-direct-mutation-state](docs/rules/no-direct-mutation-state.md): Prevent direct mutation of `this.state`
+* [no-is-mounted](docs/rules/no-is-mounted.md): Prevent usage of `isMounted`
 * [no-multi-comp](docs/rules/no-multi-comp.md): Prevent multiple component definition per file
 * [no-set-state](docs/rules/no-set-state.md): Prevent usage of `setState`
 * [no-unknown-property](docs/rules/no-unknown-property.md): Prevent usage of unknown DOM property
diff --git a/docs/rules/no-is-mounted.md b/docs/rules/no-is-mounted.md
new file mode 100644
index 00000000..365fc441
--- /dev/null
+++ b/docs/rules/no-is-mounted.md
@@ -0,0 +1,34 @@
+# Prevent usage of isMounted (no-is-mounted)
+
+[`isMounted` is an anti-pattern](anti-pattern), is not available when using ES6 classes, and it is on its way to being officially deprecated.
+
+[anti-pattern]: https://facebook.github.io/react/blog/2015/12/16/ismounted-antipattern.html
+
+## Rule Details
+
+The following patterns are considered warnings:
+
+```js
+var Hello = React.createClass({
+  handleClick: function() {
+    setTimeout(function() {
+      if (this.isMounted()) {
+        return;
+      }
+    });
+  },
+  render: function() {
+    return <div onClick={this.handleClick.bind(this)}>Hello</div>;
+  }
+});
+```
+
+The following patterns are not considered warnings:
+
+```js
+var Hello = React.createClass({
+  render: function() {
+    return <div onClick={this.props.handleClick}>Hello</div>;
+  }
+});
+```
diff --git a/index.js b/index.js
index 88a3d002..e4827777 100644
--- a/index.js
+++ b/index.js
@@ -10,6 +10,7 @@ module.exports = {
     'self-closing-comp': require('./lib/rules/self-closing-comp'),
     'no-danger': require('./lib/rules/no-danger'),
     'no-set-state': require('./lib/rules/no-set-state'),
+    'no-is-mounted': require('./lib/rules/no-is-mounted'),
     'no-deprecated': require('./lib/rules/no-deprecated'),
     'no-did-mount-set-state': require('./lib/rules/no-did-mount-set-state'),
     'no-did-update-set-state': require('./lib/rules/no-did-update-set-state'),
@@ -47,6 +48,7 @@ module.exports = {
     'no-deprecated': 0,
     'no-danger': 0,
     'no-set-state': 0,
+    'no-is-mounted': 0,
     'no-did-mount-set-state': 0,
     'no-did-update-set-state': 0,
     'react-in-jsx-scope': 0,
diff --git a/lib/rules/no-is-mounted.js b/lib/rules/no-is-mounted.js
new file mode 100644
index 00000000..bfd8c1de
--- /dev/null
+++ b/lib/rules/no-is-mounted.js
@@ -0,0 +1,39 @@
+/**
+ * @fileoverview Prevent usage of isMounted
+ * @author Joe Lencioni
+ */
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = function(context) {
+
+  // --------------------------------------------------------------------------
+  // Public
+  // --------------------------------------------------------------------------
+
+  return {
+
+    CallExpression: function(node) {
+      var callee = node.callee;
+      if (callee.type !== 'MemberExpression') {
+        return;
+      }
+      if (callee.object.type !== 'ThisExpression' || callee.property.name !== 'isMounted') {
+        return;
+      }
+      var ancestors = context.getAncestors(callee);
+      for (var i = 0, j = ancestors.length; i < j; i++) {
+        if (ancestors[i].type === 'Property' || ancestors[i].type === 'MethodDefinition') {
+          context.report(callee, 'Do not use isMounted');
+          break;
+        }
+      }
+    }
+  };
+
+};
+
+module.exports.schema = [];


Test patch:
diff --git a/tests/lib/rules/no-is-mounted.js b/tests/lib/rules/no-is-mounted.js
new file mode 100644
index 00000000..9fd38fbc
--- /dev/null
+++ b/tests/lib/rules/no-is-mounted.js
@@ -0,0 +1,110 @@
+/**
+ * @fileoverview Prevent usage of isMounted
+ * @author Joe Lencioni
+ */
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+var rule = require('../../../lib/rules/no-is-mounted');
+var RuleTester = require('eslint').RuleTester;
+
+var parserOptions = {
+  ecmaVersion: 6,
+  ecmaFeatures: {
+    jsx: true
+  }
+};
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+var ruleTester = new RuleTester();
+ruleTester.run('no-is-mounted', rule, {
+
+  valid: [{
+    code: [
+      'var Hello = function() {',
+      '};'
+    ].join('\n'),
+    parserOptions: parserOptions
+  }, {
+    code: [
+      'var Hello = React.createClass({',
+      '  render: function() {',
+      '    return <div>Hello</div>;',
+      '  }',
+      '});'
+    ].join('\n'),
+    parserOptions: parserOptions
+  }, {
+    code: [
+      'var Hello = React.createClass({',
+      '  componentDidUpdate: function() {',
+      '    someNonMemberFunction(arg);',
+      '    this.someFunc = this.isMounted;',
+      '  },',
+      '  render: function() {',
+      '    return <div>Hello</div>;',
+      '  }',
+      '});'
+    ].join('\n'),
+    parserOptions: parserOptions
+  }],
+
+  invalid: [{
+    code: [
+      'var Hello = React.createClass({',
+      '  componentDidUpdate: function() {',
+      '    if (!this.isMounted()) {',
+      '      return;',
+      '    }',
+      '  },',
+      '  render: function() {',
+      '    return <div>Hello</div>;',
+      '  }',
+      '});'
+    ].join('\n'),
+    parserOptions: parserOptions,
+    errors: [{
+      message: 'Do not use isMounted'
+    }]
+  }, {
+    code: [
+      'var Hello = React.createClass({',
+      '  someMethod: function() {',
+      '    if (!this.isMounted()) {',
+      '      return;',
+      '    }',
+      '  },',
+      '  render: function() {',
+      '    return <div onClick={this.someMethod.bind(this)}>Hello</div>;',
+      '  }',
+      '});'
+    ].join('\n'),
+    parserOptions: parserOptions,
+    errors: [{
+      message: 'Do not use isMounted'
+    }]
+  }, {
+    code: [
+      'class Hello extends React.Component {',
+      '  someMethod() {',
+      '    if (!this.isMounted()) {',
+      '      return;',
+      '    }',
+      '  }',
+      '  render() {',
+      '    return <div onClick={this.someMethod.bind(this)}>Hello</div>;',
+      '  }',
+      '};'
+    ].join('\n'),
+    parserOptions: parserOptions,
+    errors: [{
+      message: 'Do not use isMounted'
+    }]
+  }]
+});