Skip to content

Commit

Permalink
feat(plugins/x): add 'no-forward-ref' (#870)
Browse files Browse the repository at this point in the history
  • Loading branch information
Rel1cx authored Dec 6, 2024
1 parent 8f8ba13 commit 54bd15d
Show file tree
Hide file tree
Showing 10 changed files with 246 additions and 0 deletions.
2 changes: 2 additions & 0 deletions packages/plugins/eslint-plugin-react-x/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export default [
"react-x/no-default-props": "error",
"react-x/no-direct-mutation-state": "error",
"react-x/no-duplicate-key": "error",
"react-x/no-forward-ref": "warn",
"react-x/no-implicit-key": "warn",
"react-x/no-missing-key": "error",
"react-x/no-nested-components": "warn",
Expand Down Expand Up @@ -97,6 +98,7 @@ export default [
| `no-default-props` | Prevents using `defaultProps` property in favor of ES6 default parameters. | ✔️ | | |
| `no-direct-mutation-state` | Prevents direct mutation of `this.state`. | ✔️ | | |
| `no-duplicate-key` | Prevents duplicate `key` on elements in the same array or a list of `children`. | ✔️ | | |
| `no-forward-ref` | Prevents using `forwardRef`. | 🧐 | | |
| `no-implicit-key` | Prevents `key` from not being explicitly specified (e.g. spreading `key` from objects). | 🧐 | | |
| `no-leaked-conditional-rendering` | Prevents problematic leaked values from being rendered. | 🧐 | 💭 | |
| `no-missing-component-display-name` | Enforces that all components have a `displayName` which can be used in devtools. | 🐞 | | |
Expand Down
2 changes: 2 additions & 0 deletions packages/plugins/eslint-plugin-react-x/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import noCreateRef from "./rules/no-create-ref";
import noDefaultProps from "./rules/no-default-props";
import noDirectMutationState from "./rules/no-direct-mutation-state";
import noDuplicateKey from "./rules/no-duplicate-key";
import noForwardRef from "./rules/no-forward-ref";
import noImplicitKey from "./rules/no-implicit-key";
import noLeakedConditionalRendering from "./rules/no-leaked-conditional-rendering";
import noMissingComponentDisplayName from "./rules/no-missing-component-display-name";
Expand Down Expand Up @@ -93,6 +94,7 @@ export default {
"no-default-props": noDefaultProps,
"no-direct-mutation-state": noDirectMutationState,
"no-duplicate-key": noDuplicateKey,
"no-forward-ref": noForwardRef,
"no-implicit-key": noImplicitKey,
"no-leaked-conditional-rendering": noLeakedConditionalRendering,
"no-missing-component-display-name": noMissingComponentDisplayName,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { ruleTester } from "../../../../../test";
import rule, { RULE_NAME } from "./no-forward-ref";

ruleTester.run(RULE_NAME, rule, {
invalid: [
{
code: /* tsx */ `
import { forwardRef } from 'react'
forwardRef((props) => {
return null;
});
`,
errors: [{ messageId: "noForwardRef" }],
settings: {
"react-x": {
version: "19.0.0",
},
},
},
{
code: /* tsx */ `
import { forwardRef } from 'react'
forwardRef((props) => null);
`,
errors: [{ messageId: "noForwardRef" }],
settings: {
"react-x": {
version: "19.0.0",
},
},
},
{
code: /* tsx */ `
import { forwardRef } from 'react'
forwardRef(function (props) {
return null;
});
`,
errors: [{ messageId: "noForwardRef" }],
settings: {
"react-x": {
version: "19.0.0",
},
},
},
{
code: /* tsx */ `
import { forwardRef } from 'react'
forwardRef(function Component(props) {
return null;
});
`,
errors: [{ messageId: "noForwardRef" }],
settings: {
"react-x": {
version: "19.0.0",
},
},
},
{
code: /* tsx */ `
import * as React from 'react'
React.forwardRef((props) => {
return null;
});
`,
errors: [{ messageId: "noForwardRef" }],
settings: {
"react-x": {
version: "19.0.0",
},
},
},
{
code: /* tsx */ `
import * as React from 'react'
React.forwardRef((props) => null);
`,
errors: [{ messageId: "noForwardRef" }],
settings: {
"react-x": {
version: "19.0.0",
},
},
},
{
code: /* tsx */ `
import * as React from 'react'
React.forwardRef(function (props) {
return null;
});
`,
errors: [{ messageId: "noForwardRef" }],
settings: {
"react-x": {
version: "19.0.0",
},
},
},
{
code: /* tsx */ `
import * as React from 'react'
React.forwardRef(function Component(props) {
return null;
});
`,
errors: [{ messageId: "noForwardRef" }],
settings: {
"react-x": {
version: "19.0.0",
},
},
},
],
valid: [
{
code: /* tsx */ `
import * as React from 'react'
React.forwardRef(function Component(props) {
return null;
});
`,
settings: {
"react-x": {
version: "18.3.1",
},
},
},
{
code: /* tsx */ `
import * as React from 'react'
const Component = React.forwardRef((props, ref) => {
return null;
});
`,
settings: {
"react-x": {
version: "18.3.1",
},
},
},
/* tsx */ `
import * as React from 'react'
const Component = ({ ref }) => {
return null;
};
`,
/* tsx */ `
import * as React from 'react'
const Component = ({ ref, ...props }) => {
return null;
};
`,
],
});
38 changes: 38 additions & 0 deletions packages/plugins/eslint-plugin-react-x/src/rules/no-forward-ref.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { isForwardRefCall } from "@eslint-react/core";
import { decodeSettings, normalizeSettings } from "@eslint-react/shared";
import { compare } from "compare-versions";
import type { CamelCase } from "string-ts";

import { createRule } from "../utils";

export const RULE_NAME = "no-forward-ref";

export type MessageID = CamelCase<typeof RULE_NAME>;

export default createRule<[], MessageID>({
meta: {
type: "problem",
docs: {
description: "disallow the use of 'forwardRef'",
},
messages: {
noForwardRef: "In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead.",
},
schema: [],
},
name: RULE_NAME,
create(context) {
const { version } = normalizeSettings(decodeSettings(context.settings));
if (compare(version, "19.0.0", "<")) return {};
return {
CallExpression(node) {
if (!isForwardRefCall(node, context)) return;
context.report({
messageId: "noForwardRef",
node,
});
},
};
},
defaultOptions: [],
});
1 change: 1 addition & 0 deletions packages/plugins/eslint-plugin/src/configs/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const rules = {
"@eslint-react/no-default-props": "error",
"@eslint-react/no-direct-mutation-state": "error",
"@eslint-react/no-duplicate-key": "error",
"@eslint-react/no-forward-ref": "warn",
"@eslint-react/no-implicit-key": "warn",
"@eslint-react/no-missing-component-display-name": "warn",
"@eslint-react/no-missing-key": "error",
Expand Down
2 changes: 2 additions & 0 deletions packages/plugins/eslint-plugin/src/configs/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export const rules = {
"@eslint-react/no-default-props": "error",
"@eslint-react/no-direct-mutation-state": "error",
"@eslint-react/no-duplicate-key": "error",
"@eslint-react/no-forward-ref": "warn",
"@eslint-react/no-implicit-key": "warn",
"@eslint-react/no-missing-key": "error",
"@eslint-react/no-nested-components": "error",
"@eslint-react/no-prop-types": "error",
Expand Down
1 change: 1 addition & 0 deletions website/pages/docs/rules/_meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export default {
"no-default-props": "no-default-props",
"no-direct-mutation-state": "no-direct-mutation-state",
"no-duplicate-key": "no-duplicate-key",
"no-forward-ref": "no-forward-ref",
"no-implicit-key": "no-implicit-key",
"no-leaked-conditional-rendering": "no-leaked-conditional-rendering",
"no-missing-component-display-name": "no-missing-component-display-name",
Expand Down
40 changes: 40 additions & 0 deletions website/pages/docs/rules/no-forward-ref.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# no-forward-ref

## Rule category

Restriction.

## What it does

Disallows using `React.forwardRef`.

## Why is this bad?

In React 19, `forwardRef` is no longer necessary. Pass `ref` as a prop instead.

`forwardRef` will deprecated in a future release. Learn more [here](https://react.dev/blog/2024/12/05/react-19#ref-as-a-prop).

## Examples

### Failing

```tsx
import { forwardRef } from 'react';

const MyInput = forwardRef(function MyInput(props, ref) {
// ...
});
```

### Passing

```tsx
function MyInput({ ref, ...props }) {
// ...
}
```

## Further Reading

- [React: APIs forwardRef](https://react.dev/reference/react/forwardRef)
- [React: ref as a prop](https://react.dev/blog/2024/12/05/react-19#ref-as-a-prop)
1 change: 1 addition & 0 deletions website/pages/docs/rules/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
| [`no-default-props`](no-default-props) | Prevents using `defaultProps` property in favor of ES6 default parameters. | ✔️ | | |
| [`no-direct-mutation-state`](no-direct-mutation-state) | Prevents direct mutation of `this.state`. | ✔️ | | |
| [`no-duplicate-key`](no-duplicate-key) | Prevents duplicate `key` on elements in the same array or a list of `children`. | ✔️ | | |
| [`no-forward-ref`](no-forward-ref) | Prevents using `React.forwardRef`. || | |
| [`no-implicit-key`](no-implicit-key) | Prevents `key` from not being explicitly specified (e.g. spreading `key` from objects). | 🧐 | | |
| [`no-leaked-conditional-rendering`](no-leaked-conditional-rendering) | Prevents problematic leaked values from being rendered. | 🧐 | 💭 | |
| [`no-missing-component-display-name`](no-missing-component-display-name) | Enforces that all components have a `displayName` which can be used in devtools. | 🐞 | | |
Expand Down
1 change: 1 addition & 0 deletions website/pages/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
| `no-default-props` | Prevents using `defaultProps` property in favor of ES6 default parameters. |
| `no-direct-mutation-state` | Prevents direct mutation of `this.state`. |
| `no-duplicate-key` | Prevents duplicate `key` on elements in the same array or a list of `children`. |
| `no-forward-ref` | Prevents using `forwardRef`. |
| `no-implicit-key` | Prevents `key` from not being explicitly specified (e.g. spreading `key` from objects). |
| `no-leaked-conditional-rendering` | Prevents problematic leaked values from being rendered. |
| `no-missing-component-display-name` | Enforces that all components have a `displayName` which can be used in devtools. |
Expand Down

0 comments on commit 54bd15d

Please sign in to comment.