Skip to content

Commit

Permalink
Support custom render prop naming conventions
Browse files Browse the repository at this point in the history
  • Loading branch information
danreeves committed Sep 17, 2024
1 parent 34d3728 commit 18a2d38
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 11 deletions.
13 changes: 12 additions & 1 deletion docs/rules/no-unstable-nested-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ function Component() {
"react/no-unstable-nested-components": [
"off" | "warn" | "error",
{
"allowAsProps": true | false
"allowAsProps": true | false,
"propNamePattern": string
}
]
...
Expand All @@ -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
<Table
rowRenderer={(rowData) => <Row data={rowData} />}
/>
```

## 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.
28 changes: 18 additions & 10 deletions lib/rules/no-unstable-nested-components.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -202,7 +205,7 @@ function isComponentInRenderProp(node, context) {
const propName = jsxExpressionContainer.parent.name.name;

// Starts with render, e.g. <Component renderFooter={() => <div />} />
if (startsWithRender(propName)) {
if (propMatchesRenderPropPattern(propName, propNamePattern)) {
return true;
}

Expand All @@ -222,16 +225,17 @@ function isComponentInRenderProp(node, context) {
* <Component rows={ [{ render: () => <div /> }] } />
* ```
* @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)
);
}

Expand Down Expand Up @@ -271,13 +275,17 @@ module.exports = {
allowAsProps: {
type: 'boolean',
},
propNamePattern: {
type: 'string',
},
},
additionalProperties: false,
}],
},

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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions tests/lib/rules/no-unstable-nested-components.js
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,18 @@ ruleTester.run('no-unstable-nested-components', rule, {
allowAsProps: true,
}],
},
{
code: `
function ParentComponent() {
return <Table
rowRenderer={(rowData) => <Row data={data} />}
/>
}
`,
options: [{
propNamePattern: '*Renderer',
}],
},
/* TODO These minor cases are currently falsely marked due to component detection
{
code: `
Expand Down

0 comments on commit 18a2d38

Please sign in to comment.