From 18a2d38207dc23ffefb82e9e24d7a4514e10b9fd Mon Sep 17 00:00:00 2001 From: Dan Reeves Date: Mon, 16 Sep 2024 16:13:47 +0100 Subject: [PATCH] Support custom render prop naming conventions --- docs/rules/no-unstable-nested-components.md | 13 ++++++++- lib/rules/no-unstable-nested-components.js | 28 ++++++++++++------- .../rules/no-unstable-nested-components.js | 12 ++++++++ 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/docs/rules/no-unstable-nested-components.md b/docs/rules/no-unstable-nested-components.md index 8f8bda3cc8..c7be87f1fa 100644 --- a/docs/rules/no-unstable-nested-components.md +++ b/docs/rules/no-unstable-nested-components.md @@ -123,7 +123,8 @@ function Component() { "react/no-unstable-nested-components": [ "off" | "warn" | "error", { - "allowAsProps": true | false + "allowAsProps": true | false, + "propNamePattern": string } ] ... @@ -147,6 +148,16 @@ function Component() { } ``` +You can allow other render prop naming conventions by setting the `propNamePattern` option. By default this option is `"render*"`. + +For example, if `propNamePattern` is set to `"*Renderer"` the following pattern is **not** considered warnings: + +```jsx +} +/> +``` + ## 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/lib/rules/no-unstable-nested-components.js b/lib/rules/no-unstable-nested-components.js index 094d863133..b8f44b171f 100644 --- a/lib/rules/no-unstable-nested-components.js +++ b/lib/rules/no-unstable-nested-components.js @@ -5,6 +5,7 @@ 'use strict'; +const minimatch = require('minimatch'); const Components = require('../util/Components'); const docsUrl = require('../util/docsUrl'); const astUtil = require('../util/ast'); @@ -32,12 +33,13 @@ function generateErrorMessageWithParentName(parentName) { } /** - * Check whether given text starts with `render`. Comparison is case-sensitive. + * Check whether given text matches the pattern passed in. * @param {string} text Text to validate + * @param {string} pattern Pattern to match against * @returns {boolean} */ -function startsWithRender(text) { - return typeof text === 'string' && text.startsWith('render'); +function propMatchesRenderPropPattern(text, pattern) { + return typeof text === 'string' && minimatch(text, pattern); } /** @@ -165,15 +167,16 @@ function isReturnStatementOfHook(node, context) { * ``` * @param {ASTNode} node The AST node * @param {Context} context eslint context + * @param {string} propNamePattern a pattern to match render props against * @returns {boolean} True if component is declared inside a render prop, false if not */ -function isComponentInRenderProp(node, context) { +function isComponentInRenderProp(node, context, propNamePattern) { if ( node && node.parent && node.parent.type === 'Property' && node.parent.key - && startsWithRender(node.parent.key.name) + && propMatchesRenderPropPattern(node.parent.key.name, propNamePattern) ) { return true; } @@ -202,7 +205,7 @@ function isComponentInRenderProp(node, context) { const propName = jsxExpressionContainer.parent.name.name; // Starts with render, e.g.
} /> - if (startsWithRender(propName)) { + if (propMatchesRenderPropPattern(propName, propNamePattern)) { return true; } @@ -222,16 +225,17 @@ function isComponentInRenderProp(node, context) { *
}] } /> * ``` * @param {ASTNode} node The AST node + * @param {string} propNamePattern The pattern to match render props against * @returns {boolean} True if component is declared inside a render property, false if not */ -function isDirectValueOfRenderProperty(node) { +function isDirectValueOfRenderProperty(node, propNamePattern) { return ( node && node.parent && node.parent.type === 'Property' && node.parent.key && node.parent.key.type === 'Identifier' - && startsWithRender(node.parent.key.name) + && propMatchesRenderPropPattern(node.parent.key.name, propNamePattern) ); } @@ -271,6 +275,9 @@ module.exports = { allowAsProps: { type: 'boolean', }, + propNamePattern: { + type: 'string', + }, }, additionalProperties: false, }], @@ -278,6 +285,7 @@ module.exports = { create: Components.detect((context, components, utils) => { const allowAsProps = context.options.some((option) => option && option.allowAsProps); + const propNamePattern = (context.options[0] || {}).propNamePattern || 'render*'; /** * Check whether given node is declared inside class component's render block @@ -412,7 +420,7 @@ module.exports = { if ( // Support allowAsProps option - (isDeclaredInsideProps && (allowAsProps || isComponentInRenderProp(node, context))) + (isDeclaredInsideProps && (allowAsProps || isComponentInRenderProp(node, context, propNamePattern))) // Prevent reporting components created inside Array.map calls || isMapCall(node) @@ -422,7 +430,7 @@ module.exports = { || isReturnStatementOfHook(node, context) // Do not mark objects containing render methods - || isDirectValueOfRenderProperty(node) + || isDirectValueOfRenderProperty(node, propNamePattern) // Prevent reporting nested class components twice || isInsideRenderMethod(node) diff --git a/tests/lib/rules/no-unstable-nested-components.js b/tests/lib/rules/no-unstable-nested-components.js index 3e20ad648a..8a9c0aad47 100644 --- a/tests/lib/rules/no-unstable-nested-components.js +++ b/tests/lib/rules/no-unstable-nested-components.js @@ -580,6 +580,18 @@ ruleTester.run('no-unstable-nested-components', rule, { allowAsProps: true, }], }, + { + code: ` + function ParentComponent() { + return
} + /> + } + `, + options: [{ + propNamePattern: '*Renderer', + }], + }, /* TODO These minor cases are currently falsely marked due to component detection { code: `