From 041321c8938014c0fb96b622da05871fadacce66 Mon Sep 17 00:00:00 2001 From: Tate Date: Wed, 11 Aug 2021 15:48:58 -0700 Subject: [PATCH 1/2] [New Rule] prefer-function-component --- README.md | 1 + docs/rules/prefer-function-component.md | 89 ++++++++ index.js | 1 + lib/rules/prefer-function-component.js | 86 ++++++++ tests/lib/rules/prefer-function-component.js | 208 +++++++++++++++++++ 5 files changed, 385 insertions(+) create mode 100644 docs/rules/prefer-function-component.md create mode 100644 lib/rules/prefer-function-component.js create mode 100644 tests/lib/rules/prefer-function-component.js 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..8a8d0f6d37 --- /dev/null +++ b/docs/rules/prefer-function-component.md @@ -0,0 +1,89 @@ +# Forbid 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 + } + ] + } + ] +}); From fbad4bce6175bff852ebe829b06cb5393e2f5b84 Mon Sep 17 00:00:00 2001 From: Tate Thurston Date: Wed, 11 Aug 2021 16:06:52 -0700 Subject: [PATCH 2/2] Update prefer-function-component.md --- docs/rules/prefer-function-component.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/prefer-function-component.md b/docs/rules/prefer-function-component.md index 8a8d0f6d37..3ab706f3c2 100644 --- a/docs/rules/prefer-function-component.md +++ b/docs/rules/prefer-function-component.md @@ -1,4 +1,4 @@ -# Forbid class Components (react/prefer-function-component) +# Prefer function components over class components (react/prefer-function-component) This rule prevents the use of class components.