Skip to content

Commit

Permalink
[New] no-unstable-nested-components: Prevent creating unstable comp…
Browse files Browse the repository at this point in the history
…onents inside components
  • Loading branch information
AriPerkkio committed Aug 14, 2020
1 parent 9abc71d commit a45f3fe
Show file tree
Hide file tree
Showing 5 changed files with 731 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -131,6 +131,7 @@ Enable the rules that you would like to use.
* [react/no-unescaped-entities](docs/rules/no-unescaped-entities.md): Detect unescaped HTML entities, which might represent malformed tags
* [react/no-unknown-property](docs/rules/no-unknown-property.md): Prevent usage of unknown DOM property (fixable)
* [react/no-unsafe](docs/rules/no-unsafe.md): Prevent usage of unsafe lifecycle methods
* [react/no-unstable-nested-components](docs/rules/no-unstable-nested-components.md): Prevent creating unstable components inside components
* [react/no-unused-prop-types](docs/rules/no-unused-prop-types.md): Prevent definitions of unused prop types
* [react/no-unused-state](docs/rules/no-unused-state.md): Prevent definition of unused state fields
* [react/no-will-update-set-state](docs/rules/no-will-update-set-state.md): Prevent usage of setState in componentWillUpdate
Expand Down
79 changes: 79 additions & 0 deletions docs/rules/no-unstable-nested-components.md
@@ -0,0 +1,79 @@
# Prevent creating unstable components inside components (react/no-unstable-nested-components)

Creating components inside components without memoization leads to unstable components. The nested component and all its children are recreated during each re-render. Given stateful children of the nested component will lose their state on each re-render.

React reconcilation performs element type comparison with [reference equality](https://github.com/facebook/react/blob/v16.13.1/packages/react-reconciler/src/ReactChildFiber.js#L407). The reference to the same element changes on each re-render when defining components inside the render block. This leads to complete recreation of the current node and all its children. As a result the virtual DOM has to do extra unnecessary work and [possible bugs are introduced](https://codepen.io/ariperkkio/pen/vYLodLB).

## Rule Details

The following patterns are considered warnings:

```jsx
function Component() {
function UnstableNestedComponent() {
return <div />;
}

return (
<div>
<UnstableNestedComponent />
</div>
);
}
```

```jsx
class Component extends React.Component {
render() {
function UnstableNestedComponent() {
return <div />;
}

return (
<div>
<UnstableNestedComponent />
</div>
);
}
}
```

The following patterns are **not** considered warnings:

```jsx
function OutsideDefinedComponent(props) {
return <div />;
}

function Component() {
return (
<div>
<OutsideDefinedComponent />
</div>
);
}
```

```jsx
function Component() {
const MemoizedNestedComponent = React.useCallback(() => <div />, []);

return (
<div>
<MemoizedNestedComponent />
</div>
);
}
```

## Rule Options

```js
...
"react/no-unstable-nested-components": "off" | "warn" | "error"
...
```

## When Not To Use It

If you are not interested in preventing bugs related to re-creation of the nested components or do not care about optimization of virtual DOM.
1 change: 1 addition & 0 deletions index.js
Expand Up @@ -74,6 +74,7 @@ const allRules = {
'no-unescaped-entities': require('./lib/rules/no-unescaped-entities'),
'no-unknown-property': require('./lib/rules/no-unknown-property'),
'no-unsafe': require('./lib/rules/no-unsafe'),
'no-unstable-nested-components': require('./lib/rules/no-unstable-nested-components'),
'no-unused-prop-types': require('./lib/rules/no-unused-prop-types'),
'no-unused-state': require('./lib/rules/no-unused-state'),
'no-will-update-set-state': require('./lib/rules/no-will-update-set-state'),
Expand Down
150 changes: 150 additions & 0 deletions lib/rules/no-unstable-nested-components.js
@@ -0,0 +1,150 @@
/**
* @fileoverview Prevent creating unstable components inside components
* @author Ari Perkkiö
*/

'use strict';

const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');

// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------

const ERROR_MESSAGE = 'Do not create unstable nested components. Declare them outside the component or memoize them.';

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

module.exports = {
meta: {
docs: {
description: 'Prevent creating unstable components inside components',
category: 'Possible Errors',
recommended: false,
url: docsUrl('no-unstable-nested-components')
},
schema: []
},

create: Components.detect((context, components, utils) => {
/**
* Check whether given node is declared inside another component
* @param {ASTNode} node The AST node being checked
* @returns {Boolean} True if node has a parent component, false if not
*/
function isAnyParentComponent(node) {
if (!node.parent || node.parent.type === 'Program') {
return false;
}

return components.get(node.parent) || isAnyParentComponent(node.parent);
}

/**
* Check whether given node is inside class component's render block
* ```jsx
* class Component extends React.Component {
* render() {
* class NestedClassComponent extends React.Component {
* ...
* ```
* @param {ASTNode} node The AST node being checked
* @returns {Boolean} True if node is inside class component's render block, false if not
*/
function isInsideRenderMethod(node) {
const parentComponent = utils.getParentComponent();
if (!parentComponent || parentComponent.type !== 'ClassDeclaration') {
return false;
}

return (
node.parent.type === 'MethodDefinition'
&& node.parent.key
&& node.parent.key.name === 'render'
);
}

/**
* Check whether current node is a stateless component declared inside class component.
* Util's component detection fails to detect stateless components inside class components.
* ```jsx
* class Component extends React.Component {
* render() {
* const NestedComponent = () => <div />;
* ...
* ```
* @returns {Boolean} True if current node a stateless component declared inside class component, false if not
*/
function isStatelessComponentInsideClassComponent() {
const parentComponent = utils.getParentComponent();
const parentStatelessComponent = utils.getParentStatelessComponent();

return (
parentComponent
&& parentStatelessComponent
&& parentComponent.type === 'ClassDeclaration'
&& utils.getStatelessComponent(parentStatelessComponent)
);
}

/**
* Check whether given node is declared inside component prop
* ```jsx
* <Component footer={() => <div />} />
* ```
* @param {ASTNode} node The AST node being checked
* @returns {Boolean} True if node is inside a component declared inside prop, false if not
*/
function isComponentInProp(node) {
if (node.parent.type !== 'JSXExpressionContainer') {
return false;
}

// Check whether component is a render prop
if (node.parent.parent && node.parent.parent.type === 'JSXElement') {
return false;
}

return utils.isReturningJSX(node);
}

/**
* Check whether given node is a unstable nested component
* @param {ASTNode} node The AST node being checked
*/
function validate(node) {
if (!node || !node.parent) return;

if (
!components.get(node)
&& !isStatelessComponentInsideClassComponent()
&& !isComponentInProp(node)
) {
return;
}

// Prevent reporting nested class components twice
if (isInsideRenderMethod(node)) {
return;
}

if (isAnyParentComponent(node)) {
context.report({node, message: ERROR_MESSAGE});
}
}

// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------

return {
FunctionDeclaration(node) { validate(node); },
ArrowFunctionExpression(node) { validate(node); },
FunctionExpression(node) { validate(node); },
ClassDeclaration(node) { validate(node); }
};
})
};

0 comments on commit a45f3fe

Please sign in to comment.