diff --git a/README.md b/README.md
index ad95c7e922..abe9413ef7 100644
--- a/README.md
+++ b/README.md
@@ -160,6 +160,7 @@ Enable the rules that you would like to use.
| | | [react/no-will-update-set-state](docs/rules/no-will-update-set-state.md) | Prevent usage of setState in componentWillUpdate |
| | | [react/prefer-es6-class](docs/rules/prefer-es6-class.md) | Enforce ES5 or ES6 class for React Components |
| | | [react/prefer-exact-props](docs/rules/prefer-exact-props.md) | Prefer exact proptype definitions |
+| | | [react/prefer-function-component](docs/rules/prefer-function-component.md) | Prefer function components over class components |
| | 🔧 | [react/prefer-read-only-props](docs/rules/prefer-read-only-props.md) | Require read-only props. |
| | | [react/prefer-stateless-function](docs/rules/prefer-stateless-function.md) | Enforce stateless components to be written as a pure function |
| ✔ | | [react/prop-types](docs/rules/prop-types.md) | Prevent missing props validation in a React component definition |
diff --git a/docs/rules/prefer-function-component.md b/docs/rules/prefer-function-component.md
new file mode 100644
index 0000000000..3ab706f3c2
--- /dev/null
+++ b/docs/rules/prefer-function-component.md
@@ -0,0 +1,89 @@
+# Prefer function components over class components (react/prefer-function-component)
+
+This rule prevents the use of class components.
+
+Since the addition of hooks, it has been possible to write stateful React components
+using only functions. Mixing both class and function components in a code base adds unnecessary hurdles for sharing reusable logic.
+
+By default, class components that use `componentDidCatch` are enabled because there is currently no hook alternative for React. This option is configurable via `allowComponentDidCatch`.
+
+## Rule Details
+
+This rule will flag any React class components that don't use `componentDidCatch`.
+
+Examples of **incorrect** code for this rule:
+
+```jsx
+import { Component } from 'react';
+
+class Foo extends Component {
+ render() {
+ return
{this.props.foo}
;
+ }
+}
+```
+
+Examples of **correct** code for this rule:
+
+```jsx
+const Foo = function (props) {
+ return {props.foo}
;
+};
+```
+
+```jsx
+const Foo = ({ foo }) => {foo}
;
+```
+
+## Rule Options
+
+```js
+...
+"prefer-function-component": [, { "allowComponentDidCatch": }]
+...
+```
+
+- `enabled`: for enabling the rule. 0=off, 1=warn, 2=error. Defaults to 0.
+- `allowComponentDidCatch`: optional boolean set to `true` if you would like to ignore class components using `componentDidCatch` (default to `true`).
+
+### `allowComponentDidCatch`
+
+When `true` (the default) the rule will ignore components that use `componentDidCatch`
+
+Examples of **correct** code for this rule:
+
+```jsx
+import { Component } from 'react';
+
+class Foo extends Component {
+ componentDidCatch(error, errorInfo) {
+ logErrorToMyService(error, errorInfo);
+ }
+
+ render() {
+ return {this.props.foo}
;
+ }
+}
+```
+
+When `false` the rule will also flag components that use `componentDidCatch`
+
+Examples of **incorrect** code for this rule:
+
+```jsx
+import { Component } from 'react';
+
+class Foo extends Component {
+ componentDidCatch(error, errorInfo) {
+ logErrorToMyService(error, errorInfo);
+ }
+
+ render() {
+ return {this.props.foo}
;
+ }
+}
+```
+
+### Related rules
+
+- [prefer-stateless-function](./prefer-stateless-function)
diff --git a/index.js b/index.js
index e89b0c8ea4..729623b766 100644
--- a/index.js
+++ b/index.js
@@ -82,6 +82,7 @@ const allRules = {
'no-will-update-set-state': require('./lib/rules/no-will-update-set-state'),
'prefer-es6-class': require('./lib/rules/prefer-es6-class'),
'prefer-exact-props': require('./lib/rules/prefer-exact-props'),
+ 'prefer-function-component': require('./lib/rules/prefer-function-component'),
'prefer-read-only-props': require('./lib/rules/prefer-read-only-props'),
'prefer-stateless-function': require('./lib/rules/prefer-stateless-function'),
'prop-types': require('./lib/rules/prop-types'),
diff --git a/lib/rules/prefer-function-component.js b/lib/rules/prefer-function-component.js
new file mode 100644
index 0000000000..01ba5f6a40
--- /dev/null
+++ b/lib/rules/prefer-function-component.js
@@ -0,0 +1,86 @@
+/**
+ * @fileoverview Enforce function components over class components
+ * @author Tate Thurston
+ */
+
+'use strict';
+
+const docsUrl = require('../util/docsUrl');
+const Components = require('../util/Components');
+const ast = require('../util/ast');
+
+const COMPONENT_SHOULD_BE_FUNCTION = 'componentShouldBeFunction';
+const ALLOW_COMPONENT_DID_CATCH = 'allowComponentDidCatch';
+const COMPONENT_DID_CATCH = 'componentDidCatch';
+
+module.exports = {
+ meta: {
+ docs: {
+ description: 'Enforce components are written as function components',
+ category: 'Stylistic Issues',
+ recommended: false,
+ suggestion: false,
+ url: docsUrl('prefer-function-component')
+ },
+ fixable: false,
+ type: 'problem',
+ messages: {
+ [COMPONENT_SHOULD_BE_FUNCTION]:
+ 'Class component should be written as a function'
+ },
+ schema: [
+ {
+ type: 'object',
+ properties: {
+ [ALLOW_COMPONENT_DID_CATCH]: {
+ default: true,
+ type: 'boolean'
+ }
+ },
+ additionalProperties: false
+ }
+ ]
+ },
+
+ create: Components.detect((context, components, utils) => {
+ const allowComponentDidCatchOption = context.options[0] && context.options[0].allowComponentDidCatch;
+ const allowComponentDidCatch = allowComponentDidCatchOption !== false;
+
+ function shouldPreferFunction(node) {
+ if (!allowComponentDidCatch) {
+ return true;
+ }
+
+ const properties = ast
+ .getComponentProperties(node)
+ .map(ast.getPropertyName);
+ return !properties.includes(COMPONENT_DID_CATCH);
+ }
+
+ const detect = (guard) => (node) => {
+ if (guard(node) && shouldPreferFunction(node)) {
+ components.set(node, {
+ [COMPONENT_SHOULD_BE_FUNCTION]: true
+ });
+ }
+ };
+
+ return {
+ ObjectExpression: detect(utils.isES5Component),
+ ClassDeclaration: detect(utils.isES6Component),
+ ClassExpression: detect(utils.isES6Component),
+
+ 'Program:exit'() {
+ const list = components.list();
+ Object.values(list).forEach((component) => {
+ if (component[COMPONENT_SHOULD_BE_FUNCTION]) {
+ context.report({
+ node: component.node,
+ messageId: COMPONENT_SHOULD_BE_FUNCTION
+ });
+ }
+ });
+ }
+ };
+ })
+};
diff --git a/tests/lib/rules/prefer-function-component.js b/tests/lib/rules/prefer-function-component.js
new file mode 100644
index 0000000000..2a7b4094ea
--- /dev/null
+++ b/tests/lib/rules/prefer-function-component.js
@@ -0,0 +1,208 @@
+'use strict';
+
+const RuleTester = require('eslint').RuleTester;
+const rule = require('../../../lib/rules/prefer-function-component');
+
+const COMPONENT_SHOULD_BE_FUNCTION = 'componentShouldBeFunction';
+const ALLOW_COMPONENT_DID_CATCH = 'allowComponentDidCatch';
+
+const ruleTester = new RuleTester({
+ parserOptions: {
+ ecmaVersion: 2018,
+ sourceType: 'module',
+ ecmaFeatures: {
+ jsx: true
+ }
+ },
+ settings: {
+ react: {
+ version: 'latest'
+ }
+ }
+});
+
+ruleTester.run('prefer-function-component', rule, {
+ valid: [
+ {
+ // Already a stateless function
+ code: `
+ const Foo = function(props) {
+ return {props.foo}
;
+ };
+ `
+ },
+ {
+ // Already a stateless (arrow) function
+ code: 'const Foo = ({foo}) => {foo}
;'
+ },
+ {
+ // Extends from Component and uses componentDidCatch
+ code: `
+ class Foo extends React.Component {
+ componentDidCatch(error, errorInfo) {
+ logErrorToMyService(error, errorInfo);
+ }
+ render() {
+ return {this.props.foo}
;
+ }
+ }
+ `
+ },
+ {
+ // Extends from Component and uses componentDidCatch
+ code: `
+ class Foo extends React.PureComponent {
+ componentDidCatch(error, errorInfo) {
+ logErrorToMyService(error, errorInfo);
+ }
+ render() {
+ return {this.props.foo}
;
+ }
+ }
+ `
+ },
+ {
+ // Extends from Component in an expression context.
+ code: `
+ const Foo = class extends React.Component {
+ componentDidCatch(error, errorInfo) {
+ logErrorToMyService(error, errorInfo);
+ }
+ render() {
+ return {this.props.foo}
;
+ }
+ };
+ `
+ }
+ ],
+
+ invalid: [
+ {
+ code: `
+ import { Component } from 'react';
+
+ class Foo extends Component {
+ render() {
+ return {this.props.foo}
;
+ }
+ }
+ `,
+ errors: [
+ {
+ messageId: COMPONENT_SHOULD_BE_FUNCTION
+ }
+ ]
+ },
+ {
+ code: `
+ class Foo extends React.Component {
+ render() {
+ return {this.props.foo}
;
+ }
+ }
+ `,
+ errors: [
+ {
+ messageId: COMPONENT_SHOULD_BE_FUNCTION
+ }
+ ]
+ },
+ {
+ code: `
+ class Foo extends React.PureComponent {
+ render() {
+ return {this.props.foo}
;
+ }
+ }
+ `,
+ errors: [
+ {
+ messageId: COMPONENT_SHOULD_BE_FUNCTION
+ }
+ ]
+ },
+ {
+ code: `
+ const Foo = class extends React.Component {
+ render() {
+ return {this.props.foo}
;
+ }
+ };
+ `,
+ errors: [
+ {
+ messageId: COMPONENT_SHOULD_BE_FUNCTION
+ }
+ ]
+ },
+ {
+ // Extends from Component and uses componentDidCatch
+ code: `
+ class Foo extends React.Component {
+ componentDidCatch(error, errorInfo) {
+ logErrorToMyService(error, errorInfo);
+ }
+ render() {
+ return {this.props.foo}
;
+ }
+ }
+ `,
+ options: [
+ {
+ [ALLOW_COMPONENT_DID_CATCH]: false
+ }
+ ],
+ errors: [
+ {
+ messageId: COMPONENT_SHOULD_BE_FUNCTION
+ }
+ ]
+ },
+ {
+ // Extends from Component and uses componentDidCatch
+ code: `
+ class Foo extends React.PureComponent {
+ componentDidCatch(error, errorInfo) {
+ logErrorToMyService(error, errorInfo);
+ }
+ render() {
+ return {this.props.foo}
;
+ }
+ }
+ `,
+ options: [
+ {
+ [ALLOW_COMPONENT_DID_CATCH]: false
+ }
+ ],
+ errors: [
+ {
+ messageId: COMPONENT_SHOULD_BE_FUNCTION
+ }
+ ]
+ },
+ {
+ // Extends from Component in an expression context.
+ code: `
+ const Foo = class extends React.Component {
+ componentDidCatch(error, errorInfo) {
+ logErrorToMyService(error, errorInfo);
+ }
+ render() {
+ return {this.props.foo}
;
+ }
+ };
+ `,
+ options: [
+ {
+ [ALLOW_COMPONENT_DID_CATCH]: false
+ }
+ ],
+ errors: [
+ {
+ messageId: COMPONENT_SHOULD_BE_FUNCTION
+ }
+ ]
+ }
+ ]
+});