Skip to content

Commit a1273d5

Browse files
danreevesljharb
authored andcommitted
[New] no-unstable-nested-components: add propNamePattern to support custom render prop naming conventions
1 parent 3073214 commit a1273d5

File tree

4 files changed

+43
-10
lines changed

4 files changed

+43
-10
lines changed

Diff for: CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
1010
* add type generation ([#3830][] @voxpelli)
1111
* [`no-unescaped-entities`]: add suggestions ([#3831][] @StyleShit)
1212
* [`forbid-component-props`]: add `allowedForPatterns`/`disallowedForPatterns` options ([#3805][] @Efimenko)
13+
* [`no-unstable-nested-components`]: add `propNamePattern` to support custom render prop naming conventions ([#3826][] @danreeves)
1314

1415
[#3831]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3831
1516
[#3830]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3830
17+
[#3826]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3826
1618
[#3805]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3805
1719

1820
## [7.36.1] - 2024.09.12

Diff for: docs/rules/no-unstable-nested-components.md

+11
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ function Component() {
125125
{
126126
"allowAsProps": true | false,
127127
"customValidators": [] /* optional array of validators used for propTypes validation */
128+
"propNamePattern": string
128129
}
129130
]
130131
...
@@ -148,6 +149,16 @@ function Component() {
148149
}
149150
```
150151

152+
You can allow other render prop naming conventions by setting the `propNamePattern` option. By default this option is `"render*"`.
153+
154+
For example, if `propNamePattern` is set to `"*Renderer"` the following pattern is **not** considered warnings:
155+
156+
```jsx
157+
<Table
158+
rowRenderer={(rowData) => <Row data={rowData} />}
159+
/>
160+
```
161+
151162
## When Not To Use It
152163

153164
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 for: lib/rules/no-unstable-nested-components.js

+18-10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
'use strict';
77

8+
const minimatch = require('minimatch');
89
const Components = require('../util/Components');
910
const docsUrl = require('../util/docsUrl');
1011
const astUtil = require('../util/ast');
@@ -32,12 +33,13 @@ function generateErrorMessageWithParentName(parentName) {
3233
}
3334

3435
/**
35-
* Check whether given text starts with `render`. Comparison is case-sensitive.
36+
* Check whether given text matches the pattern passed in.
3637
* @param {string} text Text to validate
38+
* @param {string} pattern Pattern to match against
3739
* @returns {boolean}
3840
*/
39-
function startsWithRender(text) {
40-
return typeof text === 'string' && text.startsWith('render');
41+
function propMatchesRenderPropPattern(text, pattern) {
42+
return typeof text === 'string' && minimatch(text, pattern);
4143
}
4244

4345
/**
@@ -165,15 +167,16 @@ function isReturnStatementOfHook(node, context) {
165167
* ```
166168
* @param {ASTNode} node The AST node
167169
* @param {Context} context eslint context
170+
* @param {string} propNamePattern a pattern to match render props against
168171
* @returns {boolean} True if component is declared inside a render prop, false if not
169172
*/
170-
function isComponentInRenderProp(node, context) {
173+
function isComponentInRenderProp(node, context, propNamePattern) {
171174
if (
172175
node
173176
&& node.parent
174177
&& node.parent.type === 'Property'
175178
&& node.parent.key
176-
&& startsWithRender(node.parent.key.name)
179+
&& propMatchesRenderPropPattern(node.parent.key.name, propNamePattern)
177180
) {
178181
return true;
179182
}
@@ -202,7 +205,7 @@ function isComponentInRenderProp(node, context) {
202205
const propName = jsxExpressionContainer.parent.name.name;
203206

204207
// Starts with render, e.g. <Component renderFooter={() => <div />} />
205-
if (startsWithRender(propName)) {
208+
if (propMatchesRenderPropPattern(propName, propNamePattern)) {
206209
return true;
207210
}
208211

@@ -222,16 +225,17 @@ function isComponentInRenderProp(node, context) {
222225
* <Component rows={ [{ render: () => <div /> }] } />
223226
* ```
224227
* @param {ASTNode} node The AST node
228+
* @param {string} propNamePattern The pattern to match render props against
225229
* @returns {boolean} True if component is declared inside a render property, false if not
226230
*/
227-
function isDirectValueOfRenderProperty(node) {
231+
function isDirectValueOfRenderProperty(node, propNamePattern) {
228232
return (
229233
node
230234
&& node.parent
231235
&& node.parent.type === 'Property'
232236
&& node.parent.key
233237
&& node.parent.key.type === 'Identifier'
234-
&& startsWithRender(node.parent.key.name)
238+
&& propMatchesRenderPropPattern(node.parent.key.name, propNamePattern)
235239
);
236240
}
237241

@@ -277,13 +281,17 @@ module.exports = {
277281
allowAsProps: {
278282
type: 'boolean',
279283
},
284+
propNamePattern: {
285+
type: 'string',
286+
},
280287
},
281288
additionalProperties: false,
282289
}],
283290
},
284291

285292
create: Components.detect((context, components, utils) => {
286293
const allowAsProps = context.options.some((option) => option && option.allowAsProps);
294+
const propNamePattern = (context.options[0] || {}).propNamePattern || 'render*';
287295

288296
/**
289297
* Check whether given node is declared inside class component's render block
@@ -418,7 +426,7 @@ module.exports = {
418426

419427
if (
420428
// Support allowAsProps option
421-
(isDeclaredInsideProps && (allowAsProps || isComponentInRenderProp(node, context)))
429+
(isDeclaredInsideProps && (allowAsProps || isComponentInRenderProp(node, context, propNamePattern)))
422430

423431
// Prevent reporting components created inside Array.map calls
424432
|| isMapCall(node)
@@ -428,7 +436,7 @@ module.exports = {
428436
|| isReturnStatementOfHook(node, context)
429437

430438
// Do not mark objects containing render methods
431-
|| isDirectValueOfRenderProperty(node)
439+
|| isDirectValueOfRenderProperty(node, propNamePattern)
432440

433441
// Prevent reporting nested class components twice
434442
|| isInsideRenderMethod(node)

Diff for: tests/lib/rules/no-unstable-nested-components.js

+12
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,18 @@ ruleTester.run('no-unstable-nested-components', rule, {
580580
allowAsProps: true,
581581
}],
582582
},
583+
{
584+
code: `
585+
function ParentComponent() {
586+
return <Table
587+
rowRenderer={(rowData) => <Row data={data} />}
588+
/>
589+
}
590+
`,
591+
options: [{
592+
propNamePattern: '*Renderer',
593+
}],
594+
},
583595
/* TODO These minor cases are currently falsely marked due to component detection
584596
{
585597
code: `

0 commit comments

Comments
 (0)