Skip to content

Commit b4c3914

Browse files
committed
wip: add 'react-dom/no-render'
1 parent a52706f commit b4c3914

File tree

3 files changed

+173
-0
lines changed

3 files changed

+173
-0
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
---
2+
title: no-render
3+
---
4+
5+
**Full Name in `eslint-plugin-react-dom`**
6+
7+
```plain copy
8+
react-dom/no-render
9+
```
10+
11+
**Full Name in `@eslint-react/eslint-plugin`**
12+
13+
```plain copy
14+
@eslint-react/dom/no-render
15+
```
16+
17+
**Features**
18+
19+
`🔍` `🔄`
20+
21+
**Presets**
22+
23+
- `dom`
24+
- `recommended`
25+
- `recommended-typescript`
26+
- `recommended-type-checked`
27+
28+
## What it does
29+
30+
Replaces usages of `ReactDom.render()` with `createRoot(node).render()`.
31+
32+
An **unsafe** codemod is available for this rule.
33+
34+
## Examples
35+
36+
### Failing
37+
38+
```tsx
39+
import ReactDom from "react-dom";
40+
import Component from "Component";
41+
42+
ReactDom.render(<Component />, document.getElementById("app"));
43+
```
44+
45+
### Passing
46+
47+
```tsx
48+
import { createRoot } from "react-dom/client";
49+
import ReactDom from "react-dom";
50+
import Component from "Component";
51+
52+
const root = createRoot(document.getElementById("app"));
53+
root.render(<Component />);
54+
```
55+
56+
## Implementation
57+
58+
- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom/src/rules/no-render.ts)
59+
- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom/src/rules/no-render.spec.ts)
60+
61+
## Further Reading
62+
63+
---
64+
65+
## See Also
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { ruleTester } from "../../../../../test";
2+
import rule, { RULE_NAME } from "./no-render";
3+
4+
ruleTester.run(RULE_NAME, rule, {
5+
invalid: [
6+
{
7+
code: /* tsx */ `
8+
import ReactDom from "react-dom";
9+
import Component from "Component";
10+
11+
ReactDom.render(<Component />, document.getElementById("app"));
12+
`,
13+
errors: [{ messageId: "noRender" }],
14+
},
15+
],
16+
valid: [
17+
/* tsx */ `
18+
import { createRoot } from "react-dom/client";
19+
import ReactDom from "react-dom";
20+
import Component from "Component";
21+
22+
const root = createRoot(document.getElementById("app"));
23+
root.render(<Component />);
24+
`,
25+
],
26+
});
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import type { RuleFeature } from "@eslint-react/shared";
2+
import { getSettingsFromContext } from "@eslint-react/shared";
3+
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
4+
import { compare } from "compare-versions";
5+
import type { CamelCase } from "string-ts";
6+
7+
import { createRule } from "../utils";
8+
9+
export const RULE_NAME = "no-render";
10+
11+
export const RULE_FEATURES = [
12+
"CHK",
13+
"MOD",
14+
] as const satisfies RuleFeature[];
15+
16+
export type MessageID = CamelCase<typeof RULE_NAME>;
17+
18+
export default createRule<[], MessageID>({
19+
meta: {
20+
type: "problem",
21+
docs: {
22+
description: "replace usages of 'ReactDom.render()' with 'createRoot(node).render()'",
23+
[Symbol.for("rule_features")]: RULE_FEATURES,
24+
},
25+
fixable: "code",
26+
messages: {
27+
noRender: "[Deprecated] Use 'createRoot(node).render()' instead.",
28+
},
29+
schema: [],
30+
},
31+
name: RULE_NAME,
32+
create(context) {
33+
if (!context.sourceCode.text.includes("render")) return {};
34+
const settings = getSettingsFromContext(context);
35+
if (compare(settings.version, "19.0.0", "<")) {
36+
return {};
37+
}
38+
const reactDomNames = new Set<string>();
39+
const renderNames = new Set<string>();
40+
return {
41+
CallExpression(node) {
42+
switch (true) {
43+
case node.callee.type === T.Identifier && renderNames.has(node.callee.name):
44+
context.report({
45+
messageId: "noRender",
46+
node,
47+
});
48+
return;
49+
case node.callee.type === T.MemberExpression
50+
&& node.callee.object.type === T.Identifier
51+
&& reactDomNames.has(node.callee.object.name)
52+
&& node.callee.property.type === T.Identifier
53+
&& node.callee.property.name === "render":
54+
context.report({
55+
messageId: "noRender",
56+
node,
57+
});
58+
return;
59+
}
60+
},
61+
ImportDeclaration(node) {
62+
const [baseSource] = node.source.value.split("/");
63+
if (baseSource !== "react-dom") return;
64+
for (const specifier of node.specifiers) {
65+
switch (specifier.type) {
66+
case T.ImportSpecifier:
67+
if (specifier.imported.type !== T.Identifier) continue;
68+
if (specifier.imported.name === "render") {
69+
renderNames.add(specifier.local.name);
70+
}
71+
continue;
72+
case T.ImportDefaultSpecifier:
73+
case T.ImportNamespaceSpecifier:
74+
reactDomNames.add(specifier.local.name);
75+
continue;
76+
}
77+
}
78+
},
79+
};
80+
},
81+
defaultOptions: [],
82+
});

0 commit comments

Comments
 (0)