Skip to content

Commit 85daffe

Browse files
authored
feat: add rules react/no-set-state-in-component-xxx, closes #197, closes #198, closes #199 (#201)
1 parent 9a0eaba commit 85daffe

16 files changed

+617
-26
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,9 @@ module.exports = {
129129
- [x] `react/no-script-url`
130130
- [ ] `react/no-direct-mutation-state`
131131
- [x] `react/no-redundant-should-component-update`
132-
- [ ] `react/no-set-state-in-component-did-mount`
133-
- [ ] `react/no-set-state-in-component-did-update`
134-
- [ ] `react/no-set-state-in-component-will-update`
132+
- [x] `react/no-set-state-in-component-did-mount`
133+
- [x] `react/no-set-state-in-component-did-update`
134+
- [x] `react/no-set-state-in-component-will-update`
135135
- [x] `react/no-component-will-mount`
136136
- [x] `react/no-component-will-update`
137137
- [x] `react/no-unsafe-component-will-mount`

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797
"taze": "0.13.0",
9898
"tiny-invariant": "1.3.1",
9999
"ts-pattern": "5.0.6",
100-
"turbo": "1.10.16",
100+
"turbo": "1.11.0",
101101
"type-fest": "4.8.3",
102102
"typedoc": "0.25.4",
103103
"typedoc-plugin-markdown": "3.17.1",

packages/eslint-plugin-react/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ import noNamespace from "./rules/no-namespace";
2525
import noRedundantShouldComponentUpdate from "./rules/no-redundant-should-component-update";
2626
import noRenderReturnValue from "./rules/no-render-return-value";
2727
import noScriptUrl from "./rules/no-script-url";
28+
import noSetStateInComponentDidMount from "./rules/no-set-state-in-component-did-mount";
29+
import noSetStateInComponentDidUpdate from "./rules/no-set-state-in-component-did-update";
30+
import noSetStateInComponentWillUpdate from "./rules/no-set-state-in-component-will-update";
2831
import noStringRefs from "./rules/no-string-refs";
2932
import noUnsafeComponentWillMount from "./rules/no-unsafe-component-will-mount";
3033
import noUnsafeComponentWillReceiveProps from "./rules/no-unsafe-component-will-receive-props";
@@ -65,6 +68,9 @@ export const rules = {
6568
"no-redundant-should-component-update": noRedundantShouldComponentUpdate,
6669
"no-render-return-value": noRenderReturnValue,
6770
"no-script-url": noScriptUrl,
71+
"no-set-state-in-component-did-mount": noSetStateInComponentDidMount,
72+
"no-set-state-in-component-did-update": noSetStateInComponentDidUpdate,
73+
"no-set-state-in-component-will-update": noSetStateInComponentWillUpdate,
6874
"no-string-refs": noStringRefs,
6975
"no-unsafe-component-will-mount": noUnsafeComponentWillMount,
7076
"no-unsafe-component-will-receive-props": noUnsafeComponentWillReceiveProps,
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# react/no-set-state-in-component-did-mount
2+
3+
## Rule category
4+
5+
Suspicious.
6+
7+
## What it does
8+
9+
Disallows calling `this.setState` in `componentDidMount` outside of functions, such as callbacks.
10+
11+
## Why is this bad?
12+
13+
Updating the state after a component mount will trigger a second render() call and can lead to property/layout thrashing.
14+
15+
## Examples
16+
17+
### ❌ Incorrect
18+
19+
```tsx
20+
import React from "react";
21+
22+
class MyComponent extends React.Component {
23+
componentDidMount() {
24+
this.setState({ name: "John" });
25+
}
26+
27+
render() {
28+
return <div>Hello {this.state.name}</div>;
29+
}
30+
}
31+
```
32+
33+
### ✅ Correct
34+
35+
```tsx
36+
import React from "react";
37+
38+
class MyComponent extends React.Component {
39+
componentDidMount() {
40+
this.onMount(function callback(newName) {
41+
this.setState({
42+
name: newName,
43+
});
44+
});
45+
}
46+
47+
render() {
48+
return <div>Hello {this.state.name}</div>;
49+
}
50+
}
51+
```
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import dedent from "dedent";
2+
3+
import { allValid, defaultParserOptions, RuleTester } from "../../../../test";
4+
import rule, { RULE_NAME } from "./no-set-state-in-component-did-mount";
5+
6+
const ruleTester = new RuleTester({
7+
parser: "@typescript-eslint/parser",
8+
parserOptions: defaultParserOptions,
9+
});
10+
11+
ruleTester.run(RULE_NAME, rule, {
12+
valid: [
13+
...allValid,
14+
dedent`
15+
class Foo extends React.Component {
16+
componentDidMount() {
17+
class Bar extends Baz {
18+
componentDidMount() {
19+
this.setState({ foo: "bar" });
20+
}
21+
}
22+
}
23+
}
24+
`,
25+
],
26+
invalid: [
27+
{
28+
code: dedent`
29+
class Foo extends React.Component {
30+
componentDidMount() {
31+
this.setState({ foo: "bar" });
32+
}
33+
}
34+
`,
35+
errors: [
36+
{ messageId: "NO_SET_STATE_IN_COMPONENT_DID_MOUNT" },
37+
],
38+
},
39+
{
40+
code: dedent`
41+
const Foo = class extends React.Component {
42+
componentDidMount() {
43+
this.setState({ foo: "bar" });
44+
}
45+
}
46+
`,
47+
errors: [
48+
{ messageId: "NO_SET_STATE_IN_COMPONENT_DID_MOUNT" },
49+
],
50+
},
51+
],
52+
});
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { isOneOf, NodeType, traverseUp } from "@eslint-react/ast";
2+
import { isClassComponent } from "@eslint-react/core";
3+
import { O } from "@eslint-react/tools";
4+
import type { TSESTree } from "@typescript-eslint/utils";
5+
import { ESLintUtils } from "@typescript-eslint/utils";
6+
import type { ConstantCase } from "string-ts";
7+
8+
import { createRule } from "../utils";
9+
10+
export const RULE_NAME = "no-set-state-in-component-did-mount";
11+
12+
export type MessageID = ConstantCase<typeof RULE_NAME>;
13+
14+
function isThisSetState(node: TSESTree.CallExpression) {
15+
const { callee } = node;
16+
17+
return (
18+
callee.type === NodeType.MemberExpression
19+
&& callee.object.type === NodeType.ThisExpression
20+
&& callee.property.type === NodeType.Identifier
21+
&& callee.property.name === "setState"
22+
);
23+
}
24+
25+
function isComponentDidMount(node: TSESTree.Node) {
26+
return isOneOf([NodeType.MethodDefinition, NodeType.PropertyDefinition])(node)
27+
&& node.key.type === NodeType.Identifier
28+
&& node.key.name === "componentDidMount";
29+
}
30+
31+
export default createRule<[], MessageID>({
32+
name: RULE_NAME,
33+
meta: {
34+
type: "problem",
35+
docs: {
36+
description: "disallow `setState` in `componentDidMount`",
37+
recommended: "recommended",
38+
requiresTypeChecking: false,
39+
},
40+
schema: [],
41+
messages: {
42+
NO_SET_STATE_IN_COMPONENT_DID_MOUNT: "Do not use `setState` in `componentDidMount`.",
43+
},
44+
},
45+
defaultOptions: [],
46+
create(context) {
47+
return {
48+
CallExpression(node) {
49+
if (!isThisSetState(node)) {
50+
return;
51+
}
52+
53+
const maybeParentClass = traverseUp(node, isOneOf([NodeType.ClassDeclaration, NodeType.ClassExpression]));
54+
55+
if (O.isNone(maybeParentClass)) {
56+
return;
57+
}
58+
59+
const parentClass = maybeParentClass.value;
60+
61+
if (!isClassComponent(parentClass, context)) {
62+
return;
63+
}
64+
65+
const maybeParentMethod = traverseUp(node, isComponentDidMount);
66+
67+
if (O.isNone(maybeParentMethod)) {
68+
return;
69+
}
70+
71+
const parentMethod = maybeParentMethod.value;
72+
73+
if (parentMethod.parent !== parentClass.body) {
74+
return;
75+
}
76+
77+
if (context.sourceCode.getScope?.(node).upper !== context.sourceCode.getScope?.(parentMethod)) {
78+
return;
79+
}
80+
81+
context.report({
82+
node,
83+
messageId: "NO_SET_STATE_IN_COMPONENT_DID_MOUNT",
84+
});
85+
},
86+
};
87+
},
88+
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# react/no-set-state-in-component-did-update
2+
3+
## Rule category
4+
5+
Suspicious.
6+
7+
## What it does
8+
9+
Disallows calling `this.setState` in `componentDidUpdate` outside of functions, such as callbacks.
10+
11+
## Why is this bad?
12+
13+
Updating the state after a component mount will trigger a second render() call and can lead to property/layout thrashing.
14+
15+
## Examples
16+
17+
### ❌ Incorrect
18+
19+
```tsx
20+
import React from "react";
21+
22+
class MyComponent extends React.Component {
23+
componentDidUpdate() {
24+
this.setState({ name: "John" });
25+
}
26+
27+
render() {
28+
return <div>Hello {this.state.name}</div>;
29+
}
30+
}
31+
```
32+
33+
### ✅ Correct
34+
35+
```tsx
36+
import React from "react";
37+
38+
class MyComponent extends React.Component {
39+
componentDidUpdate() {
40+
this.onMount(function callback(newName) {
41+
this.setState({
42+
name: newName,
43+
});
44+
});
45+
}
46+
47+
render() {
48+
return <div>Hello {this.state.name}</div>;
49+
}
50+
}
51+
```
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import dedent from "dedent";
2+
3+
import { allValid, defaultParserOptions, RuleTester } from "../../../../test";
4+
import rule, { RULE_NAME } from "./no-set-state-in-component-did-update";
5+
6+
const ruleTester = new RuleTester({
7+
parser: "@typescript-eslint/parser",
8+
parserOptions: defaultParserOptions,
9+
});
10+
11+
ruleTester.run(RULE_NAME, rule, {
12+
valid: [
13+
...allValid,
14+
dedent`
15+
class Foo extends React.Component {
16+
componentDidUpdate() {
17+
class Bar extends Baz {
18+
componentDidUpdate() {
19+
this.setState({ foo: "bar" });
20+
}
21+
}
22+
}
23+
}
24+
`,
25+
],
26+
invalid: [
27+
{
28+
code: dedent`
29+
class Foo extends React.Component {
30+
componentDidUpdate() {
31+
this.setState({ foo: "bar" });
32+
}
33+
}
34+
`,
35+
errors: [
36+
{ messageId: "NO_SET_STATE_IN_COMPONENT_DID_UPDATE" },
37+
],
38+
},
39+
{
40+
code: dedent`
41+
const Foo = class extends React.Component {
42+
componentDidUpdate() {
43+
this.setState({ foo: "bar" });
44+
}
45+
}
46+
`,
47+
errors: [
48+
{ messageId: "NO_SET_STATE_IN_COMPONENT_DID_UPDATE" },
49+
],
50+
},
51+
],
52+
});

0 commit comments

Comments
 (0)