Skip to content

Commit

Permalink
New rule no-access-state-in-setstate (#190)
Browse files Browse the repository at this point in the history
  • Loading branch information
cheeZery authored and adidahiya committed Mar 19, 2019
1 parent 61e2f50 commit 987a95e
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 0 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ The built-in configuration preset you get with `"extends": "tslint-react"` is se
size={size}
/>
```
- Rule options: _none_
- `jsx-ban-elements` (since v3.4.0)
- Allows blacklisting of JSX elements with an optional explanatory message in the reported failure.
- `jsx-ban-props` (since v2.3.0)
Expand Down Expand Up @@ -121,6 +122,22 @@ The built-in configuration preset you get with `"extends": "tslint-react"` is se
</button>
);
```
- Rule options: _none_
- `no-access-state-in-setstate`
- Forbids accessing component state with `this.state` within `this.setState`
calls, since React might batch multiple `this.setState` calls, thus resulting
in accessing old state. Enforces use of callback argument instead.
```ts
// bad
this.setState({
counter: this.state.counter + 1
});
// good
this.setState(
prevState => ({ counter: prevState.counter + 1 })
);
```
- Rule options: _none_

### Development

Expand Down
119 changes: 119 additions & 0 deletions src/rules/noAccessStateInSetstateRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* @license
* Copyright 2018 Palantir Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as Lint from "tslint";
import { isCallExpression, isClassDeclaration, isPropertyAccessExpression } from "tsutils";
import * as ts from "typescript";

export class Rule extends Lint.Rules.AbstractRule {
/* tslint:disable:object-literal-sort-keys */
public static metadata: Lint.IRuleMetadata = {
ruleName: "no-access-state-in-setstate",
description: "Reports usage of this.state within setState",
rationale: Lint.Utils.dedent`
Usage of this.state might result in errors when two state calls are
called in batch and thus referencing old state and not the current state.
See [setState()](https://reactjs.org/docs/react-component.html#setstate) in the React API reference.
`,
options: null,
optionsDescription: "",
type: "functionality",
typescriptOnly: false,
};
/* tslint:enable:object-literal-sort-keys */

public static OBJECT_ARG_FAILURE =
"References to this.state are not allowed in the setState state change object.";

public static CALLBACK_ARG_FAILURE =
"References to this.state are not allowed in the setState updater, use the callback arguments instead.";

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk);
}
}

function walk(ctx: Lint.WalkContext<void>): void {
return ts.forEachChild(ctx.sourceFile, callbackForEachChild);

function callbackForEachChild(node: ts.Node): void {
if (!isClassDeclaration(node)) {
return;
}

ts.forEachChild(node, callbackForEachChildInClass);
}

function callbackForEachChildInClass(node: ts.Node): void {
if (!isCallExpression(node)) {
return ts.forEachChild(node, callbackForEachChildInClass);
}

const callExpressionArguments = node.arguments;

if (!isPropertyAccessExpression(node.expression) || callExpressionArguments.length === 0) {
return;
}

const propertyAccessExpression = node.expression;

const isThisPropertyAccess = propertyAccessExpression.expression.kind === ts.SyntaxKind.ThisKeyword;
const isSetStateCall = propertyAccessExpression.name.text === "setState";

if (!isThisPropertyAccess || !isSetStateCall) {
return;
}

const firstArgument = node.arguments[0];

if (ts.isObjectLiteralExpression(firstArgument)) {
ts.forEachChild(firstArgument, callbackForEachChildInSetStateObjectArgument);
} else if (ts.isArrowFunction(firstArgument) || ts.isFunctionExpression(firstArgument)) {
ts.forEachChild(firstArgument, callbackForEachChildInSetStateCallbackArgument);
}
}

function callbackForEachChildInSetStateObjectArgument(node: ts.Node): void {
if (!isPropertyAccessExpression(node) || !isPropertyAccessExpression(node.expression)) {
return ts.forEachChild(node, callbackForEachChildInSetStateObjectArgument);
}

if (
node.expression.expression.kind !== ts.SyntaxKind.ThisKeyword ||
node.expression.name.text !== "state"
) {
return ts.forEachChild(node, callbackForEachChildInSetStateObjectArgument);
}

ctx.addFailureAtNode(node, Rule.OBJECT_ARG_FAILURE);
}

function callbackForEachChildInSetStateCallbackArgument(node: ts.Node): void {
if (!isPropertyAccessExpression(node) || !isPropertyAccessExpression(node.expression)) {
return ts.forEachChild(node, callbackForEachChildInSetStateCallbackArgument);
}

if (
node.expression.expression.kind !== ts.SyntaxKind.ThisKeyword ||
node.expression.name.text !== "state"
) {
return ts.forEachChild(node, callbackForEachChildInSetStateCallbackArgument);
}

ctx.addFailureAtNode(node, Rule.CALLBACK_ARG_FAILURE);
}
}
65 changes: 65 additions & 0 deletions test/rules/no-access-state-in-setstate/test.tsx.lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
class SomeReactComponent extends React.Component {

someClassFunction() {

this.fooBar({
foo: this.state.foo
});

this.setState({
foo: "foo",
bar: this.barz
});

this.setState(
{
foo: "foo"
},
() => this.fooBar(this.state.foo);
);

this.setState(prevState => ({
foo: !prevState.foo
}));

this.setState((prevState, currentProps) => ({
foo: !prevState.foo,
bar: currentProps.bar
}));

this.setState({
foo: window.history.length
});

this.setState({
foo: !this.props.bar
});

this.setState({
foo: !this.state.foo
~~~~~~~~~~~~~~ [0]
});

this.setState({
foo: this.fooBar(this.state.foo)
~~~~~~~~~~~~~~ [0]
});

this.setState((prevState, currentProps) => ({
foo: !this.state.foo,
~~~~~~~~~~~~~~ [1]
bar: currentProps.bar
}));

this.setState((prevState, currentProps) => {
this.fooBar(this.state.foo);
~~~~~~~~~~~~~~ [1]
return {
bar: !prevState.bar
};
});
}
}

[0]: References to this.state are not allowed in the setState state change object.
[1]: References to this.state are not allowed in the setState updater, use the callback arguments instead.
5 changes: 5 additions & 0 deletions test/rules/no-access-state-in-setstate/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rules": {
"no-access-state-in-setstate": true
}
}

0 comments on commit 987a95e

Please sign in to comment.