Skip to content

Commit 9bec9e9

Browse files
relekangjaaberg
authored andcommitted
wip: Add new rule no-access-state-in-setstate
This rule should prevent usage of this.state inside setState calls. Such usage of this.state might result in errors when two state calls is called in batch and thus referencing old state and not the current state. An example can be an increment function: function increment() { this.setState({value: this.state.value + 1}); } If these two setState operations is grouped together in a batch it will look be something like the following, given that value is 1. setState({value: 1 + 1}) setState({value: 1 + 1}) This can be avoided with using callbacks which takes the previous state as first argument. Then react will call the argument with the correct and updated state, even when things happen in batches. And the example above will be something like. setState({value: 1 + 1}) setState({value: 2 + 1})
1 parent 1a622ea commit 9bec9e9

File tree

3 files changed

+155
-0
lines changed

3 files changed

+155
-0
lines changed

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const allRules = {
3535
'jsx-uses-react': require('./lib/rules/jsx-uses-react'),
3636
'jsx-uses-vars': require('./lib/rules/jsx-uses-vars'),
3737
'jsx-wrap-multilines': require('./lib/rules/jsx-wrap-multilines'),
38+
'no-access-state-in-setstate': require('./lib/rules/no-access-state-in-setstate'),
3839
'no-array-index-key': require('./lib/rules/no-array-index-key'),
3940
'no-children-prop': require('./lib/rules/no-children-prop'),
4041
'no-danger': require('./lib/rules/no-danger'),
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* @fileoverview Prevent usage of this.state within setState
3+
* @author Rolf Erik Lekang
4+
*/
5+
6+
'use strict';
7+
8+
// ------------------------------------------------------------------------------
9+
// Rule Definition
10+
// ------------------------------------------------------------------------------
11+
12+
module.exports = {
13+
meta: {
14+
docs: {
15+
description: 'Reports when this.state is accessed within setState',
16+
category: 'Possible Errors',
17+
recommended: false
18+
}
19+
},
20+
21+
create: function(context) {
22+
function isSetStateCall(node) {
23+
return node.type === 'CallExpression' &&
24+
node.callee &&
25+
node.callee.property &&
26+
node.callee.property.name === 'setState';
27+
}
28+
29+
return {
30+
ThisExpression: function(node) {
31+
var memberExpression = node.parent;
32+
if (memberExpression.property.name === 'state') {
33+
var current = memberExpression;
34+
while (current.type !== 'Program') {
35+
if (isSetStateCall(current)) {
36+
context.report({
37+
node: memberExpression,
38+
message: 'Use callback in setState when referencing the previous state.'
39+
});
40+
break;
41+
}
42+
current = current.parent;
43+
}
44+
}
45+
}
46+
};
47+
}
48+
};
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/**
2+
* @fileoverview Prevent usage of this.state within setState
3+
* @author Rolf Erik Lekang
4+
*/
5+
'use strict';
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
var rule = require('../../../lib/rules/no-access-state-in-setstate');
12+
var RuleTester = require('eslint').RuleTester;
13+
14+
var parserOptions = {
15+
ecmaVersion: 6,
16+
ecmaFeatures: {
17+
jsx: true
18+
}
19+
};
20+
21+
// ------------------------------------------------------------------------------
22+
// Tests
23+
// ------------------------------------------------------------------------------
24+
25+
var ruleTester = new RuleTester();
26+
ruleTester.run('no-access-state-in-setstate', rule, {
27+
valid: [{
28+
code: [
29+
'var Hello = React.createClass({',
30+
' onClick: function() {',
31+
' this.setState(state => ({value: state.value + 1}))',
32+
' }',
33+
'});'
34+
].join('\n'),
35+
parserOptions: parserOptions
36+
}],
37+
38+
invalid: [{
39+
code: [
40+
'var Hello = React.createClass({',
41+
' onClick: function() {',
42+
' this.setState({value: this.state.value + 1})',
43+
' }',
44+
'});'
45+
].join('\n'),
46+
parserOptions: parserOptions,
47+
errors: [{
48+
message: 'Use callback in setState when referencing the previous state.'
49+
}]
50+
}, {
51+
code: [
52+
'var Hello = React.createClass({',
53+
' onClick: function() {',
54+
' this.setState(() => ({value: this.state.value + 1}))',
55+
' }',
56+
'});'
57+
].join('\n'),
58+
parserOptions: parserOptions,
59+
errors: [{
60+
message: 'Use callback in setState when referencing the previous state.'
61+
}]
62+
}, {
63+
code: [
64+
'var Hello = React.createClass({',
65+
' onClick: function() {',
66+
' var nextValue = this.state.value + 1',
67+
' this.setState({value: nextValue})',
68+
' }',
69+
'});'
70+
].join('\n'),
71+
parserOptions: parserOptions,
72+
errors: [{
73+
message: 'Use callback in setState when referencing the previous state.'
74+
}]
75+
}, {
76+
code: [
77+
'function nextState(state) {',
78+
' return {value: state.value + 1}',
79+
'}',
80+
'var Hello = React.createClass({',
81+
' onClick: function() {',
82+
' this.setState(nextState(this.state))',
83+
' }',
84+
'});'
85+
].join('\n'),
86+
parserOptions: parserOptions,
87+
errors: [{
88+
message: 'Use callback in setState when referencing the previous state.'
89+
}]
90+
}, {
91+
code: [
92+
'var Hello = React.createClass({',
93+
' nextState: function() {',
94+
' return {value: this.state.value + 1}',
95+
' },',
96+
' onClick: function() {',
97+
' this.setState(nextState())',
98+
' }',
99+
'});'
100+
].join('\n'),
101+
parserOptions: parserOptions,
102+
errors: [{
103+
message: 'Use callback in setState when referencing the previous state.'
104+
}]
105+
}]
106+
});

0 commit comments

Comments
 (0)