Skip to content

Commit

Permalink
wip: Add new rule no-access-state-in-setstate
Browse files Browse the repository at this point in the history
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})
  • Loading branch information
relekang authored and jaaberg committed Aug 9, 2017
1 parent 1a622ea commit 9bec9e9
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 0 deletions.
1 change: 1 addition & 0 deletions index.js
Expand Up @@ -35,6 +35,7 @@ const allRules = {
'jsx-uses-react': require('./lib/rules/jsx-uses-react'),
'jsx-uses-vars': require('./lib/rules/jsx-uses-vars'),
'jsx-wrap-multilines': require('./lib/rules/jsx-wrap-multilines'),
'no-access-state-in-setstate': require('./lib/rules/no-access-state-in-setstate'),
'no-array-index-key': require('./lib/rules/no-array-index-key'),
'no-children-prop': require('./lib/rules/no-children-prop'),
'no-danger': require('./lib/rules/no-danger'),
Expand Down
48 changes: 48 additions & 0 deletions lib/rules/no-access-state-in-setstate.js
@@ -0,0 +1,48 @@
/**
* @fileoverview Prevent usage of this.state within setState
* @author Rolf Erik Lekang
*/

'use strict';

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

module.exports = {
meta: {
docs: {
description: 'Reports when this.state is accessed within setState',
category: 'Possible Errors',
recommended: false
}
},

create: function(context) {
function isSetStateCall(node) {
return node.type === 'CallExpression' &&
node.callee &&
node.callee.property &&
node.callee.property.name === 'setState';
}

return {
ThisExpression: function(node) {
var memberExpression = node.parent;
if (memberExpression.property.name === 'state') {
var current = memberExpression;
while (current.type !== 'Program') {
if (isSetStateCall(current)) {
context.report({
node: memberExpression,
message: 'Use callback in setState when referencing the previous state.'
});
break;
}
current = current.parent;
}
}
}
};
}
};
106 changes: 106 additions & 0 deletions tests/lib/rules/no-access-state-in-setstate.js
@@ -0,0 +1,106 @@
/**
* @fileoverview Prevent usage of this.state within setState
* @author Rolf Erik Lekang
*/
'use strict';

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

var rule = require('../../../lib/rules/no-access-state-in-setstate');
var RuleTester = require('eslint').RuleTester;

var parserOptions = {
ecmaVersion: 6,
ecmaFeatures: {
jsx: true
}
};

// ------------------------------------------------------------------------------
// Tests
// ------------------------------------------------------------------------------

var ruleTester = new RuleTester();
ruleTester.run('no-access-state-in-setstate', rule, {
valid: [{
code: [
'var Hello = React.createClass({',
' onClick: function() {',
' this.setState(state => ({value: state.value + 1}))',
' }',
'});'
].join('\n'),
parserOptions: parserOptions
}],

invalid: [{
code: [
'var Hello = React.createClass({',
' onClick: function() {',
' this.setState({value: this.state.value + 1})',
' }',
'});'
].join('\n'),
parserOptions: parserOptions,
errors: [{
message: 'Use callback in setState when referencing the previous state.'
}]
}, {
code: [
'var Hello = React.createClass({',
' onClick: function() {',
' this.setState(() => ({value: this.state.value + 1}))',
' }',
'});'
].join('\n'),
parserOptions: parserOptions,
errors: [{
message: 'Use callback in setState when referencing the previous state.'
}]
}, {
code: [
'var Hello = React.createClass({',
' onClick: function() {',
' var nextValue = this.state.value + 1',
' this.setState({value: nextValue})',
' }',
'});'
].join('\n'),
parserOptions: parserOptions,
errors: [{
message: 'Use callback in setState when referencing the previous state.'
}]
}, {
code: [
'function nextState(state) {',
' return {value: state.value + 1}',
'}',
'var Hello = React.createClass({',
' onClick: function() {',
' this.setState(nextState(this.state))',
' }',
'});'
].join('\n'),
parserOptions: parserOptions,
errors: [{
message: 'Use callback in setState when referencing the previous state.'
}]
}, {
code: [
'var Hello = React.createClass({',
' nextState: function() {',
' return {value: this.state.value + 1}',
' },',
' onClick: function() {',
' this.setState(nextState())',
' }',
'});'
].join('\n'),
parserOptions: parserOptions,
errors: [{
message: 'Use callback in setState when referencing the previous state.'
}]
}]
});

0 comments on commit 9bec9e9

Please sign in to comment.