From 89134ca9aae17031e0e6a90de8a6dbf953d9dc39 Mon Sep 17 00:00:00 2001 From: Ari Perkkio Date: Wed, 19 Aug 2020 20:28:13 +0300 Subject: [PATCH] [New] `no-unstable-nested-components`: Prevent creating unstable components inside components --- README.md | 1 + docs/rules/no-unstable-nested-components.md | 134 +++ index.js | 1 + lib/rules/no-unstable-nested-components.js | 310 ++++++ .../rules/no-unstable-nested-components.js | 974 ++++++++++++++++++ 5 files changed, 1420 insertions(+) create mode 100644 docs/rules/no-unstable-nested-components.md create mode 100644 lib/rules/no-unstable-nested-components.js create mode 100644 tests/lib/rules/no-unstable-nested-components.js diff --git a/README.md b/README.md index 2f09ce1e3c..c7c59913a1 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,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 diff --git a/docs/rules/no-unstable-nested-components.md b/docs/rules/no-unstable-nested-components.md new file mode 100644 index 0000000000..c19cb2aa13 --- /dev/null +++ b/docs/rules/no-unstable-nested-components.md @@ -0,0 +1,134 @@ +# 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
; + } + + return ( +
+ +
+ ); +} +``` + +```jsx +function SomeComponent({ footer: Footer }) { + return ( +
+
+
+ ); +} + +function Component() { + return ( +
+
} /> +
+ ); +} +``` + +```jsx +class Component extends React.Component { + render() { + function UnstableNestedComponent() { + return
; + } + + return ( +
+ +
+ ); + } +} +``` + +The following patterns are **not** considered warnings: + +```jsx +function OutsideDefinedComponent(props) { + return
; +} + +function Component() { + return ( +
+ +
+ ); +} +``` + +```jsx +function Component() { + const MemoizedNestedComponent = React.useCallback(() =>
, []); + + return ( +
+ +
+ ); +} +``` + +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
{props.renderFooter()}
; +} + +function Component() { + return ( +
+
} /> +
+ ); +} +``` + +## 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
{props.footer()}
; +} + +function Component() { + return ( +
+
} /> +
+ ); +} +``` + +## 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. diff --git a/index.js b/index.js index bc0c30a43d..e40300d7d5 100644 --- a/index.js +++ b/index.js @@ -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'), diff --git a/lib/rules/no-unstable-nested-components.js b/lib/rules/no-unstable-nested-components.js new file mode 100644 index 0000000000..7cfc2f74fe --- /dev/null +++ b/lib/rules/no-unstable-nested-components.js @@ -0,0 +1,310 @@ +/** + * @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/; + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +/** + * Generate error message with given parent component name + * @param {String} parentName Name of the parent component + * @returns {String} Error message with parent component name + */ +function generateErrorMessageWithParentName(parentName) { + return `Declare this component outside parent component "${parentName}" or memoize it.`; +} + +/** + * Get closest parent matching given matcher + * @param {ASTNode} node The AST node + * @param {Function} matcher Method used to match the parent + * @returns {ASTNode} The matching parent node, if any + */ +function getClosestMatchingParent(node, matcher) { + if (!node || !node.parent || node.parent.type === 'Program') { + return; + } + + if (matcher(node.parent)) { + return node.parent; + } + + return getClosestMatchingParent(node.parent, matcher); +} + +/** + * Matcher used to check whether given node is a `createElement` call + * @param {ASTNode} node The AST node + * @returns {Boolean} True if node is a `createElement` call, false if not + */ +function isCreateElementMatcher(node) { + return ( + node + && node.type === 'CallExpression' + && node.callee + && node.callee.property + && node.callee.property.name === 'createElement' + ); +} + +/** + * Matcher used to check whether given node is a `ObjectExpression` + * @param {ASTNode} node The AST node + * @returns {Boolean} True if node is a `ObjectExpression`, false if not + */ +function isObjectExpressionMatcher(node) { + return node && node.type === 'ObjectExpression'; +} + +/** + * 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 + ); +} + +// ------------------------------------------------------------------------------ +// 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); + + /** + * 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 given 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 = () =>
; + * ... + * ``` + * @param {ASTNode} node The AST node being checked + * @returns {Boolean} True if given 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 `createElement` call's props + * ```js + * React.createElement(Component, { + * footer: () => React.createElement("div", null) + * }) + * ``` + * @param {ASTNode} node The AST node + * @returns {Boolean} True if node is declare inside `createElement` call's props, false if not + */ + function isComponentInsideCreateElementsProp(node) { + if (!components.get(node)) { + return false; + } + + const createElementParent = getClosestMatchingParent(node, isCreateElementMatcher); + if ( + createElementParent + && createElementParent.arguments + && createElementParent.arguments[1] === getClosestMatchingParent(node, isObjectExpressionMatcher)) { + return true; + } + } + + /** + * Check whether given node is declared inside a component prop. + * ```jsx + *
} /> + * ``` + * @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 (node.parent.type !== 'JSXExpressionContainer') { + return isComponentInsideCreateElementsProp(node); + } + + return utils.isReturningJSX(node); + } + + /** + * Check whether given node is declared inside a render prop + * ```jsx + *
} /> + * {() =>
} + * ``` + * @param {ASTNode} node The AST node + * @returns {Boolean} True if component is declared inside a render prop, false if not + */ + function isComponentInRenderProp(node) { + if ( + node.parent.type === 'Property' + && node.parent.key + && RENDER_REGEXP.test(node.parent.key.name)) { + return true; + } + + // Check whether component is a render prop used as children, e.g. {() =>
} + const propNode = node.parent.parent; + if (propNode && propNode.type === 'JSXElement') { + return true; + } + + // Check whether prop name starts with render, e.g.
} /> + return ( + propNode + && propNode.type === 'JSXAttribute' + && propNode.name + && propNode.name.type === 'JSXIdentifier' + && RENDER_REGEXP.test(propNode.name.name) + ); + } + + /** + * 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; + } + + if (isDeclaredInsideProps && (allowAsProps || isComponentInRenderProp(node))) { + return; + } + + // Prevent reporting nested class components twice + if (isInsideRenderMethod(node)) { + return; + } + + // Get the closest parent component + const parentComponent = getClosestMatchingParent( + node, + (nodeToMatch) => components.get(nodeToMatch) + ); + + 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); } + }; + }) +}; diff --git a/tests/lib/rules/no-unstable-nested-components.js b/tests/lib/rules/no-unstable-nested-components.js new file mode 100644 index 0000000000..0b3ce2c04f --- /dev/null +++ b/tests/lib/rules/no-unstable-nested-components.js @@ -0,0 +1,974 @@ +/** + * @fileoverview Prevent creating unstable components inside components + * @author Ari Perkkiƶ + */ + +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const RuleTester = require('eslint').RuleTester; +const rule = require('../../../lib/rules/no-unstable-nested-components'); + +const parserOptions = { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + jsx: true + } +}; + +const ERROR_MESSAGE = 'Declare this component outside parent component "ParentComponent" or memoize it.'; +const ERROR_MESSAGE_WITHOUT_NAME = 'Declare this component outside parent component or memoize it.'; +const ERROR_MESSAGE_COMPONENT_AS_PROPS = `${ERROR_MESSAGE} If you want to allow component creation in props, set allowAsProps option to true.`; + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({parserOptions}); +ruleTester.run('no-unstable-nested-components', rule, { + valid: [ + { + code: ` + function OutsideDefinedFunctionComponent() { + return
; + } + + function ParentComponent() { + return ( +
+ +
+ ); + } + ` + }, + { + code: ` + function OutsideDefinedFunctionComponent() { + return React.createElement("div", null); + } + + function ParentComponent() { + return React.createElement( + "div", + null, + React.createElement(OutsideDefinedFunctionComponent, null) + ); + } + ` + }, + { + code: ` + const OutsideDefinedVariableComponent = () => { + return
; + } + + function ParentComponent() { + return ( +
+ +
+ ); + } + ` + }, + { + code: ` + const OutsideDefinedVariableComponent = () => { + return React.createElement("div", null); + }; + + function ParentComponent() { + return React.createElement( + "div", + null, + React.createElement(OutsideDefinedVariableComponent, null) + ); + } + ` + }, + { + code: ` + class OutsideDefinedClassComponent extends React.Component { + render() { + return
; + } + } + + function ParentComponent() { + return ( +
+ +
+ ); + } + ` + }, + { + code: ` + class OutsideDefinedClassComponent extends React.Component { + render() { + return React.createElement("div", null); + } + } + + function ParentComponent() { + return React.createElement( + "div", + null, + React.createElement(OutsideDefinedVariableComponent, null) + ); + } + ` + }, + { + code: ` + function OutsideDefinedComponentForProps() { + return
; + } + + function ComponentWithProps(props) { + return
; + } + + function ParentComponent() { + return ( + } + header={
} + /> + ); + } + ` + }, + { + code: ` + function OutsideDefinedComponentForProps() { + return React.createElement("div", null); + } + + function ComponentWithProps(props) { + return React.createElement("div", null); + } + + function ParentComponent() { + return React.createElement(ComponentWithProps, { + footer: React.createElement(OutsideDefinedComponentForProps, null), + header: React.createElement("div", null) + }); + } + ` + }, + { + code: ` + function ParentComponent() { + const MemoizedNestedVariableComponent = React.useCallback(() =>
, []); + + return ( +
+ +
+ ); + } + ` + }, + { + code: ` + function ParentComponent() { + const MemoizedNestedVariableComponent = React.useCallback( + () => React.createElement("div", null), + [] + ); + + return React.createElement( + "div", + null, + React.createElement(MemoizedNestedVariableComponent, null) + ); + } + ` + }, + { + code: ` + function ParentComponent() { + const MemoizedNestedFunctionComponent = React.useCallback( + function () { + return
; + }, + [] + ); + + return ( +
+ +
+ ); + } + ` + }, + { + code: ` + function ParentComponent() { + const MemoizedNestedVariableComponent = React.useCallback( + function () { + return React.createElement("div", null); + }, + [] + ); + + return React.createElement( + "div", + null, + React.createElement(MemoizedNestedVariableComponent, null) + ); + } + ` + }, + { + code: ` + function ParentComponent(props) { + // Should not interfere handler declarations + function onClick(event) { + props.onClick(event.target.value); + } + + function getOnHover() { + return function onHover(event) { + props.onHover(event.target); + } + } + + return ( +
+
+ ); + } + ` + }, + { + code: ` + function ParentComponent() { + function getComponent() { + return
; + } + + return ( +
+ {getComponent()} +
+ ); + } + ` + }, + { + code: ` + function ParentComponent() { + function getComponent() { + return React.createElement("div", null); + } + + return React.createElement("div", null, getComponent()); + } + ` + }, + { + code: ` + function RenderPropComponent(props) { + return props.render({}); + } + + function ParentComponent() { + return ( + + {() =>
} + + ); + } + ` + }, + { + code: ` + function RenderPropComponent(props) { + return props.render({}); + } + + function ParentComponent() { + return React.createElement( + RenderPropComponent, + null, + () => React.createElement("div", null) + ); + } + ` + }, + { + code: ` + function ParentComponent(props) { + return ( +
    + {props.items.map(item => ( +
  • + {item.name} +
  • + ))} +
+ ); + } + ` + }, + { + code: ` + function ParentComponent(props) { + return React.createElement( + "ul", + null, + props.items.map(() => + React.createElement( + "li", + { key: item.id }, + item.name + ) + ) + ) + } + ` + }, + { + code: ` + function ParentComponent(props) { + return ( +
    + {props.items.map(function Item(item) { + return ( +
  • + {item.name} +
  • + ); + })} +
+ ); + } + ` + }, + { + code: ` + function ParentComponent(props) { + return React.createElement( + "ul", + null, + props.items.map(function Item() { + return React.createElement( + "li", + { key: item.id }, + item.name + ); + }) + ); + } + ` + }, + { + code: ` + function createTestComponent(props) { + return ( +
+ ); + } + ` + }, + { + code: ` + function createTestComponent(props) { + return React.createElement("div", null); + } + ` + }, + { + code: ` + function ComponentWithProps(props) { + return
{props.footer()}
; + } + + function ParentComponent() { + return ( +
} /> + ); + } + `, + options: [{ + allowAsProps: true + }] + }, + { + code: ` + function ComponentWithProps(props) { + return React.createElement("div", null, props.footer()); + } + + function ParentComponent() { + return React.createElement(ComponentWithProps, { + footer: () => React.createElement("div", null) + }); + } + `, + options: [{ + allowAsProps: true + }] + }, + { + code: ` + function ComponentForProps(props) { + return
; + } + + function ParentComponent() { + return ( +
} /> + ); + } + ` + }, + { + code: ` + function ComponentForProps(props) { + return React.createElement("div", null); + } + + function ParentComponent() { + return React.createElement(ComponentForProps, { + renderFooter: () => React.createElement("div", null) + }); + } + ` + } + ], + + invalid: [ + { + code: ` + function ParentComponent() { + function UnstableNestedFunctionComponent() { + return
; + } + + return ( +
+ +
+ ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + function ParentComponent() { + function UnstableNestedFunctionComponent() { + return React.createElement("div", null); + } + + return React.createElement( + "div", + null, + React.createElement(UnstableNestedFunctionComponent, null) + ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + function ParentComponent() { + const UnstableNestedVariableComponent = () => { + return
; + } + + return ( +
+ +
+ ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + function ParentComponent() { + const UnstableNestedVariableComponent = () => { + return React.createElement("div", null); + } + + return React.createElement( + "div", + null, + React.createElement(UnstableNestedVariableComponent, null) + ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + const ParentComponent = () => { + function UnstableNestedFunctionComponent() { + return
; + } + + return ( +
+ +
+ ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + const ParentComponent = () => { + function UnstableNestedFunctionComponent() { + return React.createElement("div", null); + } + + return React.createElement( + "div", + null, + React.createElement(UnstableNestedFunctionComponent, null) + ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + export default () => { + function UnstableNestedFunctionComponent() { + return
; + } + + return ( +
+ +
+ ); + } + `, + errors: [{message: ERROR_MESSAGE_WITHOUT_NAME}] + }, + { + code: ` + export default () => { + function UnstableNestedFunctionComponent() { + return React.createElement("div", null); + } + + return React.createElement( + "div", + null, + React.createElement(UnstableNestedFunctionComponent, null) + ); + }; + `, + errors: [{message: ERROR_MESSAGE_WITHOUT_NAME}] + }, + { + code: ` + const ParentComponent = () => { + const UnstableNestedVariableComponent = () => { + return
; + } + + return ( +
+ +
+ ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + const ParentComponent = () => { + const UnstableNestedVariableComponent = () => { + return React.createElement("div", null); + } + + return React.createElement( + "div", + null, + React.createElement(UnstableNestedVariableComponent, null) + ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + function ParentComponent() { + class UnstableNestedClassComponent extends React.Component { + render() { + return
; + } + }; + + return ( +
+ +
+ ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + function ParentComponent() { + class UnstableNestedClassComponent extends React.Component { + render() { + return React.createElement("div", null); + } + } + + return React.createElement( + "div", + null, + React.createElement(UnstableNestedClassComponent, null) + ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + class ParentComponent extends React.Component { + render() { + class UnstableNestedClassComponent extends React.Component { + render() { + return
; + } + }; + + return ( +
+ +
+ ); + } + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + class ParentComponent extends React.Component { + render() { + class UnstableNestedClassComponent extends React.Component { + render() { + return React.createElement("div", null); + } + } + + return React.createElement( + "div", + null, + React.createElement(UnstableNestedClassComponent, null) + ); + } + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + class ParentComponent extends React.Component { + render() { + function UnstableNestedFunctionComponent() { + return
; + } + + return ( +
+ +
+ ); + } + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + class ParentComponent extends React.Component { + render() { + function UnstableNestedClassComponent() { + return React.createElement("div", null); + } + + return React.createElement( + "div", + null, + React.createElement(UnstableNestedClassComponent, null) + ); + } + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + class ParentComponent extends React.Component { + render() { + const UnstableNestedVariableComponent = () => { + return
; + } + + return ( +
+ +
+ ); + } + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + class ParentComponent extends React.Component { + render() { + const UnstableNestedClassComponent = () => { + return React.createElement("div", null); + } + + return React.createElement( + "div", + null, + React.createElement(UnstableNestedClassComponent, null) + ); + } + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + function ParentComponent() { + function getComponent() { + function NestedUnstableFunctionComponent() { + return
; + }; + + return ; + } + + return ( +
+ {getComponent()} +
+ ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + function ParentComponent() { + function getComponent() { + function NestedUnstableFunctionComponent() { + return React.createElement("div", null); + } + + return React.createElement(NestedUnstableFunctionComponent, null); + } + + return React.createElement("div", null, getComponent()); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + function ParentComponent() { + return ( +
+ {(() => { + function InlinedJSXComponent() { + return
; + } + + return ; + })()} +
+ ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + function ParentComponent() { + return React.createElement("div", null, function () { + function InlinedJSXComponent() { + return React.createElement("div", null); + } + + return React.createElement(InlinedJSXComponent, null); + }()); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + function ComponentWithProps(props) { + return
; + } + + function ParentComponent() { + return ( + ; + } + } /> + ); + } + `, + errors: [{message: ERROR_MESSAGE_COMPONENT_AS_PROPS}] + }, + { + code: ` + function ComponentWithProps(props) { + return React.createElement("div", null); + } + + function ParentComponent() { + return React.createElement(ComponentWithProps, { + footer: function SomeFooter() { + return React.createElement("div", null); + } + }); + } + `, + errors: [{message: ERROR_MESSAGE_COMPONENT_AS_PROPS}] + }, + { + code: ` + function ComponentWithProps(props) { + return
; + } + + function ParentComponent() { + return ( +
} /> + ); + } + `, + errors: [{message: ERROR_MESSAGE_COMPONENT_AS_PROPS}] + }, + { + code: ` + function ComponentWithProps(props) { + return React.createElement("div", null); + } + + function ParentComponent() { + return React.createElement(ComponentWithProps, { + footer: () => React.createElement("div", null) + }); + } + `, + errors: [{message: ERROR_MESSAGE_COMPONENT_AS_PROPS}] + }, + { + code: ` + function RenderPropComponent(props) { + return props.render({}); + } + + function ParentComponent() { + return ( + + {() => { + function UnstableNestedComponent() { + return
; + } + + return ( +
+ +
+ ); + }} + + ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + function RenderPropComponent(props) { + return props.render({}); + } + + function ParentComponent() { + return React.createElement( + RenderPropComponent, + null, + () => { + function UnstableNestedComponent() { + return React.createElement("div", null); + } + + return React.createElement( + "div", + null, + React.createElement(UnstableNestedComponent, null) + ); + } + ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + function ComponentForProps(props) { + return
; + } + + function ParentComponent() { + return ( +
} /> + ); + } + `, + errors: [{message: ERROR_MESSAGE_COMPONENT_AS_PROPS}] + }, + { + code: ` + function ComponentForProps(props) { + return React.createElement("div", null); + } + + function ParentComponent() { + return React.createElement(ComponentForProps, { + notPrefixedWithRender: () => React.createElement("div", null) + }); + } + `, + errors: [{message: ERROR_MESSAGE_COMPONENT_AS_PROPS}] + } + ] +});