diff --git a/apps/website/content/docs/rules/meta.json b/apps/website/content/docs/rules/meta.json
index 82f7537aca..48ea474435 100644
--- a/apps/website/content/docs/rules/meta.json
+++ b/apps/website/content/docs/rules/meta.json
@@ -53,6 +53,7 @@
"prefer-shorthand-boolean",
"prefer-shorthand-fragment",
"jsx-no-duplicate-props",
+ "jsx-no-undef",
"jsx-uses-vars",
"---DOM Rules---",
"dom-no-dangerously-set-innerhtml",
diff --git a/apps/website/content/docs/rules/overview.mdx b/apps/website/content/docs/rules/overview.mdx
index e5a92791a3..756b8da7eb 100644
--- a/apps/website/content/docs/rules/overview.mdx
+++ b/apps/website/content/docs/rules/overview.mdx
@@ -76,6 +76,7 @@ Linter rules can have false positives, false negatives, and some rules are depen
| [`prefer-shorthand-boolean`](./prefer-shorthand-boolean) | 0️⃣ | `🔧` | Enforces shorthand syntax for boolean attributes | |
| [`prefer-shorthand-fragment`](./prefer-shorthand-fragment) | 0️⃣ | `🔧` | Enforces shorthand syntax for fragments | |
| [`jsx-no-duplicate-props`](./jsx-no-duplicate-props) | 1️⃣ | | Disallow duplicate props in JSX elements | |
+| [`jsx-no-undef`](./jsx-no-undef) | 2️⃣ | | Disallow undefined variables in JSX elements | |
| [`jsx-uses-vars`](./jsx-uses-vars) | 1️⃣ | | Marks variables used in JSX as used | |
## DOM Rules
diff --git a/packages/plugins/eslint-plugin-react-x/src/configs/recommended-typescript.ts b/packages/plugins/eslint-plugin-react-x/src/configs/recommended-typescript.ts
index 346fce04c6..8bf3053930 100644
--- a/packages/plugins/eslint-plugin-react-x/src/configs/recommended-typescript.ts
+++ b/packages/plugins/eslint-plugin-react-x/src/configs/recommended-typescript.ts
@@ -7,6 +7,7 @@ export const name = "react-x/recommended-typescript";
export const rules = {
...recommended.rules,
"react-x/jsx-no-duplicate-props": "off",
+ "react-x/jsx-no-undef": "off",
"react-x/jsx-uses-vars": "off",
} as const satisfies RulePreset;
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 e9555ce863..ebc63b5f2e 100644
--- a/packages/plugins/eslint-plugin-react-x/src/configs/recommended.ts
+++ b/packages/plugins/eslint-plugin-react-x/src/configs/recommended.ts
@@ -4,6 +4,9 @@ import { DEFAULT_ESLINT_REACT_SETTINGS } from "@eslint-react/shared";
export const name = "react-x/recommended";
export const rules = {
+ "react-x/jsx-no-duplicate-props": "warn",
+ "react-x/jsx-no-undef": "error",
+ "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",
@@ -20,7 +23,6 @@ export const rules = {
"react-x/no-create-ref": "error",
"react-x/no-default-props": "error",
"react-x/no-direct-mutation-state": "error",
- "react-x/jsx-no-duplicate-props": "warn",
"react-x/no-duplicate-key": "warn",
"react-x/no-forward-ref": "warn",
"react-x/no-implicit-key": "warn",
@@ -41,7 +43,6 @@ export const rules = {
"react-x/no-unused-state": "warn",
"react-x/no-use-context": "warn",
"react-x/no-useless-forward-ref": "warn",
- "react-x/jsx-uses-vars": "warn",
} as const satisfies RulePreset;
export const settings = {
diff --git a/packages/plugins/eslint-plugin-react-x/src/plugin.ts b/packages/plugins/eslint-plugin-react-x/src/plugin.ts
index 29eb2b74c0..4e485b401e 100644
--- a/packages/plugins/eslint-plugin-react-x/src/plugin.ts
+++ b/packages/plugins/eslint-plugin-react-x/src/plugin.ts
@@ -2,6 +2,7 @@ import { name, version } from "../package.json";
import avoidShorthandBoolean from "./rules/avoid-shorthand-boolean";
import avoidShorthandFragment from "./rules/avoid-shorthand-fragment";
import jsxNoDuplicateProps from "./rules/jsx-no-duplicate-props";
+import jsxNoUndef from "./rules/jsx-no-undef";
import jsxUsesVars from "./rules/jsx-uses-vars";
import noAccessStateInSetstate from "./rules/no-access-state-in-setstate";
import noArrayIndexKey from "./rules/no-array-index-key";
@@ -111,6 +112,7 @@ export const plugin = {
// Part: JSX only rules
"jsx-no-duplicate-props": jsxNoDuplicateProps,
+ "jsx-no-undef": jsxNoUndef,
"jsx-uses-vars": jsxUsesVars,
// Part: deprecated rules
diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-no-undef.md b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-no-undef.md
new file mode 100644
index 0000000000..11a5264bc5
--- /dev/null
+++ b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-no-undef.md
@@ -0,0 +1,69 @@
+---
+title: jsx-no-undef
+---
+
+**Full Name in `eslint-plugin-react-x`**
+
+```plain copy
+react-x/jsx-no-undef
+```
+
+**Full Name in `@eslint-react/eslint-plugin`**
+
+```plain copy
+@eslint-react/jsx-no-undef
+```
+
+**Presets**
+
+- `core`
+- `recommended`
+
+## Description
+
+This rule is used to prevent the use of undefined variables in JSX. It checks for any undefined variables in the JSX code and reports them as errors.
+
+## Examples
+
+### Failing
+
+```jsx
+const MyComponent = () => {
+ return (
+
+
+
+ );
+};
+```
+
+### Passing
+
+```jsx
+import Foo from "./Foo";
+
+const MyComponent = () => {
+ return (
+
+
+
+ );
+};
+```
+
+```jsx
+const Foo = () => Foo
;
+
+const MyComponent = () => {
+ return (
+
+
+
+ );
+};
+```
+
+## Implementation
+
+- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/jsx-no-undef.ts)
+- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/jsx-no-undef.spec.ts)
diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-no-undef.spec.ts b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-no-undef.spec.ts
new file mode 100644
index 0000000000..c2d99aea7d
--- /dev/null
+++ b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-no-undef.spec.ts
@@ -0,0 +1,76 @@
+import tsx from "dedent";
+
+import { ruleTester } from "../../../../../test";
+import rule, { RULE_NAME } from "./jsx-no-undef";
+
+ruleTester.run(RULE_NAME, rule, {
+ invalid: [
+ {
+ code: tsx`
+ const element = ;
+ `,
+ errors: [
+ {
+ messageId: "jsxNoUndef",
+ data: { name: "Foo" },
+ },
+ ],
+ },
+ {
+ code: tsx`
+ const element = ;
+ const element = ;
+ `,
+ errors: [
+ {
+ messageId: "jsxNoUndef",
+ data: { name: "Foo" },
+ },
+ {
+ messageId: "jsxNoUndef",
+ data: { name: "Bar" },
+ },
+ ],
+ },
+ {
+ code: tsx`
+ function Foo() {
+ return ;
+ }
+ `,
+ errors: [
+ {
+ messageId: "jsxNoUndef",
+ data: { name: "Bar" },
+ },
+ ],
+ },
+ ],
+ valid: [
+ {
+ code: tsx`
+ function Foo() {
+ return ;
+ }
+ function Bar() {
+ return ;
+ }
+ `,
+ },
+ {
+ code: tsx`
+ import { Foo } from "./Foo";
+ import { Bar } from "./Bar";
+
+ function App() {
+ return (
+
+
+
+
+ );
+ }
+ `,
+ },
+ ],
+});
diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-no-undef.ts b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-no-undef.ts
new file mode 100644
index 0000000000..e8d914a424
--- /dev/null
+++ b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-no-undef.ts
@@ -0,0 +1,56 @@
+import type { RuleContext, RuleFeature } from "@eslint-react/kit";
+import type { RuleListener } from "@typescript-eslint/utils/ts-eslint";
+import type { CamelCase } from "string-ts";
+import * as VAR from "@eslint-react/var";
+
+import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
+import { createRule } from "../utils";
+
+export const RULE_NAME = "jsx-no-undef";
+
+export const RULE_FEATURES = [] as const satisfies RuleFeature[];
+
+export type MessageID = CamelCase;
+
+export default createRule<[], MessageID>({
+ meta: {
+ type: "problem",
+ docs: {
+ description: "Disallow undefined variables in JSX.",
+ [Symbol.for("rule_features")]: RULE_FEATURES,
+ },
+ messages: {
+ jsxNoUndef: "JSX variable {{name}} is not defined.",
+ },
+ schema: [],
+ },
+ name: RULE_NAME,
+ create,
+ defaultOptions: [],
+});
+
+export function create(context: RuleContext): RuleListener {
+ return {
+ JSXIdentifier(node) {
+ if (node.name === "this") {
+ return;
+ }
+ // Skip JsxIntrinsicElements
+ if (/^[a-z]/u.test(node.name)) {
+ return;
+ }
+ // Skip JSXMemberExpression property
+ if (node.parent.type === T.JSXMemberExpression && node.parent.property === node) {
+ return;
+ }
+ const initialScope = context.sourceCode.getScope(node);
+ if (VAR.findVariable(node.name, initialScope) == null) {
+ context.report({
+ messageId: "jsxNoUndef",
+ node,
+ data: { name: node.name },
+ });
+ }
+ },
+ };
+}
diff --git a/packages/plugins/eslint-plugin/src/configs/all.ts b/packages/plugins/eslint-plugin/src/configs/all.ts
index dc537151ed..3b356743f7 100644
--- a/packages/plugins/eslint-plugin/src/configs/all.ts
+++ b/packages/plugins/eslint-plugin/src/configs/all.ts
@@ -12,6 +12,9 @@ export const name = "@eslint-react/all";
export const rules = {
"@eslint-react/avoid-shorthand-boolean": "off",
"@eslint-react/avoid-shorthand-fragment": "off",
+ "@eslint-react/jsx-no-duplicate-props": "warn",
+ "@eslint-react/jsx-no-undef": "error",
+ "@eslint-react/jsx-uses-vars": "warn",
"@eslint-react/no-access-state-in-setstate": "error",
"@eslint-react/no-array-index-key": "warn",
"@eslint-react/no-children-count": "warn",
@@ -31,7 +34,6 @@ export const rules = {
"@eslint-react/no-create-ref": "error",
"@eslint-react/no-default-props": "error",
"@eslint-react/no-direct-mutation-state": "error",
- "@eslint-react/jsx-no-duplicate-props": "warn",
"@eslint-react/no-duplicate-key": "warn",
"@eslint-react/no-forward-ref": "warn",
"@eslint-react/no-implicit-key": "warn",
@@ -58,7 +60,6 @@ export const rules = {
"@eslint-react/prefer-destructuring-assignment": "warn",
"@eslint-react/prefer-shorthand-boolean": "warn",
"@eslint-react/prefer-shorthand-fragment": "warn",
- "@eslint-react/jsx-uses-vars": "warn",
"@eslint-react/dom/no-dangerously-set-innerhtml": "warn",
"@eslint-react/dom/no-dangerously-set-innerhtml-with-children": "error",
diff --git a/packages/plugins/eslint-plugin/src/configs/core.ts b/packages/plugins/eslint-plugin/src/configs/core.ts
index 4d8c674cbc..b361fed188 100644
--- a/packages/plugins/eslint-plugin/src/configs/core.ts
+++ b/packages/plugins/eslint-plugin/src/configs/core.ts
@@ -5,6 +5,9 @@ import reactx from "eslint-plugin-react-x";
export const name = "@eslint-react/core";
export const rules = {
+ "@eslint-react/jsx-no-duplicate-props": "warn",
+ "@eslint-react/jsx-no-undef": "error",
+ "@eslint-react/jsx-uses-vars": "warn",
"@eslint-react/no-access-state-in-setstate": "error",
"@eslint-react/no-array-index-key": "warn",
"@eslint-react/no-children-count": "warn",
@@ -21,7 +24,6 @@ export const rules = {
"@eslint-react/no-create-ref": "error",
"@eslint-react/no-default-props": "error",
"@eslint-react/no-direct-mutation-state": "error",
- "@eslint-react/jsx-no-duplicate-props": "warn",
"@eslint-react/no-duplicate-key": "warn",
"@eslint-react/no-forward-ref": "warn",
"@eslint-react/no-implicit-key": "warn",
@@ -43,7 +45,6 @@ export const rules = {
"@eslint-react/no-use-context": "warn",
"@eslint-react/no-useless-forward-ref": "warn",
"@eslint-react/no-useless-fragment": "warn",
- "@eslint-react/jsx-uses-vars": "warn",
} as const satisfies RulePreset;
export const plugins = {