diff --git a/apps/website/content/docs/rules/meta.json b/apps/website/content/docs/rules/meta.json index c8dae46f2..82297ed17 100644 --- a/apps/website/content/docs/rules/meta.json +++ b/apps/website/content/docs/rules/meta.json @@ -45,6 +45,7 @@ "no-set-state-in-component-did-update", "no-set-state-in-component-will-update", "no-string-refs", + "no-unnecessary-key", "no-unnecessary-use-callback", "no-unnecessary-use-memo", "no-unnecessary-use-prefix", diff --git a/apps/website/content/docs/rules/overview.mdx b/apps/website/content/docs/rules/overview.mdx index 58e46132f..3a270bf85 100644 --- a/apps/website/content/docs/rules/overview.mdx +++ b/apps/website/content/docs/rules/overview.mdx @@ -70,6 +70,7 @@ full: true | [`no-set-state-in-component-did-update`](./no-set-state-in-component-did-update) | 1️⃣ | | Disallow calling `this.setState` in `componentDidUpdate` outside of functions, such as callbacks | | | [`no-set-state-in-component-will-update`](./no-set-state-in-component-will-update) | 1️⃣ | | Disallow calling `this.setState` in `componentWillUpdate` outside of functions, such as callbacks | | | [`no-string-refs`](./no-string-refs) | 2️⃣ | `🔄` | Replaces string refs with callback refs | >=16.3.0 | +| [`no-unnecessary-key`](./no-unnecessary-key) | 0️⃣ | `🧪` | Prevents the use of unnecessary `key` props on JSX elements when rendering lists | | | [`no-unnecessary-use-callback`](./no-unnecessary-use-callback) | 0️⃣ | `🧪` | Disallow unnecessary usage of `useCallback` | | | [`no-unnecessary-use-memo`](./no-unnecessary-use-memo) | 0️⃣ | `🧪` | Disallow unnecessary usage of `useMemo` | | | [`no-unnecessary-use-prefix`](./no-unnecessary-use-prefix) | 0️⃣ | | Enforces that a function with the `use` prefix should use at least one Hook inside of it | | diff --git a/packages/plugins/eslint-plugin-react-x/src/configs/recommended-type-checked.ts b/packages/plugins/eslint-plugin-react-x/src/configs/recommended-type-checked.ts index caee1003d..fb2caef33 100644 --- a/packages/plugins/eslint-plugin-react-x/src/configs/recommended-type-checked.ts +++ b/packages/plugins/eslint-plugin-react-x/src/configs/recommended-type-checked.ts @@ -8,7 +8,6 @@ export const rules = { ...recommendedTypeScript.rules, "react-x/no-leaked-conditional-rendering": "warn", "react-x/no-unused-props": "warn", - // "react-x/prefer-read-only-props": "warn", } as const satisfies RulePreset; export const settings = { diff --git a/packages/plugins/eslint-plugin-react-x/src/configs/recommended.ts b/packages/plugins/eslint-plugin-react-x/src/configs/recommended.ts index 7a3ad3db9..2e7912f16 100644 --- a/packages/plugins/eslint-plugin-react-x/src/configs/recommended.ts +++ b/packages/plugins/eslint-plugin-react-x/src/configs/recommended.ts @@ -6,22 +6,15 @@ export const name = "react-x/recommended"; export const rules = { "react-x/jsx-no-comment-textnodes": "warn", "react-x/jsx-no-duplicate-props": "warn", - // "react-x/jsx-no-iife": "warn", - // "react-x/jsx-no-undef": "error", - // "react-x/jsx-shorthand-boolean": "warn", - // "react-x/jsx-shorthand-fragment": "warn", "react-x/jsx-uses-react": "warn", "react-x/jsx-uses-vars": "warn", - "react-x/no-access-state-in-setstate": "error", "react-x/no-array-index-key": "warn", "react-x/no-children-count": "warn", "react-x/no-children-for-each": "warn", "react-x/no-children-map": "warn", "react-x/no-children-only": "warn", - // "react-x/no-children-prop": "warn", "react-x/no-children-to-array": "warn", - // "react-x/no-class-component": "warn", "react-x/no-clone-element": "warn", "react-x/no-comment-textnodes": "warn", "react-x/no-component-will-mount": "error", @@ -34,11 +27,7 @@ export const rules = { "react-x/no-duplicate-key": "error", "react-x/no-forward-ref": "warn", "react-x/no-implicit-key": "warn", - // "react-x/no-leaked-conditional-rendering": "warn", - // "react-x/no-missing-component-display-name": "warn", - // "react-x/no-missing-context-display-name": "warn", "react-x/no-missing-key": "error", - // "react-x/no-misused-capture-owner-stack": "error", "react-x/no-nested-component-definitions": "error", "react-x/no-nested-lazy-component-declarations": "error", "react-x/no-prop-types": "error", @@ -47,8 +36,6 @@ export const rules = { "react-x/no-set-state-in-component-did-update": "warn", "react-x/no-set-state-in-component-will-update": "warn", "react-x/no-string-refs": "error", - // "react-x/no-unnecessary-use-callback": "warn", - // "react-x/no-unnecessary-use-memo": "warn", "react-x/no-unnecessary-use-prefix": "warn", "react-x/no-unsafe-component-will-mount": "warn", "react-x/no-unsafe-component-will-receive-props": "warn", @@ -56,14 +43,9 @@ export const rules = { "react-x/no-unstable-context-value": "warn", "react-x/no-unstable-default-props": "warn", "react-x/no-unused-class-component-members": "warn", - // "react-x/no-unused-props": "warn", "react-x/no-unused-state": "warn", "react-x/no-use-context": "warn", "react-x/no-useless-forward-ref": "warn", - // "react-x/no-useless-fragment": "warn", - // "react-x/prefer-destructuring-assignment": "warn", - // "react-x/prefer-namespace-import": "warn", - // "react-x/prefer-read-only-props": "error", "react-x/prefer-use-state-lazy-initialization": "warn", } as const satisfies RulePreset; diff --git a/packages/plugins/eslint-plugin-react-x/src/plugin.ts b/packages/plugins/eslint-plugin-react-x/src/plugin.ts index 885bda8c5..124735805 100644 --- a/packages/plugins/eslint-plugin-react-x/src/plugin.ts +++ b/packages/plugins/eslint-plugin-react-x/src/plugin.ts @@ -43,6 +43,7 @@ import noSetStateInComponentDidMount from "./rules/no-set-state-in-component-did import noSetStateInComponentDidUpdate from "./rules/no-set-state-in-component-did-update"; import noSetStateInComponentWillUpdate from "./rules/no-set-state-in-component-will-update"; import noStringRefs from "./rules/no-string-refs"; +import noUnnecessaryKey from "./rules/no-unnecessary-key"; import noUnnecessaryUseCallback from "./rules/no-unnecessary-use-callback"; import noUnnecessaryUseMemo from "./rules/no-unnecessary-use-memo"; import noUnnecessaryUsePrefix from "./rules/no-unnecessary-use-prefix"; @@ -62,6 +63,7 @@ import preferNamespaceImport from "./rules/prefer-namespace-import"; import preferReadOnlyProps from "./rules/prefer-read-only-props"; import preferUseStateLazyInitialization from "./rules/prefer-use-state-lazy-initialization"; +// Removed rules import avoidShorthandBoolean from "./rules-removed/avoid-shorthand-boolean"; import avoidShorthandFragment from "./rules-removed/avoid-shorthand-fragment"; import noCommentTextnodes from "./rules-removed/no-comment-textnodes"; @@ -119,6 +121,7 @@ export const plugin = { "no-set-state-in-component-did-update": noSetStateInComponentDidUpdate, "no-set-state-in-component-will-update": noSetStateInComponentWillUpdate, "no-string-refs": noStringRefs, + "no-unnecessary-key": noUnnecessaryKey, "no-unnecessary-use-callback": noUnnecessaryUseCallback, "no-unnecessary-use-memo": noUnnecessaryUseMemo, "no-unnecessary-use-prefix": noUnnecessaryUsePrefix, diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-key.mdx b/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-key.mdx new file mode 100644 index 000000000..dbf985d6b --- /dev/null +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-key.mdx @@ -0,0 +1,105 @@ +--- +title: no-unnecessary-key +--- + +**Full Name in `eslint-plugin-react-x`** + +```sh copy +react-x/no-unnecessary-key +``` + +**Full Name in `@eslint-react/eslint-plugin`** + +```sh copy +@eslint-react/no-unnecessary-key +``` + +**Features** + +`🧪` + +## Description + +This rule prevents the use of unnecessary `key` props on JSX elements when rendering lists. + +When rendering a list of elements in React, the `key` prop should only be placed on the outermost element for each item in the list. Adding keys to nested child elements is redundant, can cause confusion, and may lead to subtle bugs during refactoring. + +For example, if an element with a `key` is wrapped with a `React.Fragment` or another component, the `key` must be moved to the new wrapping element. Forgetting to remove the original `key` from the child element can lead to runtime warnings from React if it's duplicated, or simply leave unnecessary code. This rule helps identify and remove these redundant `key` props. + +## Examples + +### Failing + +```jsx +// A key on a child element inside a map is unnecessary +things.map(thing => ( +
Hello
{/* 🚨 This key is unnecessary */} +)}
Hello