Skip to content

Commit 987a95e

Browse files
cheeZeryadidahiya
authored andcommitted
New rule no-access-state-in-setstate (#190)
1 parent 61e2f50 commit 987a95e

File tree

4 files changed

+206
-0
lines changed

4 files changed

+206
-0
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ The built-in configuration preset you get with `"extends": "tslint-react"` is se
5252
size={size}
5353
/>
5454
```
55+
- Rule options: _none_
5556
- `jsx-ban-elements` (since v3.4.0)
5657
- Allows blacklisting of JSX elements with an optional explanatory message in the reported failure.
5758
- `jsx-ban-props` (since v2.3.0)
@@ -121,6 +122,22 @@ The built-in configuration preset you get with `"extends": "tslint-react"` is se
121122
</button>
122123
);
123124
```
125+
- Rule options: _none_
126+
- `no-access-state-in-setstate`
127+
- Forbids accessing component state with `this.state` within `this.setState`
128+
calls, since React might batch multiple `this.setState` calls, thus resulting
129+
in accessing old state. Enforces use of callback argument instead.
130+
```ts
131+
// bad
132+
this.setState({
133+
counter: this.state.counter + 1
134+
});
135+
// good
136+
this.setState(
137+
prevState => ({ counter: prevState.counter + 1 })
138+
);
139+
```
140+
- Rule options: _none_
124141

125142
### Development
126143

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/**
2+
* @license
3+
* Copyright 2018 Palantir Technologies, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import * as Lint from "tslint";
19+
import { isCallExpression, isClassDeclaration, isPropertyAccessExpression } from "tsutils";
20+
import * as ts from "typescript";
21+
22+
export class Rule extends Lint.Rules.AbstractRule {
23+
/* tslint:disable:object-literal-sort-keys */
24+
public static metadata: Lint.IRuleMetadata = {
25+
ruleName: "no-access-state-in-setstate",
26+
description: "Reports usage of this.state within setState",
27+
rationale: Lint.Utils.dedent`
28+
Usage of this.state might result in errors when two state calls are
29+
called in batch and thus referencing old state and not the current state.
30+
See [setState()](https://reactjs.org/docs/react-component.html#setstate) in the React API reference.
31+
`,
32+
options: null,
33+
optionsDescription: "",
34+
type: "functionality",
35+
typescriptOnly: false,
36+
};
37+
/* tslint:enable:object-literal-sort-keys */
38+
39+
public static OBJECT_ARG_FAILURE =
40+
"References to this.state are not allowed in the setState state change object.";
41+
42+
public static CALLBACK_ARG_FAILURE =
43+
"References to this.state are not allowed in the setState updater, use the callback arguments instead.";
44+
45+
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
46+
return this.applyWithFunction(sourceFile, walk);
47+
}
48+
}
49+
50+
function walk(ctx: Lint.WalkContext<void>): void {
51+
return ts.forEachChild(ctx.sourceFile, callbackForEachChild);
52+
53+
function callbackForEachChild(node: ts.Node): void {
54+
if (!isClassDeclaration(node)) {
55+
return;
56+
}
57+
58+
ts.forEachChild(node, callbackForEachChildInClass);
59+
}
60+
61+
function callbackForEachChildInClass(node: ts.Node): void {
62+
if (!isCallExpression(node)) {
63+
return ts.forEachChild(node, callbackForEachChildInClass);
64+
}
65+
66+
const callExpressionArguments = node.arguments;
67+
68+
if (!isPropertyAccessExpression(node.expression) || callExpressionArguments.length === 0) {
69+
return;
70+
}
71+
72+
const propertyAccessExpression = node.expression;
73+
74+
const isThisPropertyAccess = propertyAccessExpression.expression.kind === ts.SyntaxKind.ThisKeyword;
75+
const isSetStateCall = propertyAccessExpression.name.text === "setState";
76+
77+
if (!isThisPropertyAccess || !isSetStateCall) {
78+
return;
79+
}
80+
81+
const firstArgument = node.arguments[0];
82+
83+
if (ts.isObjectLiteralExpression(firstArgument)) {
84+
ts.forEachChild(firstArgument, callbackForEachChildInSetStateObjectArgument);
85+
} else if (ts.isArrowFunction(firstArgument) || ts.isFunctionExpression(firstArgument)) {
86+
ts.forEachChild(firstArgument, callbackForEachChildInSetStateCallbackArgument);
87+
}
88+
}
89+
90+
function callbackForEachChildInSetStateObjectArgument(node: ts.Node): void {
91+
if (!isPropertyAccessExpression(node) || !isPropertyAccessExpression(node.expression)) {
92+
return ts.forEachChild(node, callbackForEachChildInSetStateObjectArgument);
93+
}
94+
95+
if (
96+
node.expression.expression.kind !== ts.SyntaxKind.ThisKeyword ||
97+
node.expression.name.text !== "state"
98+
) {
99+
return ts.forEachChild(node, callbackForEachChildInSetStateObjectArgument);
100+
}
101+
102+
ctx.addFailureAtNode(node, Rule.OBJECT_ARG_FAILURE);
103+
}
104+
105+
function callbackForEachChildInSetStateCallbackArgument(node: ts.Node): void {
106+
if (!isPropertyAccessExpression(node) || !isPropertyAccessExpression(node.expression)) {
107+
return ts.forEachChild(node, callbackForEachChildInSetStateCallbackArgument);
108+
}
109+
110+
if (
111+
node.expression.expression.kind !== ts.SyntaxKind.ThisKeyword ||
112+
node.expression.name.text !== "state"
113+
) {
114+
return ts.forEachChild(node, callbackForEachChildInSetStateCallbackArgument);
115+
}
116+
117+
ctx.addFailureAtNode(node, Rule.CALLBACK_ARG_FAILURE);
118+
}
119+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
class SomeReactComponent extends React.Component {
2+
3+
someClassFunction() {
4+
5+
this.fooBar({
6+
foo: this.state.foo
7+
});
8+
9+
this.setState({
10+
foo: "foo",
11+
bar: this.barz
12+
});
13+
14+
this.setState(
15+
{
16+
foo: "foo"
17+
},
18+
() => this.fooBar(this.state.foo);
19+
);
20+
21+
this.setState(prevState => ({
22+
foo: !prevState.foo
23+
}));
24+
25+
this.setState((prevState, currentProps) => ({
26+
foo: !prevState.foo,
27+
bar: currentProps.bar
28+
}));
29+
30+
this.setState({
31+
foo: window.history.length
32+
});
33+
34+
this.setState({
35+
foo: !this.props.bar
36+
});
37+
38+
this.setState({
39+
foo: !this.state.foo
40+
~~~~~~~~~~~~~~ [0]
41+
});
42+
43+
this.setState({
44+
foo: this.fooBar(this.state.foo)
45+
~~~~~~~~~~~~~~ [0]
46+
});
47+
48+
this.setState((prevState, currentProps) => ({
49+
foo: !this.state.foo,
50+
~~~~~~~~~~~~~~ [1]
51+
bar: currentProps.bar
52+
}));
53+
54+
this.setState((prevState, currentProps) => {
55+
this.fooBar(this.state.foo);
56+
~~~~~~~~~~~~~~ [1]
57+
return {
58+
bar: !prevState.bar
59+
};
60+
});
61+
}
62+
}
63+
64+
[0]: References to this.state are not allowed in the setState state change object.
65+
[1]: References to this.state are not allowed in the setState updater, use the callback arguments instead.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"rules": {
3+
"no-access-state-in-setstate": true
4+
}
5+
}

0 commit comments

Comments
 (0)