-
Notifications
You must be signed in to change notification settings - Fork 76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add new rule noAccessStateInSetstate #190
Conversation
Inspired by ESLint's no-access-state-in-setstate rule
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fully agree that it is a great first step forward. Would like to see it being available.
|
||
this.setState((prevState, currentProps) => { | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
this.fooBar(this.state.foo); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this case the error message might read a bit strange, since the code is using the callback.
Maybe Use first argument from callback inside setState instead of this.state.
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very good idea, will do that!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, unfortunately it might be a little difficult. In my now updated code, you'd have to copy the callbackForEachChildInSetStateArgument
function, for checking a callback and adding another failure text to the node. At least that's the only way I can think of right now, but I don't have that much experience with TSLint yet :-)
So for a quick solution I simply made the failure text a bit more generic.
|
||
this.setState((prevState, currentProps) => ({ | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
foo: !this.state.foo, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it would of course be much nicer to have the marker only on the occurrence(s) of this.state
but I would consider it nice to have.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I sat down again with my code and now I'm pretty sure I found a still very easy and even more reliable way to not only find accesses of this.state but also can now display the error message exactly at the right node.
I will push the changes at the weekend! Thanks for the idea!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or even better push it right away. Looking forward to your review!
I now first check if a node is a setState call and if so, I check each child of the first argument of that call, determining if that node is a this.state access.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow, really nice!
return ts.forEachChild(node, callbackForEachChildInSetStateArgument); | ||
} | ||
|
||
if (!node.getText().startsWith("this.state.")) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this.state
could also be a umber, boolean or be to some function without accessing it's fields I would suggest to omit the last .
in the search expression. (You might want to add a test case for that one.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just realize that I maybe should start the linter with checking if we're even in some kind of React component class.
And yeah, I guess you're right, this.state
doesn't necessarly need to be an object. I tried to find some source telling you that you shouldn't store some primitive value directly as a state, but couldn't find any, so maybe that isn't even a code smell.. There will be a warning from React though (this one).
But I will transform that check to a RegEx, the one in Line 52 as well :-) Something like ^this\.state\b
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, there might be some things that can be done regarding performance I guess.
And I would of course prefer a fast rule over a slow one, but a slow one would even be better then none.
So it's up to you and since no contributor has reacted on this PR yet you might as well improve inside this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, we should aim for a performant, fast rule, so I'll gladly check with TSLint's guide again. Anything particular you have in mind?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the current impllementation makes heavy use of the getText
method, and the guide has some comments regarding it. I think this and checking that the file even can contain/contains this.setState
might be the most low hanging fruits.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, it took me some time. But now I guess I did improve the performance. I got rid of both getText
calls, and in addition only walk the children of classes.
This linter will still run in every class, React.Component
or not, but as far as I know you'd need a TypedRule to prevent this. But definitely something that could still be improved.
Thanks for the helpful suggestions, sorry for the long delay, and again looking forward to the code review!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is awesome, looking forward to use it.
(Not sure what it means that I approve this, but doing it anyways, since I would love to see this being merged.)
@cheeZery I would really love to have this rule availble.
|
calls, since React might batch multiple `this.setState` calls, thus resulting | ||
in accessing old state. Enforces use of callback argument instead. | ||
```ts | ||
// bad |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this did not get syntax highlighted / formatted correctly -- check out the markdown preview
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 \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you don't need the trailing \
, dedent handles that.
also the leading backtick should go right after dedent
, and the trailing backtick should go after the text block on a new line. see https://github.com/palantir/tslint/blob/e080fc4ceb58e907d48bf5419ceb96e6899dd5d5/src/rules/strictBooleanExpressionsRule.ts#L37, for example
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.`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be good to reference these react docs in this rationale. you can use markdown
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a link to the docs. Is the phrasing alright? (English being not my first language and all)
}; | ||
/* tslint:enable:object-literal-sort-keys */ | ||
|
||
public static FAILURE_STRING = "Avoid using this.state in first argument of setState."; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how about we say this instead:
- for uses of
this.state
in the callback update syntax: "References to this.state are not allowed in the setState updater, use the callback arguments instead." - for uses of
this.state
in the stateChange object syntax: "References to this.state are not allowed in the setState state change object."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea! Unfortunately the only way I came up to achieve this was to duplicate the callback function which checks the setState
argument. Any better idea? :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have some ideas for cleaning this up a bit, I can push an additional commit after this merges
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe you can share your ideas now, since I will refactor this again a bit. Or you just push them, as soon as I provide you guys with the new version!
@karfau sorry for the delay again. And thanks for the review. Normally I would gladly suggest to suggest add the code to another TSLint plugin repo. But now since @adidahiya reviewed my changes, this might get merged anyway :-) @adidahiya thanks for your review! I will get to the fixes soon! Although I'm sure this rule would just be added since there already was an open PR. Otherwise we probably would wait a few weeks/months until we maybe just can use https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/no-access-state-in-setstate.md |
thanks @cheeZery! |
Actually I merged this too soon. This rule should use type information and only cover React components. Sorry, I will have to revert it for now, you are welcome to open a new PR with that change. |
This reverts commit 987a95e.
This reverts commit 987a95e.
Wow, this way of handling the issue is really kind of a harsh way to handle it in my opinion. Making the rule available but not enabling it by default (which I would assume is the case anyways, but I didn't check) so people can file improvement PRs would make much more sense for me. And it is already only checking classes, which should be a very (beside class components) in any react project... just my 2 cents |
It's also pretty easy to write this as a custom rule in your project or your own rule set, try it out in a sizable React project, and merge it into the "official" tslint-react repo once it's well-tested.
I don't think this is a good assumption; I have worked on many React projects where this is not the case. |
@adidahiya you are definitely right in that this should be a typed rule and only run in |
Yeah, I noticed that comment a little too late unfortunately. Thanks for understanding. |
@adidahiya I might need some help or a tip for this one, since this is my first time working with the type checker 😳 and it would also be the first rule using type information in the repo. So first of all I put a tsconfig file in the test dir to get a program and with that the type checker. From there I tried some stuff but couldn't really came up with something useful. In the new Any help is appreciated! :-) |
Fixes #172 (and #80 to some extent)
Overview of change:
Took ESLint-plugin-React's no-access-state-in-setstate rule and implemented it in tslint-react. I know no maintainer seems to have reviewed #172 yet, but I took a swing at it anyway, hope that was okay!
Is there anything you'd like reviewers to focus on?
You will find that I did the checks with pretty simple string comparisons (e.g.
expression.getText().indexOf("this.state.") !== -1;
). I started creating the checks with actually using the AST but found that the string solution is not only imho more reliable but of course also a lot easier to write and understand. But maybe there're better ways to check it!No matter the way the check of the
this.setState
argument is implemented, there are still ways to bypass the check and creating - if you want so - false-positives.You'd have to determine every variable containing (or deriving from) the current state and every functions whose return value depends on it, and then check if those variables and methods are used in
this.setState
. ESLint-plugin-React even does something like that.But I guess my pull request atleast offers a good starting point and will catch most of the error-prone code lines.
Since English isn't my first language, all texts (e.g. rule description) might be improvable :-)
Looking forward to your review!