Skip to content

Commit 890427c

Browse files
NotWoodsljharb
authored andcommitted
[New] forward-ref-uses-ref: add rule for checking ref parameter
1 parent 597553d commit 890427c

File tree

6 files changed

+410
-0
lines changed

6 files changed

+410
-0
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
1414
* version settings: Allow react defaultVersion to be configurable ([#3771][] @onlywei)
1515
* [`jsx-closing-tag-location`]: add `line-aligned` option ([#3777] @kimtaejin3)
1616
* [`no-danger`]: add `customComponentNames` option ([#3748][] @akulsr0)
17+
* [`forward-ref-uses-ref`]: add rule for checking ref parameter is added ([#3667][] @NotWoods)
1718

1819
### Changed
1920
* [Refactor] `variableUtil`: Avoid creating a single flat variable scope for each lookup ([#3782][] @DanielRosenwasser)
@@ -27,6 +28,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
2728
[#3748]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3748
2829
[#3724]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3724
2930
[#3694]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3694
31+
[#3694]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3667
3032

3133
### Fixed
3234
* [`no-invalid-html-attribute`]: substitute placeholders in suggestion messages ([#3759][] @mdjermanovic)

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 | | | | | |

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/latest/use/core-concepts#rule-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

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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: 'forwardRef is used with this component but no ref parameter is set',
18+
addRefParameter: 'Add a ref parameter',
19+
removeForwardRef: 'Remove forwardRef wrapper',
20+
};
21+
22+
module.exports = {
23+
meta: {
24+
docs: {
25+
description: 'Require all forwardRef components include a ref parameter',
26+
category: 'Possible Errors',
27+
recommended: false,
28+
url: docsUrl('forward-ref-uses-ref'),
29+
},
30+
messages,
31+
schema: [],
32+
type: 'suggestion',
33+
hasSuggestions: true,
34+
},
35+
36+
create(context) {
37+
const sourceCode = context.getSourceCode();
38+
/**
39+
* @param {ASTNode} node
40+
* @returns {boolean} If the node represents the identifier `forwardRef`.
41+
*/
42+
function isForwardRefIdentifier(node) {
43+
return node.type === 'Identifier' && node.name === 'forwardRef';
44+
}
45+
46+
/**
47+
* @param {ASTNode} node
48+
* @returns {boolean} If the node represents a function call `forwardRef()` or `React.forwardRef()`.
49+
*/
50+
function isForwardRefCall(node) {
51+
return (
52+
node.type === 'CallExpression'
53+
&& (isForwardRefIdentifier(node.callee)
54+
|| (node.callee.type === 'MemberExpression' && isForwardRefIdentifier(node.callee.property)))
55+
);
56+
}
57+
58+
return {
59+
'FunctionExpression, ArrowFunctionExpression'(node) {
60+
if (!isForwardRefCall(node.parent)) {
61+
return;
62+
}
63+
64+
if (node.params.length === 1) {
65+
report(context, messages.missingRefParameter, 'missingRefParameter', {
66+
node,
67+
suggest: [
68+
Object.assign(
69+
getMessageData('addRefParameter', messages.addRefParameter),
70+
{
71+
fix(fixer) {
72+
const param = node.params[0];
73+
// If using shorthand arrow function syntax, add parentheses around the new parameter pair
74+
const shouldAddParentheses = node.type === 'ArrowFunctionExpression' && !isParenthesized(context, param);
75+
return [].concat(
76+
shouldAddParentheses ? fixer.insertTextBefore(param, '(') : [],
77+
fixer.insertTextAfter(param, `, ref${shouldAddParentheses ? ')' : ''}`)
78+
);
79+
},
80+
}
81+
),
82+
Object.assign(
83+
getMessageData('removeForwardRef', messages.removeForwardRef),
84+
{
85+
fix: (fixer) => fixer.replaceText(node.parent, sourceCode.getText(node)),
86+
}
87+
),
88+
],
89+
});
90+
}
91+
},
92+
};
93+
},
94+
};

lib/rules/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ module.exports = {
1515
'forbid-elements': require('./forbid-elements'),
1616
'forbid-foreign-prop-types': require('./forbid-foreign-prop-types'),
1717
'forbid-prop-types': require('./forbid-prop-types'),
18+
'forward-ref-uses-ref': require('./forward-ref-uses-ref'),
1819
'function-component-definition': require('./function-component-definition'),
1920
'hook-use-state': require('./hook-use-state'),
2021
'iframe-missing-sandbox': require('./iframe-missing-sandbox'),

0 commit comments

Comments
 (0)