Skip to content

Commit dee960b

Browse files
authored
refactor(plugins/x): rename 'ensure-forward-ref-using-ref' to 'no-useless-forward-ref' (#987)
1 parent 5a95379 commit dee960b

20 files changed

+225
-122
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# ADR 0000: Choice between "useless" and "unnecessary" in rule naming
2+
3+
## Status
4+
5+
Accepted
6+
7+
## Context
8+
9+
When naming rules related to identifying redundant or non-functional code (e.g., in linting or static analysis), the terms "useless" and "unnecessary" both convey a sense of superfluity. However, their nuanced semantic differences could lead to ambiguity if misapplied. For example, a `forwardRef`-wrapped component that is never passed a `ref` is entirely without purpose, but other scenarios might involve optional code that is only redundant in specific contexts. A consistent naming convention is needed to ensure clarity and accuracy in rule documentation and error messaging.
10+
11+
## Decision
12+
13+
Use "useless" to describe code that has no functional purpose under any circumstances (e.g., a `forwardRef` with no `ref` usage).
14+
Use "unnecessary" to describe code that is contextually redundant but not strictly non-functional (e.g., an `useEffect` that runs event-specific logic).
15+
16+
## Consequences
17+
18+
- Improved clarity: Developers can better discern whether code is strictly non-functional ("useless") or situationally redundant ("unnecessary").
19+
20+
- Consistency: Rules and error messages will align with precise semantic definitions.
21+
22+
- Potential learning curve: Teams may need documentation to understand the distinction initially.
23+
24+
## Alternatives Considered
25+
26+
1. Using only "unnecessary" for all cases:
27+
- Rejected because it conflates fundamentally distinct scenarios (strictly non-functional vs. contextually redundant), reducing diagnostic precision.
28+
29+
2. Using only "useless" for all cases:
30+
- Rejected because it could mislabel code that is optional but valid in other contexts, leading to confusion or dismissal of valid feedback.
31+
32+
## Related ADRs
33+
34+
N/A
35+
36+
## Links
37+
38+
N/A
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# ADR 0001: Rename ensure-forward-ref-using-ref to no-useless-forward-ref
2+
3+
## Status
4+
5+
Accepted
6+
7+
## Context
8+
9+
The rule `ensure-forward-ref-using-ref` detects React `forwardRef`-wrapped components that never receive a `ref` prop. This matches the "useless" criteria defined in ADR 0000, as such components serve no functional purpose. The current name lacks alignment with our established `no-<category>-<term>` naming convention and semantic categorization.
10+
11+
## Decision
12+
13+
Rename the rule to **`no-useless-forward-ref`** to:
14+
15+
1. Adhere to the `no-<category>-<term>` pattern used in similar rules (e.g., `no-useless-state`).
16+
2. Reflect the strict "useless" classification per ADR 0000, since unreferenced `forwardRef` components are functionally inert.
17+
18+
## Consequences
19+
20+
- **Consistency**: Aligns with existing rule taxonomy and terminology.
21+
- **Clarity**: Clearly signals the rule's focus on non-functional code.
22+
- **Documentation updates**: Requires migration guides and rule metadata changes.
23+
24+
## Alternatives Considered
25+
26+
Using `no-unnecessary-forward-ref`:
27+
28+
- Rejected because "unnecessary" implies optional redundancy, whereas unreferenced `forwardRef` has zero runtime utility.
29+
30+
## Related ADRs
31+
32+
- [ADR 0000: Choice between "useless" and "unnecessary" in rule naming](./0000-choice-between-useless-and-unnecessary-in-rule-naming.md)
33+
34+
## Links
35+
36+
N/A

.vscode/settings.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"editor.defaultFormatter": "dprint.dprint"
44
},
55
"[jsonc]": {
6-
"editor.defaultFormatter": "dprint.dprint"
6+
"editor.defaultFormatter": "vscode.json-language-features"
77
},
88
"[typescript]": {
99
"editor.defaultFormatter": "dprint.dprint"

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
## v1.33.0 (Draft)
2+
3+
### 🪄 Improvements
4+
5+
- refactor(plugins/x): rename `ensure-forward-ref-using-ref` to `no-useless-forward-ref`
6+
7+
### 📝 Changes you should be aware of
8+
9+
The following rules have been renamed:
10+
111
## v1.32.1 (2025-03-13)
212

313
### 🐞 Fixes

apps/website/content/docs/changelog.md

+10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22
title: Changelog
33
---
44

5+
## v1.33.0 (Draft)
6+
7+
### 🪄 Improvements
8+
9+
- refactor(plugins/x): rename `ensure-forward-ref-using-ref` to `no-useless-forward-ref`
10+
11+
### 📝 Changes you should be aware of
12+
13+
The following rules have been renamed:
14+
515
## v1.32.1 (2025-03-13)
616

717
### 🐞 Fixes

apps/website/content/docs/deprecated.md

+6-5
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ full: true
88

99
| Rule | Replaced by | Deprecated in |
1010
| :--------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------- | :------------ |
11-
| [`jsx-uses-vars`](/docs/rules/jsx-uses-vars) | [`use-jsx-vars`](/docs/rules/use-jsx-vars) | 1.22.0 |
1211
| [`jsx-no-duplicate-props`](/docs/rules/jsx-no-duplicate-props) | [`no-duplicate-jsx-props`](/docs/rules/no-duplicate-jsx-props) | 1.22.0 |
13-
| [`no-complicated-conditional-rendering`](/docs/rules/no-complicated-conditional-rendering) | [`no-complex-conditional-rendering`](/docs/rules/no-complex-conditional-rendering) | 1.6.0 |
14-
| [`no-children-in-void-dom-elements`](/docs/rules/dom-no-children-in-void-dom-elements) | [`no-void-elements-with-children`](/docs/rules/dom-no-void-elements-with-children) | 1.22.0 |
15-
| [`no-redundant-custom-hook`](/docs/rules/hooks-extra-no-useless-custom-hooks) | [`no-useless-custom-hooks`](/docs/rules/hooks-extra-no-useless-custom-hooks) | 1.21.0 |
12+
| [`jsx-uses-vars`](/docs/rules/jsx-uses-vars) | [`use-jsx-vars`](/docs/rules/use-jsx-vars) | 1.22.0 |
1613
| [`ensure-custom-hooks-using-other-hooks`](/docs/rules/hooks-extra-no-useless-custom-hooks) | [`no-useless-custom-hooks`](/docs/rules/hooks-extra-no-useless-custom-hooks) | 1.13.0 |
17-
| [`ensure-use-memo-has-non-empty-deps`](/docs/rules/hooks-extra-ensure-use-memo-has-non-empty-deps) | [`no-unnecessary-use-memo`](/docs/rules/hooks-extra-no-unnecessary-use-memo) | 1.13.0 |
14+
| [`ensure-forward-ref-using-ref`](/docs/rules/ensure-forward-ref-using-ref) | [`no-useless-forward-ref`](/docs/rules/no-useless-forward-ref) | 1.33.0 |
1815
| [`ensure-use-callback-has-non-empty-deps`](/docs/rules/hooks-extra-ensure-use-callback-has-non-empty-deps) | [`no-unnecessary-use-callback`](/docs/rules/hooks-extra-no-unnecessary-use-callback) | 1.13.0 |
16+
| [`ensure-use-memo-has-non-empty-deps`](/docs/rules/hooks-extra-ensure-use-memo-has-non-empty-deps) | [`no-unnecessary-use-memo`](/docs/rules/hooks-extra-no-unnecessary-use-memo) | 1.13.0 |
17+
| [`no-children-in-void-dom-elements`](/docs/rules/dom-no-children-in-void-dom-elements) | [`no-void-elements-with-children`](/docs/rules/dom-no-void-elements-with-children) | 1.22.0 |
18+
| [`no-complicated-conditional-rendering`](/docs/rules/no-complicated-conditional-rendering) | [`no-complex-conditional-rendering`](/docs/rules/no-complex-conditional-rendering) | 1.6.0 |
19+
| [`no-redundant-custom-hook`](/docs/rules/hooks-extra-no-useless-custom-hooks) | [`no-useless-custom-hooks`](/docs/rules/hooks-extra-no-useless-custom-hooks) | 1.21.0 |
1920

2021
## Presets
2122

apps/website/content/docs/roadmap.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ title: Roadmap
3232

3333
### Add suggestion-fix feature to rules that can be fixed interactively
3434

35-
- [ ] `ensure-forward-ref-using-ref`
35+
- [ ] `no-useless-forward-ref`
3636
- [ ] `no-leaked-conditional-rendering`
3737
- [ ] `no-redundant-should-component-update`
3838
- [ ] `no-unused-class-component-members`

apps/website/content/docs/rules/meta.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"---Core Rules---",
55
"avoid-shorthand-boolean",
66
"avoid-shorthand-fragment",
7-
"ensure-forward-ref-using-ref",
7+
"no-useless-forward-ref",
88
"no-access-state-in-setstate",
99
"no-array-index-key",
1010
"no-children-count",

apps/website/content/docs/rules/overview.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ full: true
2020
| :----------------------------------------------------------------------------------- | :- | :------------ | :---------------------------------------------------------------------------------------------------- | :------: |
2121
| [`avoid-shorthand-boolean`](./avoid-shorthand-boolean) | 0️⃣ | `🔍` `🔧` | Enforces the use of explicit boolean values for boolean attributes. | |
2222
| [`avoid-shorthand-fragment`](./avoid-shorthand-fragment) | 0️⃣ | `🔍` | Enforces the use of explicit `<Fragment>` components instead of the shorthand `<>` or `</>` syntax. | |
23-
| [`ensure-forward-ref-using-ref`](./ensure-forward-ref-using-ref) | 1️⃣ | `🔍` | Requires that components wrapped with `forwardRef` must have a `ref` parameter. | |
23+
| [`no-useless-forward-ref`](./no-useless-forward-ref) | 1️⃣ | `🔍` | Requires that components wrapped with `forwardRef` must have a `ref` parameter. | |
2424
| [`no-access-state-in-setstate`](./no-access-state-in-setstate) | 2️⃣ | `🔍` | Prevents accessing `this.state` inside `setState` calls. | |
2525
| [`no-array-index-key`](./no-array-index-key) | 1️⃣ | `🔍` | Prevents using an item's index in the array as its key | |
2626
| [`no-children-count`](./no-children-count) | 1️⃣ | `🔍` | Prevents using `Children.count`. | |

apps/website/next.config.mjs

+5-5
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ const config = {
8989
destination: "/docs/rules/no-complex-conditional-rendering",
9090
permanent: true,
9191
},
92+
{
93+
source: "/docs/rules/ensure-forward-ref-using-ref",
94+
destination: "/docs/rules/no-useless-forward-ref",
95+
permanent: true,
96+
},
9297
{
9398
source: "/docs/rules/dom-no-children-in-void-dom-elements",
9499
destination: "/docs/rules/dom-no-void-elements-with-children",
@@ -114,11 +119,6 @@ const config = {
114119
destination: "/docs/rules/hooks-extra-no-useless-custom-hooks",
115120
permanent: true,
116121
},
117-
{
118-
source: "/docs/rules/debug-react-hooks",
119-
destination: "/docs/rules/debug-hook",
120-
permanent: true,
121-
},
122122
];
123123
},
124124
};

apps/website/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"@local/configs": "workspace:*",
3636
"@mdx-js/mdx": "^3.1.0",
3737
"@next/eslint-plugin-next": "^15.2.2",
38-
"@tailwindcss/postcss": "^4.0.13",
38+
"@tailwindcss/postcss": "^4.0.14",
3939
"@tsconfig/next": "^2.0.3",
4040
"@tsconfig/node22": "^22.0.0",
4141
"@tsconfig/strictest": "^2.0.5",
@@ -58,7 +58,7 @@
5858
"eslint-plugin-simple-import-sort": "^12.1.1",
5959
"eslint-plugin-unicorn": "^57.0.0",
6060
"postcss": "^8.5.3",
61-
"tailwindcss": "^4.0.13",
61+
"tailwindcss": "^4.0.14",
6262
"tailwindcss-animated": "^2.0.0",
6363
"typescript": "^5.8.2",
6464
"typescript-eslint": "^8.26.1"

packages/plugins/eslint-plugin-react-x/src/configs/recommended.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { DEFAULT_ESLINT_REACT_SETTINGS } from "@eslint-react/shared";
44
export const name = "react-x/recommended";
55

66
export const rules = {
7-
"react-x/ensure-forward-ref-using-ref": "warn",
87
"react-x/no-access-state-in-setstate": "error",
98
"react-x/no-array-index-key": "warn",
109
"react-x/no-children-count": "warn",
@@ -41,6 +40,7 @@ export const rules = {
4140
"react-x/no-unused-class-component-members": "warn",
4241
"react-x/no-unused-state": "warn",
4342
"react-x/no-use-context": "warn",
43+
"react-x/no-useless-forward-ref": "warn",
4444
"react-x/use-jsx-vars": "warn",
4545
} as const satisfies RulePreset;
4646

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { name, version } from "../package.json";
22
import avoidShorthandBoolean from "./rules/avoid-shorthand-boolean";
33
import avoidShorthandFragment from "./rules/avoid-shorthand-fragment";
4-
import forwardRefUsingRef from "./rules/ensure-forward-ref-using-ref";
54
import noAccessStateInSetstate from "./rules/no-access-state-in-setstate";
65
import noArrayIndexKey from "./rules/no-array-index-key";
76
import noChildrenCount from "./rules/no-children-count";
@@ -44,6 +43,7 @@ import noUnstableDefaultProps from "./rules/no-unstable-default-props";
4443
import noUnusedClassComponentMembers from "./rules/no-unused-class-component-members";
4544
import noUnusedState from "./rules/no-unused-state";
4645
import noUseContext from "./rules/no-use-context";
46+
import noUselessForwardRef from "./rules/no-useless-forward-ref";
4747
import noUselessFragment from "./rules/no-useless-fragment";
4848
import preferDestructuringAssignment from "./rules/prefer-destructuring-assignment";
4949
import preferReactNamespaceImport from "./rules/prefer-react-namespace-import";
@@ -60,7 +60,6 @@ export const plugin = {
6060
rules: {
6161
"avoid-shorthand-boolean": avoidShorthandBoolean,
6262
"avoid-shorthand-fragment": avoidShorthandFragment,
63-
"ensure-forward-ref-using-ref": forwardRefUsingRef,
6463
"no-access-state-in-setstate": noAccessStateInSetstate,
6564
"no-array-index-key": noArrayIndexKey,
6665
"no-children-count": noChildrenCount,
@@ -103,6 +102,7 @@ export const plugin = {
103102
"no-unused-class-component-members": noUnusedClassComponentMembers,
104103
"no-unused-state": noUnusedState,
105104
"no-use-context": noUseContext,
105+
"no-useless-forward-ref": noUselessForwardRef,
106106
"no-useless-fragment": noUselessFragment,
107107
"prefer-destructuring-assignment": preferDestructuringAssignment,
108108
"prefer-react-namespace-import": preferReactNamespaceImport,
@@ -112,6 +112,8 @@ export const plugin = {
112112
"use-jsx-vars": useJsxVars,
113113

114114
// Part: deprecated rules
115+
/** @deprecated Use `no-useless-forward-ref` instead */
116+
"ensure-forward-ref-using-ref": noUselessForwardRef,
115117
/** @deprecated Use `no-duplicate-jsx-props` instead */
116118
"jsx-no-duplicate-props": noDuplicateJsxProps,
117119
/** @deprecated Use `use-jsx-vars` instead */

packages/plugins/eslint-plugin-react-x/src/rules/no-forward-ref.md

+2
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ function MyInput({ ref, value, onChange }: MyInputProps & { ref: React.RefObject
9898

9999
## See Also
100100

101+
- [`no-useless-forward-ref`](./no-useless-forward-ref)\
102+
Enforces that `forwardRef` is only used when a `ref` parameter is declared.
101103
- [`no-context-provider`](./no-context-provider)\
102104
Replaces usages of `<Context.Provider>` with `<Context>`.
103105
- [`no-use-context`](./no-use-context)\

packages/plugins/eslint-plugin-react-x/src/rules/ensure-forward-ref-using-ref.md renamed to packages/plugins/eslint-plugin-react-x/src/rules/no-useless-forward-ref.md

+17-9
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
---
2-
title: ensure-forward-ref-using-ref
2+
title: no-useless-forward-ref
33
---
44

55
**Full Name in `eslint-plugin-react-x`**
66

77
```plain copy
8-
react-x/ensure-forward-ref-using-ref
8+
react-x/no-useless-forward-ref
99
```
1010

1111
**Full Name in `@eslint-react/eslint-plugin`**
1212

1313
```plain copy
14-
@eslint-react/ensure-forward-ref-using-ref
14+
@eslint-react/no-useless-forward-ref
1515
```
1616

1717
**Features**
@@ -27,11 +27,12 @@ react-x/ensure-forward-ref-using-ref
2727

2828
## What it does
2929

30-
Requires that components wrapped with `forwardRef` must have a `ref` parameter.
30+
Enforces that `forwardRef` is only used when a `ref` parameter is declared.
3131

32-
This rule checks all React components using `forwardRef` and verifies that there is a second parameter.
32+
This rule enforces that:
3333

34-
Omitting the `ref` argument is usually a bug, and components not using `ref` don't need to be wrapped by `forwardRef`.
34+
1. Components using `forwardRef` must declare a `ref` parameter
35+
2. Components not using `ref` should not be wrapped with `forwardRef`
3536

3637
## Examples
3738

@@ -42,7 +43,7 @@ import React from "react";
4243

4344
const MyComponent = React.forwardRef((props) => {
4445
// ^^^^^
45-
// - 'forwardRef' is used with this component but no 'ref' parameter is set.
46+
// - 'forwardRef' wrapper is useless without 'ref' parameter.
4647
return <button />;
4748
});
4849
```
@@ -59,9 +60,16 @@ const MyComponent = React.forwardRef<HTMLButtonElement>((props, ref) => {
5960

6061
## Implementation
6162

62-
- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/ensure-forward-ref-using-ref.ts)
63-
- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/ensure-forward-ref-using-ref.spec.ts)
63+
- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-forward-ref.ts)
64+
- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-forward-ref.spec.ts)
6465

6566
## Further Reading
6667

6768
- [React: forwardRef](https://react.dev/reference/react/forwardRef)
69+
70+
---
71+
72+
## See Also
73+
74+
- [`no-forward-ref`](./no-forward-ref)\
75+
Replaces usages of `forwardRef` with passing `ref` as a prop.

0 commit comments

Comments
 (0)