Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# ADR 0000: Choice between "useless" and "unnecessary" in rule naming

## Status

Accepted

## Context

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.

## Decision

Use "useless" to describe code that has no functional purpose under any circumstances (e.g., a `forwardRef` with no `ref` usage).
Use "unnecessary" to describe code that is contextually redundant but not strictly non-functional (e.g., an `useEffect` that runs event-specific logic).

## Consequences

- Improved clarity: Developers can better discern whether code is strictly non-functional ("useless") or situationally redundant ("unnecessary").

- Consistency: Rules and error messages will align with precise semantic definitions.

- Potential learning curve: Teams may need documentation to understand the distinction initially.

## Alternatives Considered

1. Using only "unnecessary" for all cases:
- Rejected because it conflates fundamentally distinct scenarios (strictly non-functional vs. contextually redundant), reducing diagnostic precision.

2. Using only "useless" for all cases:
- Rejected because it could mislabel code that is optional but valid in other contexts, leading to confusion or dismissal of valid feedback.

## Related ADRs

N/A

## Links

N/A
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# ADR 0001: Rename ensure-forward-ref-using-ref to no-useless-forward-ref

## Status

Accepted

## Context

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.

## Decision

Rename the rule to **`no-useless-forward-ref`** to:

1. Adhere to the `no-<category>-<term>` pattern used in similar rules (e.g., `no-useless-state`).
2. Reflect the strict "useless" classification per ADR 0000, since unreferenced `forwardRef` components are functionally inert.

## Consequences

- **Consistency**: Aligns with existing rule taxonomy and terminology.
- **Clarity**: Clearly signals the rule's focus on non-functional code.
- **Documentation updates**: Requires migration guides and rule metadata changes.

## Alternatives Considered

Using `no-unnecessary-forward-ref`:

- Rejected because "unnecessary" implies optional redundancy, whereas unreferenced `forwardRef` has zero runtime utility.

## Related ADRs

- [ADR 0000: Choice between "useless" and "unnecessary" in rule naming](./0000-choice-between-useless-and-unnecessary-in-rule-naming.md)

## Links

N/A
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"editor.defaultFormatter": "dprint.dprint"
},
"[jsonc]": {
"editor.defaultFormatter": "dprint.dprint"
"editor.defaultFormatter": "vscode.json-language-features"
},
"[typescript]": {
"editor.defaultFormatter": "dprint.dprint"
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## v1.33.0 (Draft)

### 🪄 Improvements

- refactor(plugins/x): rename `ensure-forward-ref-using-ref` to `no-useless-forward-ref`

### 📝 Changes you should be aware of

The following rules have been renamed:

## v1.32.1 (2025-03-13)

### 🐞 Fixes
Expand Down
10 changes: 10 additions & 0 deletions apps/website/content/docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@
title: Changelog
---

## v1.33.0 (Draft)

### 🪄 Improvements

- refactor(plugins/x): rename `ensure-forward-ref-using-ref` to `no-useless-forward-ref`

### 📝 Changes you should be aware of

The following rules have been renamed:

## v1.32.1 (2025-03-13)

### 🐞 Fixes
Expand Down
11 changes: 6 additions & 5 deletions apps/website/content/docs/deprecated.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ full: true

| Rule | Replaced by | Deprecated in |
| :--------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------- | :------------ |
| [`jsx-uses-vars`](/docs/rules/jsx-uses-vars) | [`use-jsx-vars`](/docs/rules/use-jsx-vars) | 1.22.0 |
| [`jsx-no-duplicate-props`](/docs/rules/jsx-no-duplicate-props) | [`no-duplicate-jsx-props`](/docs/rules/no-duplicate-jsx-props) | 1.22.0 |
| [`no-complicated-conditional-rendering`](/docs/rules/no-complicated-conditional-rendering) | [`no-complex-conditional-rendering`](/docs/rules/no-complex-conditional-rendering) | 1.6.0 |
| [`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 |
| [`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 |
| [`jsx-uses-vars`](/docs/rules/jsx-uses-vars) | [`use-jsx-vars`](/docs/rules/use-jsx-vars) | 1.22.0 |
| [`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 |
| [`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 |
| [`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 |
| [`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 |
| [`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 |
| [`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 |
| [`no-complicated-conditional-rendering`](/docs/rules/no-complicated-conditional-rendering) | [`no-complex-conditional-rendering`](/docs/rules/no-complex-conditional-rendering) | 1.6.0 |
| [`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 |

## Presets

Expand Down
2 changes: 1 addition & 1 deletion apps/website/content/docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ title: Roadmap

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

- [ ] `ensure-forward-ref-using-ref`
- [ ] `no-useless-forward-ref`
- [ ] `no-leaked-conditional-rendering`
- [ ] `no-redundant-should-component-update`
- [ ] `no-unused-class-component-members`
Expand Down
2 changes: 1 addition & 1 deletion apps/website/content/docs/rules/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"---Core Rules---",
"avoid-shorthand-boolean",
"avoid-shorthand-fragment",
"ensure-forward-ref-using-ref",
"no-useless-forward-ref",
"no-access-state-in-setstate",
"no-array-index-key",
"no-children-count",
Expand Down
2 changes: 1 addition & 1 deletion apps/website/content/docs/rules/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ full: true
| :----------------------------------------------------------------------------------- | :- | :------------ | :---------------------------------------------------------------------------------------------------- | :------: |
| [`avoid-shorthand-boolean`](./avoid-shorthand-boolean) | 0️⃣ | `🔍` `🔧` | Enforces the use of explicit boolean values for boolean attributes. | |
| [`avoid-shorthand-fragment`](./avoid-shorthand-fragment) | 0️⃣ | `🔍` | Enforces the use of explicit `<Fragment>` components instead of the shorthand `<>` or `</>` syntax. | |
| [`ensure-forward-ref-using-ref`](./ensure-forward-ref-using-ref) | 1️⃣ | `🔍` | Requires that components wrapped with `forwardRef` must have a `ref` parameter. | |
| [`no-useless-forward-ref`](./no-useless-forward-ref) | 1️⃣ | `🔍` | Requires that components wrapped with `forwardRef` must have a `ref` parameter. | |
| [`no-access-state-in-setstate`](./no-access-state-in-setstate) | 2️⃣ | `🔍` | Prevents accessing `this.state` inside `setState` calls. | |
| [`no-array-index-key`](./no-array-index-key) | 1️⃣ | `🔍` | Prevents using an item's index in the array as its key | |
| [`no-children-count`](./no-children-count) | 1️⃣ | `🔍` | Prevents using `Children.count`. | |
Expand Down
10 changes: 5 additions & 5 deletions apps/website/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ const config = {
destination: "/docs/rules/no-complex-conditional-rendering",
permanent: true,
},
{
source: "/docs/rules/ensure-forward-ref-using-ref",
destination: "/docs/rules/no-useless-forward-ref",
permanent: true,
},
{
source: "/docs/rules/dom-no-children-in-void-dom-elements",
destination: "/docs/rules/dom-no-void-elements-with-children",
Expand All @@ -114,11 +119,6 @@ const config = {
destination: "/docs/rules/hooks-extra-no-useless-custom-hooks",
permanent: true,
},
{
source: "/docs/rules/debug-react-hooks",
destination: "/docs/rules/debug-hook",
permanent: true,
},
];
},
};
Expand Down
4 changes: 2 additions & 2 deletions apps/website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"@local/configs": "workspace:*",
"@mdx-js/mdx": "^3.1.0",
"@next/eslint-plugin-next": "^15.2.2",
"@tailwindcss/postcss": "^4.0.13",
"@tailwindcss/postcss": "^4.0.14",
"@tsconfig/next": "^2.0.3",
"@tsconfig/node22": "^22.0.0",
"@tsconfig/strictest": "^2.0.5",
Expand All @@ -58,7 +58,7 @@
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unicorn": "^57.0.0",
"postcss": "^8.5.3",
"tailwindcss": "^4.0.13",
"tailwindcss": "^4.0.14",
"tailwindcss-animated": "^2.0.0",
"typescript": "^5.8.2",
"typescript-eslint": "^8.26.1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { DEFAULT_ESLINT_REACT_SETTINGS } from "@eslint-react/shared";
export const name = "react-x/recommended";

export const rules = {
"react-x/ensure-forward-ref-using-ref": "warn",
"react-x/no-access-state-in-setstate": "error",
"react-x/no-array-index-key": "warn",
"react-x/no-children-count": "warn",
Expand Down Expand Up @@ -41,6 +40,7 @@ export const rules = {
"react-x/no-unused-class-component-members": "warn",
"react-x/no-unused-state": "warn",
"react-x/no-use-context": "warn",
"react-x/no-useless-forward-ref": "warn",
"react-x/use-jsx-vars": "warn",
} as const satisfies RulePreset;

Expand Down
6 changes: 4 additions & 2 deletions packages/plugins/eslint-plugin-react-x/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { name, version } from "../package.json";
import avoidShorthandBoolean from "./rules/avoid-shorthand-boolean";
import avoidShorthandFragment from "./rules/avoid-shorthand-fragment";
import forwardRefUsingRef from "./rules/ensure-forward-ref-using-ref";
import noAccessStateInSetstate from "./rules/no-access-state-in-setstate";
import noArrayIndexKey from "./rules/no-array-index-key";
import noChildrenCount from "./rules/no-children-count";
Expand Down Expand Up @@ -44,6 +43,7 @@ import noUnstableDefaultProps from "./rules/no-unstable-default-props";
import noUnusedClassComponentMembers from "./rules/no-unused-class-component-members";
import noUnusedState from "./rules/no-unused-state";
import noUseContext from "./rules/no-use-context";
import noUselessForwardRef from "./rules/no-useless-forward-ref";
import noUselessFragment from "./rules/no-useless-fragment";
import preferDestructuringAssignment from "./rules/prefer-destructuring-assignment";
import preferReactNamespaceImport from "./rules/prefer-react-namespace-import";
Expand All @@ -60,7 +60,6 @@ export const plugin = {
rules: {
"avoid-shorthand-boolean": avoidShorthandBoolean,
"avoid-shorthand-fragment": avoidShorthandFragment,
"ensure-forward-ref-using-ref": forwardRefUsingRef,
"no-access-state-in-setstate": noAccessStateInSetstate,
"no-array-index-key": noArrayIndexKey,
"no-children-count": noChildrenCount,
Expand Down Expand Up @@ -103,6 +102,7 @@ export const plugin = {
"no-unused-class-component-members": noUnusedClassComponentMembers,
"no-unused-state": noUnusedState,
"no-use-context": noUseContext,
"no-useless-forward-ref": noUselessForwardRef,
"no-useless-fragment": noUselessFragment,
"prefer-destructuring-assignment": preferDestructuringAssignment,
"prefer-react-namespace-import": preferReactNamespaceImport,
Expand All @@ -112,6 +112,8 @@ export const plugin = {
"use-jsx-vars": useJsxVars,

// Part: deprecated rules
/** @deprecated Use `no-useless-forward-ref` instead */
"ensure-forward-ref-using-ref": noUselessForwardRef,
/** @deprecated Use `no-duplicate-jsx-props` instead */
"jsx-no-duplicate-props": noDuplicateJsxProps,
/** @deprecated Use `use-jsx-vars` instead */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ function MyInput({ ref, value, onChange }: MyInputProps & { ref: React.RefObject

## See Also

- [`no-useless-forward-ref`](./no-useless-forward-ref)\
Enforces that `forwardRef` is only used when a `ref` parameter is declared.
- [`no-context-provider`](./no-context-provider)\
Replaces usages of `<Context.Provider>` with `<Context>`.
- [`no-use-context`](./no-use-context)\
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
---
title: ensure-forward-ref-using-ref
title: no-useless-forward-ref
---

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

```plain copy
react-x/ensure-forward-ref-using-ref
react-x/no-useless-forward-ref
```

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

```plain copy
@eslint-react/ensure-forward-ref-using-ref
@eslint-react/no-useless-forward-ref
```

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

## What it does

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

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

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

## Examples

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

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

## Implementation

- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/ensure-forward-ref-using-ref.ts)
- [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)
- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-forward-ref.ts)
- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-forward-ref.spec.ts)

## Further Reading

- [React: forwardRef](https://react.dev/reference/react/forwardRef)

---

## See Also

- [`no-forward-ref`](./no-forward-ref)\
Replaces usages of `forwardRef` with passing `ref` as a prop.
Loading