Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[New]
no-unstable-nested-components
: Prevent creating unstable comp…
…onents inside components
- Loading branch information
1 parent
9abc71d
commit 35f073e
Showing
5 changed files
with
949 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
# 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 | ||
|
||
function SomeComponent({ footer: Footer }) { | ||
return ( | ||
<div> | ||
<Footer /> | ||
</div> | ||
); | ||
} | ||
|
||
function Component() { | ||
return ( | ||
<div> | ||
<SomeComponent footer={() => <div />} /> | ||
</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> | ||
); | ||
} | ||
``` | ||
|
||
By default component creation is allowed inside component props only if prop name starts with `render`. See `allowAsProps` option for disabling this limitation completely. | ||
|
||
```jsx | ||
function SomeComponent(props) { | ||
return <div>{props.renderFooter()}</div>; | ||
} | ||
|
||
function Component() { | ||
return ( | ||
<div> | ||
<SomeComponent renderFooter={() => <div />} /> | ||
</div> | ||
); | ||
} | ||
``` | ||
|
||
## Rule Options | ||
|
||
```js | ||
... | ||
"react/no-unstable-nested-components": [ | ||
"off" | "warn" | "error", | ||
{ "allowAsProps": true | false } | ||
] | ||
... | ||
``` | ||
|
||
You can allow component creation inside component props by setting `allowAsProps` option to true. When using this option make sure you are **calling** the props in the receiving component and not using them as elements. | ||
|
||
The following patterns are **not** considered warnings: | ||
|
||
```jsx | ||
function SomeComponent(props) { | ||
return <div>{props.footer()}</div>; | ||
} | ||
|
||
function Component() { | ||
return ( | ||
<div> | ||
<SomeComponent footer={() => <div />} /> | ||
</div> | ||
); | ||
} | ||
``` | ||
|
||
## 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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,227 @@ | ||
/** | ||
* @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_WITHOUT_NAME = 'Declare this component outside parent component or memoize it.'; | ||
const COMPONENT_AS_PROPS_INFO = ' If you want to allow component creation in props, set allowAsProps option to true.'; | ||
const RENDER_REGEXP = /^render/; | ||
|
||
function generateErrorMessageWithParentName(parentName) { | ||
return `Declare this component outside parent component "${parentName}" or memoize it.`; | ||
} | ||
|
||
// ------------------------------------------------------------------------------ | ||
// 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: [{ | ||
type: 'object', | ||
properties: { | ||
customValidators: { | ||
type: 'array', | ||
items: { | ||
type: 'string' | ||
} | ||
}, | ||
allowAsProps: { | ||
type: 'boolean' | ||
} | ||
}, | ||
additionalProperties: false | ||
}] | ||
}, | ||
|
||
create: Components.detect((context, components, utils) => { | ||
const allowAsProps = context.options.some((option) => option && option.allowAsProps); | ||
|
||
/** | ||
* Get the closest parent component of the given node if any exist | ||
* @param {ASTNode} node The AST node | ||
* @returns {ASTNode} Node of the closest parent component, if any. | ||
*/ | ||
function getClosestParentComponentNode(node) { | ||
if (!node || !node.parent || node.parent.type === 'Program') { | ||
return; | ||
} | ||
|
||
if (components.get(node.parent)) { | ||
return node.parent; | ||
} | ||
|
||
return getClosestParentComponentNode(node.parent); | ||
} | ||
|
||
/** | ||
* Resolve the component name of given node | ||
* @param {ASTNode} node The AST node of the component | ||
* @returns {String} Name of the component, if any | ||
*/ | ||
function resolveComponentName(node) { | ||
const parentName = node.id && node.id.name; | ||
if (parentName) return parentName; | ||
|
||
return ( | ||
node.type === 'ArrowFunctionExpression' | ||
&& node.parent | ||
&& node.parent.id | ||
&& node.parent.id.name | ||
); | ||
} | ||
|
||
/** | ||
* Check whether given node is declared 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 function component declared inside class component. | ||
* Util's component detection fails to detect function components inside class components. | ||
* ```jsx | ||
* class Component extends React.Component { | ||
* render() { | ||
* const NestedComponent = () => <div />; | ||
* ... | ||
* ``` | ||
* @param {ASTNode} node The AST node being checked | ||
* @returns {Boolean} True if current node a function component declared inside class component, false if not | ||
*/ | ||
function isFunctionComponentInsideClassComponent(node) { | ||
const parentComponent = utils.getParentComponent(); | ||
const parentStatelessComponent = utils.getParentStatelessComponent(); | ||
|
||
return ( | ||
parentComponent | ||
&& parentStatelessComponent | ||
&& parentComponent.type === 'ClassDeclaration' | ||
&& utils.getStatelessComponent(parentStatelessComponent) | ||
&& utils.isReturningJSX(node) | ||
); | ||
} | ||
|
||
/** | ||
* Check whether given node is declared inside a component prop. | ||
* Props prefixed with `render` are allowed. All prop names can be allowed with `allowAsProps` option. | ||
* ```jsx | ||
* <Component footer={() => <div />} /> | ||
* ``` | ||
* @param {ASTNode} node The AST node being checked | ||
* @returns {Boolean} True if node is a component declared inside prop, false if not | ||
*/ | ||
function isComponentInProp(node) { | ||
if (allowAsProps || node.parent.type !== 'JSXExpressionContainer') { | ||
return false; | ||
} | ||
|
||
// Check whether component is a render prop, e.g. <Component>{() => <div />}</Component> | ||
const propNode = node.parent.parent; | ||
if (propNode && propNode.type === 'JSXElement') { | ||
return false; | ||
} | ||
|
||
// Check whether prop name starts with render, e.g. <Component renderFooter={() => <div />} /> | ||
if ( | ||
propNode | ||
&& propNode.type === 'JSXAttribute' | ||
&& propNode.name | ||
&& propNode.name.type === 'JSXIdentifier' | ||
&& RENDER_REGEXP.test(propNode.name.name)) { | ||
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; | ||
} | ||
|
||
const isDeclaredInsideProps = isComponentInProp(node); | ||
if ( | ||
!components.get(node) | ||
&& !isFunctionComponentInsideClassComponent(node) | ||
&& !isDeclaredInsideProps) { | ||
return; | ||
} | ||
|
||
// Prevent reporting nested class components twice | ||
if (isInsideRenderMethod(node)) { | ||
return; | ||
} | ||
|
||
const parentComponent = getClosestParentComponentNode(node); | ||
if (parentComponent) { | ||
const parentName = resolveComponentName(parentComponent); | ||
|
||
// Exclude lowercase parents, e.g. function createTestComponent() | ||
// React-dom prevents creating lowercase components | ||
if (parentName && parentName[0] === parentName[0].toLowerCase()) { | ||
return; | ||
} | ||
|
||
let message = parentName | ||
? generateErrorMessageWithParentName(parentName) | ||
: ERROR_MESSAGE_WITHOUT_NAME; | ||
|
||
// Add information about allowAsProps option when component is declared inside prop | ||
if (isDeclaredInsideProps && !allowAsProps) { | ||
message += COMPONENT_AS_PROPS_INFO; | ||
} | ||
|
||
context.report({node, message}); | ||
} | ||
} | ||
|
||
// -------------------------------------------------------------------------- | ||
// Public | ||
// -------------------------------------------------------------------------- | ||
|
||
return { | ||
FunctionDeclaration(node) { validate(node); }, | ||
ArrowFunctionExpression(node) { validate(node); }, | ||
FunctionExpression(node) { validate(node); }, | ||
ClassDeclaration(node) { validate(node); } | ||
}; | ||
}) | ||
}; |
Oops, something went wrong.