Skip to content

Commit 790dd9f

Browse files
authored
Merge pull request #1945 from lukyth/lukyth/state-in-constructor
[New] Add `state-in-constructor` rule
2 parents 57f0127 + 9415814 commit 790dd9f

File tree

8 files changed

+516
-28
lines changed

8 files changed

+516
-28
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ Enable the rules that you would like to use.
134134
* [react/self-closing-comp](docs/rules/self-closing-comp.md): Prevent extra closing tags for components without children (fixable)
135135
* [react/sort-comp](docs/rules/sort-comp.md): Enforce component methods order (fixable)
136136
* [react/sort-prop-types](docs/rules/sort-prop-types.md): Enforce propTypes declarations alphabetical sorting
137+
* [react/state-in-constructor](docs/rules/state-in-constructor.md): Enforce the state initialization style to be either in a constructor or with a class property
137138
* [react/style-prop-object](docs/rules/style-prop-object.md): Enforce style prop value being an object
138139
* [react/void-dom-elements-no-children](docs/rules/void-dom-elements-no-children.md): Prevent void DOM elements (e.g. `<img />`, `<br />`) from receiving children
139140

docs/rules/state-in-constructor.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Enforce state initialization style (react/state-in-constructor)
2+
3+
This rule will enforce the state initialization style to be either in a constructor or with a class property.
4+
5+
## Rule Options
6+
7+
```js
8+
...
9+
"react/state-in-constructor": [<enabled>, <mode>]
10+
...
11+
```
12+
13+
### `always` mode
14+
15+
Will enforce the state initialization style to be in a constructor. This is the default mode.
16+
17+
The following patterns are considered warnings:
18+
19+
```jsx
20+
class Foo extends React.Component {
21+
state = { bar: 0 }
22+
render() {
23+
return <div>Foo</div>
24+
}
25+
}
26+
```
27+
28+
The following patterns are **not** considered warnings:
29+
30+
```jsx
31+
class Foo extends React.Component {
32+
constructor(props) {
33+
super(props)
34+
this.state = { bar: 0 }
35+
}
36+
render() {
37+
return <div>Foo</div>
38+
}
39+
}
40+
```
41+
42+
### `never` mode
43+
44+
Will enforce the state initialization style to be with a class property.
45+
46+
The following patterns are considered warnings:
47+
48+
```jsx
49+
class Foo extends React.Component {
50+
constructor(props) {
51+
super(props)
52+
this.state = { bar: 0 }
53+
}
54+
render() {
55+
return <div>Foo</div>
56+
}
57+
}
58+
```
59+
60+
The following patterns are **not** considered warnings:
61+
62+
```jsx
63+
class Foo extends React.Component {
64+
state = { bar: 0 }
65+
render() {
66+
return <div>Foo</div>
67+
}
68+
}
69+
```
70+
71+
72+
## When Not To Use It
73+
74+
When the way a component state is being initialized doesn't matter.

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ const allRules = {
8080
'self-closing-comp': require('./lib/rules/self-closing-comp'),
8181
'sort-comp': require('./lib/rules/sort-comp'),
8282
'sort-prop-types': require('./lib/rules/sort-prop-types'),
83+
'state-in-constructor': require('./lib/rules/state-in-constructor'),
8384
'style-prop-object': require('./lib/rules/style-prop-object'),
8485
'void-dom-elements-no-children': require('./lib/rules/void-dom-elements-no-children')
8586
};

lib/rules/no-direct-mutation-state.js

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,6 @@ module.exports = {
5959
return node;
6060
}
6161

62-
/**
63-
* Determine if this MemberExpression is for `this.state`
64-
* @param {Object} node The node to process
65-
* @returns {Boolean}
66-
*/
67-
function isStateMemberExpression(node) {
68-
return node.object.type === 'ThisExpression' && node.property.name === 'state';
69-
}
70-
7162
/**
7263
* Determine if we should currently ignore assignments in this component.
7364
* @param {?Object} component The component to process
@@ -101,7 +92,7 @@ module.exports = {
10192
return;
10293
}
10394
const item = getOuterMemberExpression(node.left);
104-
if (isStateMemberExpression(item)) {
95+
if (utils.isStateMemberExpression(item)) {
10596
const mutations = (component && component.mutations) || [];
10697
mutations.push(node.left.object);
10798
components.set(node, {
@@ -117,7 +108,7 @@ module.exports = {
117108
return;
118109
}
119110
const item = getOuterMemberExpression(node.argument);
120-
if (isStateMemberExpression(item)) {
111+
if (utils.isStateMemberExpression(item)) {
121112
const mutations = (component && component.mutations) || [];
122113
mutations.push(item);
123114
components.set(node, {

lib/rules/state-in-constructor.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* @fileoverview Enforce the state initialization style to be either in a constructor or with a class property
3+
* @author Kanitkorn Sujautra
4+
*/
5+
'use strict';
6+
7+
const Components = require('../util/Components');
8+
const docsUrl = require('../util/docsUrl');
9+
10+
// ------------------------------------------------------------------------------
11+
// Rule Definition
12+
// ------------------------------------------------------------------------------
13+
14+
module.exports = {
15+
meta: {
16+
docs: {
17+
description: 'State initialization in an ES6 class component should be in a constructor',
18+
category: 'Stylistic Issues',
19+
recommended: false,
20+
url: docsUrl('state-in-constructor')
21+
},
22+
schema: [{
23+
enum: ['always', 'never']
24+
}]
25+
},
26+
27+
create: Components.detect((context, components, utils) => {
28+
const option = context.options[0] || 'always';
29+
return {
30+
ClassProperty: function (node) {
31+
if (
32+
option === 'always' &&
33+
!node.static &&
34+
node.key.name === 'state' &&
35+
utils.getParentES6Component()
36+
) {
37+
context.report({
38+
node,
39+
message: 'State initialization should be in a constructor'
40+
});
41+
}
42+
},
43+
AssignmentExpression(node) {
44+
if (
45+
option === 'never' &&
46+
utils.isStateMemberExpression(node.left) &&
47+
utils.inConstructor() &&
48+
utils.getParentES6Component()
49+
) {
50+
context.report({
51+
node,
52+
message: 'State initialization should be in a class property'
53+
});
54+
}
55+
}
56+
};
57+
})
58+
};

lib/util/Components.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,30 @@ function componentRule(rule, context) {
298298
return calledOnPragma;
299299
},
300300

301+
/**
302+
* Check if we are in a class constructor
303+
* @return {boolean} true if we are in a class constructor, false if not
304+
*/
305+
inConstructor: function() {
306+
let scope = context.getScope();
307+
while (scope) {
308+
if (scope.block && scope.block.parent && scope.block.parent.kind === 'constructor') {
309+
return true;
310+
}
311+
scope = scope.upper;
312+
}
313+
return false;
314+
},
315+
316+
/**
317+
* Determine if the node is MemberExpression of `this.state`
318+
* @param {Object} node The node to process
319+
* @returns {Boolean}
320+
*/
321+
isStateMemberExpression: function(node) {
322+
return node.type === 'MemberExpression' && node.object.type === 'ThisExpression' && node.property.name === 'state';
323+
},
324+
301325
getReturnPropertyAndNode(ASTnode) {
302326
let property;
303327
let node = ASTnode;

lib/util/usedPropTypes.js

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -200,21 +200,6 @@ module.exports = function usedPropTypesInstructions(context, components, utils)
200200
return key.type === 'Identifier' ? key.name : key.value;
201201
}
202202

203-
/**
204-
* Check if we are in a class constructor
205-
* @return {boolean} true if we are in a class constructor, false if not
206-
*/
207-
function inConstructor() {
208-
let scope = context.getScope();
209-
while (scope) {
210-
if (scope.block && scope.block.parent && scope.block.parent.kind === 'constructor') {
211-
return true;
212-
}
213-
scope = scope.upper;
214-
}
215-
return false;
216-
}
217-
218203
/**
219204
* Retrieve the name of a property node
220205
* @param {ASTNode} node The AST node with the property.
@@ -226,7 +211,7 @@ module.exports = function usedPropTypesInstructions(context, components, utils)
226211
const isDirectPrevProp = DIRECT_PREV_PROPS_REGEX.test(sourceCode.getText(node));
227212
const isDirectSetStateProp = isPropArgumentInSetStateUpdater(node);
228213
const isInClassComponent = utils.getParentES6Component() || utils.getParentES5Component();
229-
const isNotInConstructor = !inConstructor(node);
214+
const isNotInConstructor = !utils.inConstructor(node);
230215
const isNotInLifeCycleMethod = !inLifeCycleMethod();
231216
const isNotInSetStateUpdater = !inSetStateUpdater();
232217
if ((isDirectProp || isDirectNextProp || isDirectPrevProp || isDirectSetStateProp)
@@ -383,7 +368,7 @@ module.exports = function usedPropTypesInstructions(context, components, utils)
383368
|| DIRECT_NEXT_PROPS_REGEX.test(nodeSource)
384369
|| DIRECT_PREV_PROPS_REGEX.test(nodeSource);
385370
const reportedNode = (
386-
!isDirectProp && !inConstructor() && !inComponentWillReceiveProps() ?
371+
!isDirectProp && !utils.inConstructor() && !inComponentWillReceiveProps() ?
387372
node.parent.property :
388373
node.property
389374
);

0 commit comments

Comments
 (0)