Skip to content

Commit 0830226

Browse files
committed
Add prefer-stateless-function rule (fixes #214)
1 parent 1b0caca commit 0830226

File tree

5 files changed

+289
-1
lines changed

5 files changed

+289
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ The plugin has a [recommended configuration](#user-content-recommended-configura
8686
* [no-string-refs](docs/rules/no-string-refs.md): Prevent using string references in `ref` attribute.
8787
* [no-unknown-property](docs/rules/no-unknown-property.md): Prevent usage of unknown DOM property (fixable)
8888
* [prefer-es6-class](docs/rules/prefer-es6-class.md): Enforce ES5 or ES6 class for React Components
89+
* [prefer-stateless-function](docs/rules/prefer-stateless-function.md): Enforce stateless React Components to be written as a pure function
8990
* [prop-types](docs/rules/prop-types.md): Prevent missing props validation in a React component definition
9091
* [react-in-jsx-scope](docs/rules/react-in-jsx-scope.md): Prevent missing `React` when using JSX
9192
* [require-extension](docs/rules/require-extension.md): Restrict file extensions that may be required
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Enforce stateless React Components to be written as a pure function (prefer-stateless-function)
2+
3+
Stateless functional components are more simple than class based components and will benefit from future React performance optimizations specific to these components.
4+
5+
## Rule Details
6+
7+
This rule will check your class based React components for
8+
9+
* lifecycle methods: `state`, `getInitialState`, `componentWillMount`, `componentDidMount`, `componentWillReceiveProps`, `shouldComponentUpdate`, `componentWillUpdate`, `componentDidUpdate` and `componentWillUnmount`
10+
* usage of `this.setState`
11+
* presence of `ref` attribute in JSX
12+
13+
If none of these 3 elements are found then the rule warn you to write this component as a pure function.
14+
15+
The following patterns are considered warnings:
16+
17+
```js
18+
var Hello = React.createClass({
19+
render: function() {
20+
return <div>Hello {this.props.name}</div>;
21+
}
22+
});
23+
```
24+
25+
```js
26+
class Hello extends React.Component {
27+
sayHello() {
28+
alert(`Hello ${this.props.name}`)
29+
}
30+
render() {
31+
return <div onClick={this.sayHello}>Hello {this.props.name}</div>;
32+
}
33+
}
34+
```
35+
36+
The following patterns are not considered warnings:
37+
38+
```js
39+
const Foo = function(props) {
40+
return <div>{props.foo}</div>;
41+
};
42+
```
43+
44+
```js
45+
class Foo extends React.Component {
46+
shouldComponentUpdate() {
47+
return false;
48+
}
49+
render() {
50+
return <div>{this.props.foo}</div>;
51+
}
52+
}
53+
```

index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ module.exports = {
4040
'forbid-prop-types': require('./lib/rules/forbid-prop-types'),
4141
'prefer-es6-class': require('./lib/rules/prefer-es6-class'),
4242
'jsx-key': require('./lib/rules/jsx-key'),
43-
'no-string-refs': require('./lib/rules/no-string-refs')
43+
'no-string-refs': require('./lib/rules/no-string-refs'),
44+
'prefer-stateless-function': require('./lib/rules/prefer-stateless-function')
4445
},
4546
configs: {
4647
recommended: {
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/**
2+
* @fileoverview Enforce stateless components to be written as a pure function
3+
* @author Yannick Croissant
4+
*/
5+
'use strict';
6+
7+
var Components = require('../util/Components');
8+
9+
// ------------------------------------------------------------------------------
10+
// Rule Definition
11+
// ------------------------------------------------------------------------------
12+
13+
module.exports = Components.detect(function(context, components, utils) {
14+
15+
var sourceCode = context.getSourceCode();
16+
17+
var lifecycleMethods = [
18+
'state',
19+
'getInitialState',
20+
'componentWillMount',
21+
'componentDidMount',
22+
'componentWillReceiveProps',
23+
'shouldComponentUpdate',
24+
'componentWillUpdate',
25+
'componentDidUpdate',
26+
'componentWillUnmount'
27+
];
28+
29+
// --------------------------------------------------------------------------
30+
// Public
31+
// --------------------------------------------------------------------------
32+
33+
/**
34+
* Get properties name
35+
* @param {Object} node - Property.
36+
* @returns {String} Property name.
37+
*/
38+
function getPropertyName(node) {
39+
// Special case for class properties
40+
// (babel-eslint does not expose property name so we have to rely on tokens)
41+
if (node.type === 'ClassProperty') {
42+
var tokens = context.getFirstTokens(node, 2);
43+
return tokens[1] && tokens[1].type === 'Identifier' ? tokens[1].value : tokens[0].value;
44+
}
45+
46+
return node.key.name;
47+
}
48+
49+
/**
50+
* Get properties for a given AST node
51+
* @param {ASTNode} node The AST node being checked.
52+
* @returns {Array} Properties array.
53+
*/
54+
function getComponentProperties(node) {
55+
switch (node.type) {
56+
case 'ClassDeclaration':
57+
return node.body.body;
58+
case 'ObjectExpression':
59+
return node.properties;
60+
default:
61+
return [];
62+
}
63+
}
64+
65+
/**
66+
* Check if a given AST node have any lifecycle method
67+
* @param {ASTNode} node The AST node being checked.
68+
* @returns {Boolean} True if the node has at least one lifecycle method, false if not.
69+
*/
70+
function hasLifecycleMethod(node) {
71+
var properties = getComponentProperties(node);
72+
return properties.some(function(property) {
73+
return lifecycleMethods.indexOf(getPropertyName(property)) !== -1;
74+
});
75+
}
76+
77+
/**
78+
* Mark a setState as used
79+
* @param {ASTNode} node The AST node being checked.
80+
*/
81+
function markSetStateAsUsed(node) {
82+
components.set(node, {
83+
useSetState: true
84+
});
85+
}
86+
87+
/**
88+
* Mark a ref as used
89+
* @param {ASTNode} node The AST node being checked.
90+
*/
91+
function markRefAsUsed(node) {
92+
components.set(node, {
93+
useRef: true
94+
});
95+
}
96+
97+
return {
98+
CallExpression: function(node) {
99+
var callee = node.callee;
100+
if (callee.type !== 'MemberExpression') {
101+
return;
102+
}
103+
if (callee.object.type !== 'ThisExpression' || callee.property.name !== 'setState') {
104+
return;
105+
}
106+
markSetStateAsUsed(node);
107+
},
108+
109+
JSXAttribute: function(node) {
110+
var name = sourceCode.getText(node.name);
111+
if (name !== 'ref') {
112+
return;
113+
}
114+
markRefAsUsed(node);
115+
},
116+
117+
'Program:exit': function() {
118+
var list = components.list();
119+
for (var component in list) {
120+
if (
121+
!list.hasOwnProperty(component) ||
122+
hasLifecycleMethod(list[component].node) ||
123+
list[component].useSetState ||
124+
list[component].useRef ||
125+
(!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node))
126+
) {
127+
continue;
128+
}
129+
130+
context.report({
131+
node: list[component].node,
132+
message: 'Component should be written as a pure function'
133+
});
134+
}
135+
}
136+
};
137+
138+
});
139+
140+
module.exports.schema = [];
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* @fileoverview Enforce stateless components to be written as a pure function
3+
* @author Yannick Croissant
4+
*/
5+
'use strict';
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
var rule = require('../../../lib/rules/prefer-stateless-function');
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('prefer-stateless-function', rule, {
27+
28+
valid: [
29+
{
30+
code: [
31+
'const Foo = function(props) {',
32+
' return <div>{props.foo}</div>;',
33+
'};'
34+
].join('\n'),
35+
parserOptions: parserOptions
36+
}, {
37+
code: [
38+
'class Foo extends React.Component {',
39+
' shouldComponentUpdate() {',
40+
' return fasle;',
41+
' }',
42+
' render() {',
43+
' return <div>{this.props.foo}</div>;',
44+
' }',
45+
'}'
46+
].join('\n'),
47+
parserOptions: parserOptions
48+
}, {
49+
code: 'const Foo = ({foo}) => <div>{foo}</div>;',
50+
parserOptions: parserOptions
51+
}, {
52+
code: [
53+
'class Foo extends React.Component {',
54+
' changeState() {',
55+
' this.setState({foo: "clicked"});',
56+
' }',
57+
' render() {',
58+
' return <div onClick={this.changeState.bind(this)}>{this.state.foo || "bar"}</div>;',
59+
' }',
60+
'}'
61+
].join('\n'),
62+
parserOptions: parserOptions
63+
}, {
64+
code: [
65+
'class Foo extends React.Component {',
66+
' doStuff() {',
67+
' this.refs.foo.style.backgroundColor = "red";',
68+
' }',
69+
' render() {',
70+
' return <div ref="foo" onClick={this.doStuff}>{this.props.foo}</div>;',
71+
' }',
72+
'}'
73+
].join('\n'),
74+
parserOptions: parserOptions
75+
}
76+
],
77+
78+
invalid: [
79+
{
80+
code: [
81+
'class Foo extends React.Component {',
82+
' render() {',
83+
' return <div>{this.props.foo}</div>;',
84+
' }',
85+
'}'
86+
].join('\n'),
87+
parserOptions: parserOptions,
88+
errors: [{
89+
message: 'Component should be written as a pure function'
90+
}]
91+
}
92+
]
93+
});

0 commit comments

Comments
 (0)