Skip to content

Commit 54bd15d

Browse files
authored
feat(plugins/x): add 'no-forward-ref' (#870)
1 parent 8f8ba13 commit 54bd15d

File tree

10 files changed

+246
-0
lines changed

10 files changed

+246
-0
lines changed

packages/plugins/eslint-plugin-react-x/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export default [
4848
"react-x/no-default-props": "error",
4949
"react-x/no-direct-mutation-state": "error",
5050
"react-x/no-duplicate-key": "error",
51+
"react-x/no-forward-ref": "warn",
5152
"react-x/no-implicit-key": "warn",
5253
"react-x/no-missing-key": "error",
5354
"react-x/no-nested-components": "warn",
@@ -97,6 +98,7 @@ export default [
9798
| `no-default-props` | Prevents using `defaultProps` property in favor of ES6 default parameters. | ✔️ | | |
9899
| `no-direct-mutation-state` | Prevents direct mutation of `this.state`. | ✔️ | | |
99100
| `no-duplicate-key` | Prevents duplicate `key` on elements in the same array or a list of `children`. | ✔️ | | |
101+
| `no-forward-ref` | Prevents using `forwardRef`. | 🧐 | | |
100102
| `no-implicit-key` | Prevents `key` from not being explicitly specified (e.g. spreading `key` from objects). | 🧐 | | |
101103
| `no-leaked-conditional-rendering` | Prevents problematic leaked values from being rendered. | 🧐 | 💭 | |
102104
| `no-missing-component-display-name` | Enforces that all components have a `displayName` which can be used in devtools. | 🐞 | | |

packages/plugins/eslint-plugin-react-x/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import noCreateRef from "./rules/no-create-ref";
2626
import noDefaultProps from "./rules/no-default-props";
2727
import noDirectMutationState from "./rules/no-direct-mutation-state";
2828
import noDuplicateKey from "./rules/no-duplicate-key";
29+
import noForwardRef from "./rules/no-forward-ref";
2930
import noImplicitKey from "./rules/no-implicit-key";
3031
import noLeakedConditionalRendering from "./rules/no-leaked-conditional-rendering";
3132
import noMissingComponentDisplayName from "./rules/no-missing-component-display-name";
@@ -93,6 +94,7 @@ export default {
9394
"no-default-props": noDefaultProps,
9495
"no-direct-mutation-state": noDirectMutationState,
9596
"no-duplicate-key": noDuplicateKey,
97+
"no-forward-ref": noForwardRef,
9698
"no-implicit-key": noImplicitKey,
9799
"no-leaked-conditional-rendering": noLeakedConditionalRendering,
98100
"no-missing-component-display-name": noMissingComponentDisplayName,
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { ruleTester } from "../../../../../test";
2+
import rule, { RULE_NAME } from "./no-forward-ref";
3+
4+
ruleTester.run(RULE_NAME, rule, {
5+
invalid: [
6+
{
7+
code: /* tsx */ `
8+
import { forwardRef } from 'react'
9+
forwardRef((props) => {
10+
return null;
11+
});
12+
`,
13+
errors: [{ messageId: "noForwardRef" }],
14+
settings: {
15+
"react-x": {
16+
version: "19.0.0",
17+
},
18+
},
19+
},
20+
{
21+
code: /* tsx */ `
22+
import { forwardRef } from 'react'
23+
forwardRef((props) => null);
24+
`,
25+
errors: [{ messageId: "noForwardRef" }],
26+
settings: {
27+
"react-x": {
28+
version: "19.0.0",
29+
},
30+
},
31+
},
32+
{
33+
code: /* tsx */ `
34+
import { forwardRef } from 'react'
35+
forwardRef(function (props) {
36+
return null;
37+
});
38+
`,
39+
errors: [{ messageId: "noForwardRef" }],
40+
settings: {
41+
"react-x": {
42+
version: "19.0.0",
43+
},
44+
},
45+
},
46+
{
47+
code: /* tsx */ `
48+
import { forwardRef } from 'react'
49+
forwardRef(function Component(props) {
50+
return null;
51+
});
52+
`,
53+
errors: [{ messageId: "noForwardRef" }],
54+
settings: {
55+
"react-x": {
56+
version: "19.0.0",
57+
},
58+
},
59+
},
60+
{
61+
code: /* tsx */ `
62+
import * as React from 'react'
63+
React.forwardRef((props) => {
64+
return null;
65+
});
66+
`,
67+
errors: [{ messageId: "noForwardRef" }],
68+
settings: {
69+
"react-x": {
70+
version: "19.0.0",
71+
},
72+
},
73+
},
74+
{
75+
code: /* tsx */ `
76+
import * as React from 'react'
77+
React.forwardRef((props) => null);
78+
`,
79+
errors: [{ messageId: "noForwardRef" }],
80+
settings: {
81+
"react-x": {
82+
version: "19.0.0",
83+
},
84+
},
85+
},
86+
{
87+
code: /* tsx */ `
88+
import * as React from 'react'
89+
React.forwardRef(function (props) {
90+
return null;
91+
});
92+
`,
93+
errors: [{ messageId: "noForwardRef" }],
94+
settings: {
95+
"react-x": {
96+
version: "19.0.0",
97+
},
98+
},
99+
},
100+
{
101+
code: /* tsx */ `
102+
import * as React from 'react'
103+
React.forwardRef(function Component(props) {
104+
return null;
105+
});
106+
`,
107+
errors: [{ messageId: "noForwardRef" }],
108+
settings: {
109+
"react-x": {
110+
version: "19.0.0",
111+
},
112+
},
113+
},
114+
],
115+
valid: [
116+
{
117+
code: /* tsx */ `
118+
import * as React from 'react'
119+
React.forwardRef(function Component(props) {
120+
return null;
121+
});
122+
`,
123+
settings: {
124+
"react-x": {
125+
version: "18.3.1",
126+
},
127+
},
128+
},
129+
{
130+
code: /* tsx */ `
131+
import * as React from 'react'
132+
133+
const Component = React.forwardRef((props, ref) => {
134+
return null;
135+
});
136+
`,
137+
settings: {
138+
"react-x": {
139+
version: "18.3.1",
140+
},
141+
},
142+
},
143+
/* tsx */ `
144+
import * as React from 'react'
145+
146+
const Component = ({ ref }) => {
147+
return null;
148+
};
149+
`,
150+
/* tsx */ `
151+
import * as React from 'react'
152+
153+
const Component = ({ ref, ...props }) => {
154+
return null;
155+
};
156+
`,
157+
],
158+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { isForwardRefCall } from "@eslint-react/core";
2+
import { decodeSettings, normalizeSettings } from "@eslint-react/shared";
3+
import { compare } from "compare-versions";
4+
import type { CamelCase } from "string-ts";
5+
6+
import { createRule } from "../utils";
7+
8+
export const RULE_NAME = "no-forward-ref";
9+
10+
export type MessageID = CamelCase<typeof RULE_NAME>;
11+
12+
export default createRule<[], MessageID>({
13+
meta: {
14+
type: "problem",
15+
docs: {
16+
description: "disallow the use of 'forwardRef'",
17+
},
18+
messages: {
19+
noForwardRef: "In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead.",
20+
},
21+
schema: [],
22+
},
23+
name: RULE_NAME,
24+
create(context) {
25+
const { version } = normalizeSettings(decodeSettings(context.settings));
26+
if (compare(version, "19.0.0", "<")) return {};
27+
return {
28+
CallExpression(node) {
29+
if (!isForwardRefCall(node, context)) return;
30+
context.report({
31+
messageId: "noForwardRef",
32+
node,
33+
});
34+
},
35+
};
36+
},
37+
defaultOptions: [],
38+
});

packages/plugins/eslint-plugin/src/configs/all.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export const rules = {
3434
"@eslint-react/no-default-props": "error",
3535
"@eslint-react/no-direct-mutation-state": "error",
3636
"@eslint-react/no-duplicate-key": "error",
37+
"@eslint-react/no-forward-ref": "warn",
3738
"@eslint-react/no-implicit-key": "warn",
3839
"@eslint-react/no-missing-component-display-name": "warn",
3940
"@eslint-react/no-missing-key": "error",

packages/plugins/eslint-plugin/src/configs/core.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export const rules = {
2323
"@eslint-react/no-default-props": "error",
2424
"@eslint-react/no-direct-mutation-state": "error",
2525
"@eslint-react/no-duplicate-key": "error",
26+
"@eslint-react/no-forward-ref": "warn",
27+
"@eslint-react/no-implicit-key": "warn",
2628
"@eslint-react/no-missing-key": "error",
2729
"@eslint-react/no-nested-components": "error",
2830
"@eslint-react/no-prop-types": "error",

website/pages/docs/rules/_meta.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export default {
3030
"no-default-props": "no-default-props",
3131
"no-direct-mutation-state": "no-direct-mutation-state",
3232
"no-duplicate-key": "no-duplicate-key",
33+
"no-forward-ref": "no-forward-ref",
3334
"no-implicit-key": "no-implicit-key",
3435
"no-leaked-conditional-rendering": "no-leaked-conditional-rendering",
3536
"no-missing-component-display-name": "no-missing-component-display-name",
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# no-forward-ref
2+
3+
## Rule category
4+
5+
Restriction.
6+
7+
## What it does
8+
9+
Disallows using `React.forwardRef`.
10+
11+
## Why is this bad?
12+
13+
In React 19, `forwardRef` is no longer necessary. Pass `ref` as a prop instead.
14+
15+
`forwardRef` will deprecated in a future release. Learn more [here](https://react.dev/blog/2024/12/05/react-19#ref-as-a-prop).
16+
17+
## Examples
18+
19+
### Failing
20+
21+
```tsx
22+
import { forwardRef } from 'react';
23+
24+
const MyInput = forwardRef(function MyInput(props, ref) {
25+
// ...
26+
});
27+
```
28+
29+
### Passing
30+
31+
```tsx
32+
function MyInput({ ref, ...props }) {
33+
// ...
34+
}
35+
```
36+
37+
## Further Reading
38+
39+
- [React: APIs forwardRef](https://react.dev/reference/react/forwardRef)
40+
- [React: ref as a prop](https://react.dev/blog/2024/12/05/react-19#ref-as-a-prop)

website/pages/docs/rules/overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
| [`no-default-props`](no-default-props) | Prevents using `defaultProps` property in favor of ES6 default parameters. | ✔️ | | |
4747
| [`no-direct-mutation-state`](no-direct-mutation-state) | Prevents direct mutation of `this.state`. | ✔️ | | |
4848
| [`no-duplicate-key`](no-duplicate-key) | Prevents duplicate `key` on elements in the same array or a list of `children`. | ✔️ | | |
49+
| [`no-forward-ref`](no-forward-ref) | Prevents using `React.forwardRef`. || | |
4950
| [`no-implicit-key`](no-implicit-key) | Prevents `key` from not being explicitly specified (e.g. spreading `key` from objects). | 🧐 | | |
5051
| [`no-leaked-conditional-rendering`](no-leaked-conditional-rendering) | Prevents problematic leaked values from being rendered. | 🧐 | 💭 | |
5152
| [`no-missing-component-display-name`](no-missing-component-display-name) | Enforces that all components have a `displayName` which can be used in devtools. | 🐞 | | |

website/pages/roadmap.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
| `no-default-props` | Prevents using `defaultProps` property in favor of ES6 default parameters. |
3838
| `no-direct-mutation-state` | Prevents direct mutation of `this.state`. |
3939
| `no-duplicate-key` | Prevents duplicate `key` on elements in the same array or a list of `children`. |
40+
| `no-forward-ref` | Prevents using `forwardRef`. |
4041
| `no-implicit-key` | Prevents `key` from not being explicitly specified (e.g. spreading `key` from objects). |
4142
| `no-leaked-conditional-rendering` | Prevents problematic leaked values from being rendered. |
4243
| `no-missing-component-display-name` | Enforces that all components have a `displayName` which can be used in devtools. |

0 commit comments

Comments
 (0)