Skip to content

Commit 76a663e

Browse files
committed
feat(react-x): add 'jsx-no-iife' rule, closes #1112
1 parent c1c4961 commit 76a663e

File tree

13 files changed

+480
-166
lines changed

13 files changed

+480
-166
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"---X Rules---",
55
"jsx-key-before-spread",
66
"jsx-no-duplicate-props",
7+
"jsx-no-iife",
78
"jsx-no-undef",
89
"jsx-uses-react",
910
"jsx-uses-vars",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ The `jsx-*` rules check for issues exclusive to JSX syntax, which are absent fro
3333
| :----------------------------------------------------------------------------------- | :-- | :-------: | :-------------------------------------------------------------------------------------------------- | :------: |
3434
| [`jsx-key-before-spread`](./jsx-key-before-spread) | 1️⃣ | | Enforces that the `key` attribute is placed before the spread attribute in JSX elements | |
3535
| [`jsx-no-duplicate-props`](./jsx-no-duplicate-props) | 1️⃣ | | Disallow duplicate props in JSX elements | |
36+
| [`jsx-no-iife`](./jsx-no-iife) | 0️⃣ | | Disallows `IIFE` in JSX elements | |
3637
| [`jsx-no-undef`](./jsx-no-undef) | 0️⃣ | | Disallow undefined variables in JSX elements | |
3738
| [`jsx-uses-react`](./jsx-uses-react) | 1️⃣ | | Marks React variables as used when JSX is used | |
3839
| [`jsx-uses-vars`](./jsx-uses-vars) | 1️⃣ | | Marks variables used in JSX elements as used | |

apps/website/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@
3737
"@eslint/markdown": "^6.4.0",
3838
"@local/configs": "workspace:*",
3939
"@mdx-js/mdx": "^3.1.0",
40-
"@tailwindcss/postcss": "^4.1.7",
40+
"@tailwindcss/postcss": "^4.1.8",
4141
"@theguild/remark-mermaid": "^0.3.0",
4242
"@tsconfig/next": "^2.0.3",
4343
"@tsconfig/node22": "^22.0.2",
4444
"@tsconfig/strictest": "^2.0.5",
4545
"@types/hast": "^3.0.4",
4646
"@types/mdx": "^2.0.13",
47-
"@types/node": "^22.15.22",
47+
"@types/node": "^22.15.24",
4848
"@types/react": "^19.1.6",
4949
"@types/react-dom": "^19.1.5",
5050
"autoprefixer": "^10.4.21",
@@ -57,8 +57,8 @@
5757
"eslint-plugin-react-refresh": "^0.4.20",
5858
"eslint-plugin-unicorn": "^59.0.1",
5959
"importx": "^0.5.2",
60-
"postcss": "^8.5.3",
61-
"tailwindcss": "^4.1.7",
60+
"postcss": "^8.5.4",
61+
"tailwindcss": "^4.1.8",
6262
"tailwindcss-animated": "^2.0.0",
6363
"typescript": "^5.8.3",
6464
"typescript-eslint": "^8.33.0"

examples/next-app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"@tsconfig/next": "^2.0.3",
2222
"@tsconfig/node22": "^22.0.2",
2323
"@tsconfig/strictest": "^2.0.5",
24-
"@types/node": "^22.15.22",
24+
"@types/node": "^22.15.24",
2525
"@types/react": "^19.1.6",
2626
"@types/react-dom": "^19.1.5",
2727
"eslint": "^9.27.0",

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,13 @@
5959
"@swc/core": "^1.11.29",
6060
"@tsconfig/node22": "^22.0.2",
6161
"@tsconfig/strictest": "^2.0.5",
62-
"@types/node": "^22.15.22",
62+
"@types/node": "^22.15.24",
6363
"@types/react": "^19.1.6",
6464
"@types/react-dom": "^19.1.5",
6565
"@typescript-eslint/parser": "^8.33.0",
6666
"@typescript-eslint/rule-tester": "^8.33.0",
6767
"@typescript-eslint/types": "^8.33.0",
68-
"ansis": "^4.0.0",
68+
"ansis": "^4.1.0",
6969
"cspell": "^9.0.2",
7070
"dedent": "^1.6.0",
7171
"dprint": "^0.50.0",

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import avoidShorthandBoolean from "./rules/avoid-shorthand-boolean";
33
import avoidShorthandFragment from "./rules/avoid-shorthand-fragment";
44
import jsxKeyBeforeSpread from "./rules/jsx-key-before-spread";
55
import jsxNoDuplicateProps from "./rules/jsx-no-duplicate-props";
6+
import jsxNoIife from "./rules/jsx-no-iife";
67
import jsxNoUndef from "./rules/jsx-no-undef";
78
import jsxUsesReact from "./rules/jsx-uses-react";
89
import jsxUsesVars from "./rules/jsx-uses-vars";
@@ -119,6 +120,7 @@ export const plugin = {
119120
// Part: JSX only rules
120121
"jsx-key-before-spread": jsxKeyBeforeSpread,
121122
"jsx-no-duplicate-props": jsxNoDuplicateProps,
123+
"jsx-no-iife": jsxNoIife,
122124
"jsx-no-undef": jsxNoUndef,
123125
"jsx-uses-react": jsxUsesReact,
124126
"jsx-uses-vars": jsxUsesVars,
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
---
2+
title: jsx-no-iife
3+
---
4+
5+
**Full Name in `eslint-plugin-react-x`**
6+
7+
```sh copy
8+
react-x/jsx-no-iife
9+
```
10+
11+
**Full Name in `@eslint-react/eslint-plugin`**
12+
13+
```sh copy
14+
@eslint-react/jsx-no-iife
15+
```
16+
17+
## Description
18+
19+
Disallows `IIFE` in JSX elements.
20+
21+
Technically, this is valid JS, but it is not conventional inside React components. `IIFE` in JSX may be hard to follow and they will probably not optimized by [React Compiler](https://react.dev/learn/react-compiler), which means slower app rendering.
22+
23+
## Examples
24+
25+
### Failing
26+
27+
```tsx
28+
function MyComponent() {
29+
// hooks etc
30+
31+
return (
32+
<SomeJsx>
33+
<SomeMoreJsx />
34+
35+
{(() => {
36+
const filteredThings = things.filter(callback);
37+
38+
if (filteredThings.length === 0) {
39+
return <Empty />;
40+
}
41+
42+
return filteredThings.map((thing) => <Thing key={thing.id} data={thing} />);
43+
})()}
44+
45+
<SomeMoreJsx />
46+
</SomeJsx>
47+
);
48+
}
49+
```
50+
51+
### Passing
52+
53+
```tsx
54+
function MyComponent() {
55+
// hooks etc
56+
57+
const thingsList = things.filter(callback);
58+
59+
return (
60+
<SomeJsx>
61+
<SomeMoreJsx />
62+
{thingsList.length === 0
63+
? <Empty />
64+
: thingsList.map((thing) => <Thing key={thing.id} data={thing} />)}
65+
<SomeMoreJsx />
66+
</SomeJsx>
67+
);
68+
}
69+
```
70+
71+
```tsx
72+
function MyComponent() {
73+
// hooks etc
74+
75+
const thingsList = useMemo(() => {
76+
const filteredThings = things.filter(callback);
77+
78+
if (filteredThings.length === 0) {
79+
return <Empty />;
80+
}
81+
82+
return filteredThings.map((thing) => <Thing key={thing.id} data={thing} />);
83+
}, [things]);
84+
85+
return (
86+
<SomeJsx>
87+
<SomeMoreJsx />
88+
{thingsList}
89+
<SomeMoreJsx />
90+
</SomeJsx>
91+
);
92+
}
93+
```
94+
95+
### Passing But Not Recommended
96+
97+
```tsx
98+
function MyComponent() {
99+
// hooks etc
100+
101+
const thingsList = (() => {
102+
const filteredThings = things.filter(callback);
103+
104+
if (filteredThings.length === 0) {
105+
return <Empty />;
106+
}
107+
108+
return filteredThings.map((thing) => <Thing key={thing.id} data={thing} />);
109+
})();
110+
111+
return (
112+
<SomeJsx>
113+
<SomeMoreJsx />
114+
{thingsList}
115+
<SomeMoreJsx />
116+
</SomeJsx>
117+
);
118+
}
119+
```
120+
121+
## Implementation
122+
123+
- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/jsx-no-iife.ts)
124+
- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/jsx-no-iife.spec.ts)
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import tsx from "dedent";
2+
3+
import { ruleTester } from "../../../../../test";
4+
import rule, { RULE_NAME } from "./jsx-no-iife";
5+
6+
ruleTester.run(RULE_NAME, rule, {
7+
invalid: [
8+
{
9+
code: tsx`
10+
function MyComponent() {
11+
// hooks etc
12+
13+
return (
14+
<SomeJsx>
15+
<SomeMoreJsx />
16+
17+
{(() => {
18+
const filteredThings = things.filter(callback);
19+
20+
if (filteredThings.length === 0) {
21+
return <Empty />;
22+
}
23+
24+
return filteredThings.map((thing) => <Thing key={thing.id} data={thing} />);
25+
})()}
26+
27+
<SomeMoreJsx />
28+
</SomeJsx>
29+
);
30+
}
31+
`,
32+
errors: [
33+
{
34+
messageId: "jsxNoIife",
35+
data: { name: "Foo" },
36+
},
37+
],
38+
},
39+
],
40+
valid: [
41+
tsx`
42+
function MyComponent() {
43+
// hooks etc
44+
45+
const someThings = (() => {
46+
const filteredThings = things.filter(callback);
47+
48+
if (filteredThings.length === 0) {
49+
return <Empty />;
50+
}
51+
52+
return filteredThings.map((thing) => <Thing key={thing.id} data={thing} />);
53+
})();
54+
55+
return (
56+
<SomeJsx>
57+
<SomeMoreJsx />
58+
{someThings}
59+
<SomeMoreJsx />
60+
</SomeJsx>
61+
);
62+
}
63+
`,
64+
tsx`
65+
function MyComponent() {
66+
// hooks etc
67+
68+
const someThings = useMemo(() => {
69+
const filteredThings = things.filter(callback);
70+
71+
if (filteredThings.length === 0) {
72+
return <Empty />;
73+
}
74+
75+
return filteredThings.map((thing) => <Thing key={thing.id} data={thing} />);
76+
}, [things]);
77+
78+
return (
79+
<SomeJsx>
80+
<SomeMoreJsx />
81+
{someThings}
82+
<SomeMoreJsx />
83+
</SomeJsx>
84+
);
85+
}
86+
`,
87+
tsx`
88+
function MyComponent() {
89+
// hooks etc
90+
91+
const thingsList = useMemo(() => {
92+
const filteredThings = things.filter(callback);
93+
94+
if (filteredThings.length === 0) {
95+
return <Empty />;
96+
}
97+
98+
return filteredThings.map((thing) => <Thing key={thing.id} data={thing} />);
99+
}, [things]);
100+
101+
return (
102+
<SomeJsx>
103+
<SomeMoreJsx />
104+
{thingsList}
105+
<SomeMoreJsx />
106+
</SomeJsx>
107+
);
108+
}
109+
`,
110+
tsx`
111+
function MyComponent() {
112+
// hooks etc
113+
114+
const thingsList = (() => {
115+
const filteredThings = things.filter(callback);
116+
117+
if (filteredThings.length === 0) {
118+
return <Empty />;
119+
}
120+
121+
return filteredThings.map((thing) => <Thing key={thing.id} data={thing} />);
122+
})();
123+
124+
return (
125+
<SomeJsx>
126+
<SomeMoreJsx />
127+
{thingsList}
128+
<SomeMoreJsx />
129+
</SomeJsx>
130+
);
131+
}
132+
`,
133+
],
134+
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type * as AST from "@eslint-react/ast";
2+
import type { RuleContext, RuleFeature } from "@eslint-react/kit";
3+
4+
import type { RuleListener } from "@typescript-eslint/utils/ts-eslint";
5+
import type { CamelCase } from "string-ts";
6+
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
7+
import { createRule } from "../utils";
8+
9+
export const RULE_NAME = "jsx-no-iife";
10+
11+
export const RULE_FEATURES = [] as const satisfies RuleFeature[];
12+
13+
export type MessageID = CamelCase<typeof RULE_NAME>;
14+
15+
export default createRule<[], MessageID>({
16+
meta: {
17+
type: "problem",
18+
docs: {
19+
description: "Disallows 'IIFE' in JSX elements.",
20+
[Symbol.for("rule_features")]: RULE_FEATURES,
21+
},
22+
messages: {
23+
jsxNoIife: "",
24+
},
25+
schema: [],
26+
},
27+
name: RULE_NAME,
28+
create,
29+
defaultOptions: [],
30+
});
31+
32+
export function create(context: RuleContext<MessageID, []>): RuleListener {
33+
return {
34+
"JSXElement :function"(node: AST.TSESTreeFunction) {
35+
if (node.parent.type === T.CallExpression && node.parent.callee === node) {
36+
context.report({
37+
messageId: "jsxNoIife",
38+
node: node.parent,
39+
});
40+
}
41+
},
42+
"JSXFragment :function"(node: AST.TSESTreeFunction) {
43+
if (node.parent.type === T.CallExpression && node.parent.callee === node) {
44+
context.report({
45+
messageId: "jsxNoIife",
46+
node: node.parent,
47+
});
48+
}
49+
},
50+
};
51+
}

0 commit comments

Comments
 (0)