Skip to content

Commit ff4cc76

Browse files
committed
[New] forward-ref-uses-ref: add rule for checking ref parameter is added
1 parent 37d8bbe commit ff4cc76

File tree

6 files changed

+379
-0
lines changed

6 files changed

+379
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ module.exports = [
301301
| [forbid-elements](docs/rules/forbid-elements.md) | Disallow certain elements | | | | | |
302302
| [forbid-foreign-prop-types](docs/rules/forbid-foreign-prop-types.md) | Disallow using another component's propTypes | | | | | |
303303
| [forbid-prop-types](docs/rules/forbid-prop-types.md) | Disallow certain propTypes | | | | | |
304+
| [forward-ref-uses-ref](docs/rules/forward-ref-uses-ref.md) | Require all forwardRef components include a ref parameter | | | | 💡 | |
304305
| [function-component-definition](docs/rules/function-component-definition.md) | Enforce a specific function type for function components | | | 🔧 | | |
305306
| [hook-use-state](docs/rules/hook-use-state.md) | Ensure destructuring and symmetric naming of useState hook value and setter variables | | | | 💡 | |
306307
| [iframe-missing-sandbox](docs/rules/iframe-missing-sandbox.md) | Enforce sandbox attribute on iframe elements | | | | | |

configs/recommended.js

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module.exports = Object.assign({}, all, {
66
languageOptions: all.languageOptions,
77
rules: {
88
'react/display-name': 2,
9+
'react/forward-ref-uses-ref': 2,
910
'react/jsx-key': 2,
1011
'react/jsx-no-comment-textnodes': 2,
1112
'react/jsx-no-duplicate-props': 2,

docs/rules/forward-ref-uses-ref.md

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Require all forwardRef components include a ref parameter (`react/forward-ref-uses-ref`)
2+
3+
💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
4+
5+
<!-- end auto-generated rule header -->
6+
7+
Requires that components wrapped with `forwardRef` must have a `ref` parameter. Omitting the `ref` argument is usually a bug, and components not using `ref` don't need to be wrapped by `forwardRef`.
8+
9+
See <https://react.dev/reference/react/forwardRef>
10+
11+
## Rule Details
12+
13+
This rule checks all React components using `forwardRef` and verifies that there is a second parameter.
14+
15+
The following patterns are considered warnings:
16+
17+
```jsx
18+
var React = require('react');
19+
20+
var Component = React.forwardRef((props) => (
21+
<div />
22+
));
23+
```
24+
25+
The following patterns are **not** considered warnings:
26+
27+
```jsx
28+
var React = require('react');
29+
30+
var Component = React.forwardRef((props, ref) => (
31+
<div ref={ref} />
32+
));
33+
34+
var Component = React.forwardRef((props, ref) => (
35+
<div />
36+
));
37+
38+
var Component = (props) => (
39+
<div />
40+
);
41+
```
42+
43+
## When not to use
44+
45+
If you don't want to enforce that components using `forwardRef` utilize the forwarded ref.

lib/rules/forward-ref-uses-ref.js

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* @fileoverview Require all forwardRef components include a ref parameter
3+
*/
4+
5+
'use strict';
6+
7+
const isParenthesized = require('../util/ast').isParenthesized;
8+
const docsUrl = require('../util/docsUrl');
9+
const report = require('../util/report');
10+
const getMessageData = require('../util/message');
11+
12+
// ------------------------------------------------------------------------------
13+
// Rule Definition
14+
// ------------------------------------------------------------------------------
15+
16+
const messages = {
17+
missingRefParameter:
18+
'forwardRef is used with this component but no ref parameter is set',
19+
addRefParameter: 'Add a ref parameter',
20+
removeForwardRef: 'Remove forwardRef wrapper',
21+
};
22+
23+
module.exports = {
24+
meta: {
25+
docs: {
26+
description: 'Require all forwardRef components include a ref parameter',
27+
category: 'Possible Errors',
28+
recommended: true,
29+
url: docsUrl('forward-ref-uses-ref'),
30+
},
31+
messages,
32+
schema: [],
33+
type: 'suggestion',
34+
hasSuggestions: true,
35+
},
36+
37+
create(context) {
38+
const sourceCode = context.sourceCode;
39+
/**
40+
* @param {ASTNode} node
41+
* @returns {boolean} If the node represents the identifier `forwardRef`.
42+
*/
43+
function isForwardRefIdentifier(node) {
44+
return node.type === 'Identifier' && node.name === 'forwardRef';
45+
}
46+
47+
/**
48+
* @param {ASTNode} node
49+
* @returns {boolean} If the node represents a function call `forwardRef()` or `React.forwardRef()`.
50+
*/
51+
function isForwardRefCall(node) {
52+
return (
53+
node.type === 'CallExpression'
54+
&& (isForwardRefIdentifier(node.callee)
55+
|| (node.callee.type === 'MemberExpression'
56+
&& isForwardRefIdentifier(node.callee.property)))
57+
);
58+
}
59+
60+
return {
61+
'FunctionExpression, ArrowFunctionExpression'(node) {
62+
if (!isForwardRefCall(node.parent)) {
63+
return;
64+
}
65+
66+
if (node.params.length === 1) {
67+
report(context, messages.missingRefParameter, 'missingRefParameter', {
68+
node,
69+
suggest: [
70+
Object.assign(
71+
getMessageData('addRefParameter', messages.addRefParameter),
72+
{
73+
* fix(fixer) {
74+
const param = node.params[0];
75+
if (
76+
node.type === 'ArrowFunctionExpression'
77+
&& !isParenthesized(context, param)
78+
) {
79+
yield fixer.insertTextBefore(param, '(');
80+
yield fixer.insertTextAfter(param, ')');
81+
}
82+
83+
yield fixer.insertTextAfter(param, ', ref');
84+
},
85+
}
86+
),
87+
Object.assign(
88+
getMessageData('removeForwardRef', messages.removeForwardRef),
89+
{
90+
fix: (fixer) => fixer.replaceText(node.parent, sourceCode.getText(node)),
91+
}
92+
),
93+
],
94+
});
95+
}
96+
},
97+
};
98+
},
99+
};

lib/rules/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ module.exports = {
1313
'forbid-elements': require('./forbid-elements'),
1414
'forbid-foreign-prop-types': require('./forbid-foreign-prop-types'),
1515
'forbid-prop-types': require('./forbid-prop-types'),
16+
'forward-ref-uses-ref': require('./forward-ref-uses-ref'),
1617
'function-component-definition': require('./function-component-definition'),
1718
'hook-use-state': require('./hook-use-state'),
1819
'iframe-missing-sandbox': require('./iframe-missing-sandbox'),

0 commit comments

Comments
 (0)