-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add prefer-stateless-function rule (fixes #214)
- Loading branch information
Showing
5 changed files
with
289 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
# Enforce stateless React Components to be written as a pure function (prefer-stateless-function) | ||
|
||
Stateless functional components are more simple than class based components and will benefit from future React performance optimizations specific to these components. | ||
|
||
## Rule Details | ||
|
||
This rule will check your class based React components for | ||
|
||
* lifecycle methods: `state`, `getInitialState`, `componentWillMount`, `componentDidMount`, `componentWillReceiveProps`, `shouldComponentUpdate`, `componentWillUpdate`, `componentDidUpdate` and `componentWillUnmount` | ||
* usage of `this.setState` | ||
* presence of `ref` attribute in JSX | ||
|
||
If none of these 3 elements are found then the rule warn you to write this component as a pure function. | ||
|
||
The following patterns are considered warnings: | ||
|
||
```js | ||
var Hello = React.createClass({ | ||
render: function() { | ||
return <div>Hello {this.props.name}</div>; | ||
} | ||
}); | ||
``` | ||
|
||
```js | ||
class Hello extends React.Component { | ||
sayHello() { | ||
alert(`Hello ${this.props.name}`) | ||
} | ||
render() { | ||
return <div onClick={this.sayHello}>Hello {this.props.name}</div>; | ||
} | ||
} | ||
``` | ||
|
||
The following patterns are not considered warnings: | ||
|
||
```js | ||
const Foo = function(props) { | ||
return <div>{props.foo}</div>; | ||
}; | ||
``` | ||
|
||
```js | ||
class Foo extends React.Component { | ||
shouldComponentUpdate() { | ||
return false; | ||
} | ||
render() { | ||
return <div>{this.props.foo}</div>; | ||
} | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
/** | ||
* @fileoverview Enforce stateless components to be written as a pure function | ||
* @author Yannick Croissant | ||
*/ | ||
'use strict'; | ||
|
||
var Components = require('../util/Components'); | ||
|
||
// ------------------------------------------------------------------------------ | ||
// Rule Definition | ||
// ------------------------------------------------------------------------------ | ||
|
||
module.exports = Components.detect(function(context, components, utils) { | ||
|
||
var sourceCode = context.getSourceCode(); | ||
|
||
var lifecycleMethods = [ | ||
'state', | ||
'getInitialState', | ||
'componentWillMount', | ||
'componentDidMount', | ||
'componentWillReceiveProps', | ||
'shouldComponentUpdate', | ||
'componentWillUpdate', | ||
'componentDidUpdate', | ||
'componentWillUnmount' | ||
]; | ||
|
||
// -------------------------------------------------------------------------- | ||
// Public | ||
// -------------------------------------------------------------------------- | ||
|
||
/** | ||
* Get properties name | ||
* @param {Object} node - Property. | ||
* @returns {String} Property name. | ||
*/ | ||
function getPropertyName(node) { | ||
// Special case for class properties | ||
// (babel-eslint does not expose property name so we have to rely on tokens) | ||
if (node.type === 'ClassProperty') { | ||
var tokens = context.getFirstTokens(node, 2); | ||
return tokens[1] && tokens[1].type === 'Identifier' ? tokens[1].value : tokens[0].value; | ||
} | ||
|
||
return node.key.name; | ||
} | ||
|
||
/** | ||
* Get properties for a given AST node | ||
* @param {ASTNode} node The AST node being checked. | ||
* @returns {Array} Properties array. | ||
*/ | ||
function getComponentProperties(node) { | ||
switch (node.type) { | ||
case 'ClassDeclaration': | ||
return node.body.body; | ||
case 'ObjectExpression': | ||
return node.properties; | ||
default: | ||
return []; | ||
} | ||
} | ||
|
||
/** | ||
* Check if a given AST node have any lifecycle method | ||
* @param {ASTNode} node The AST node being checked. | ||
* @returns {Boolean} True if the node has at least one lifecycle method, false if not. | ||
*/ | ||
function hasLifecycleMethod(node) { | ||
var properties = getComponentProperties(node); | ||
return properties.some(function(property) { | ||
return lifecycleMethods.indexOf(getPropertyName(property)) !== -1; | ||
}); | ||
} | ||
|
||
/** | ||
* Mark a setState as used | ||
* @param {ASTNode} node The AST node being checked. | ||
*/ | ||
function markSetStateAsUsed(node) { | ||
components.set(node, { | ||
useSetState: true | ||
}); | ||
} | ||
|
||
/** | ||
* Mark a ref as used | ||
* @param {ASTNode} node The AST node being checked. | ||
*/ | ||
function markRefAsUsed(node) { | ||
components.set(node, { | ||
useRef: true | ||
}); | ||
} | ||
|
||
return { | ||
CallExpression: function(node) { | ||
var callee = node.callee; | ||
if (callee.type !== 'MemberExpression') { | ||
return; | ||
} | ||
if (callee.object.type !== 'ThisExpression' || callee.property.name !== 'setState') { | ||
return; | ||
} | ||
markSetStateAsUsed(node); | ||
}, | ||
|
||
JSXAttribute: function(node) { | ||
var name = sourceCode.getText(node.name); | ||
if (name !== 'ref') { | ||
return; | ||
} | ||
markRefAsUsed(node); | ||
}, | ||
|
||
'Program:exit': function() { | ||
var list = components.list(); | ||
for (var component in list) { | ||
if ( | ||
!list.hasOwnProperty(component) || | ||
hasLifecycleMethod(list[component].node) || | ||
list[component].useSetState || | ||
list[component].useRef || | ||
(!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node)) | ||
) { | ||
continue; | ||
} | ||
|
||
context.report({ | ||
node: list[component].node, | ||
message: 'Component should be written as a pure function' | ||
}); | ||
} | ||
} | ||
}; | ||
|
||
}); | ||
|
||
module.exports.schema = []; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
/** | ||
* @fileoverview Enforce stateless components to be written as a pure function | ||
* @author Yannick Croissant | ||
*/ | ||
'use strict'; | ||
|
||
// ------------------------------------------------------------------------------ | ||
// Requirements | ||
// ------------------------------------------------------------------------------ | ||
|
||
var rule = require('../../../lib/rules/prefer-stateless-function'); | ||
var RuleTester = require('eslint').RuleTester; | ||
|
||
var parserOptions = { | ||
ecmaVersion: 6, | ||
ecmaFeatures: { | ||
jsx: true | ||
} | ||
}; | ||
|
||
// ------------------------------------------------------------------------------ | ||
// Tests | ||
// ------------------------------------------------------------------------------ | ||
|
||
var ruleTester = new RuleTester(); | ||
ruleTester.run('prefer-stateless-function', rule, { | ||
|
||
valid: [ | ||
{ | ||
code: [ | ||
'const Foo = function(props) {', | ||
' return <div>{props.foo}</div>;', | ||
'};' | ||
].join('\n'), | ||
parserOptions: parserOptions | ||
}, { | ||
code: [ | ||
'class Foo extends React.Component {', | ||
' shouldComponentUpdate() {', | ||
' return fasle;', | ||
' }', | ||
' render() {', | ||
' return <div>{this.props.foo}</div>;', | ||
' }', | ||
'}' | ||
].join('\n'), | ||
parserOptions: parserOptions | ||
}, { | ||
code: 'const Foo = ({foo}) => <div>{foo}</div>;', | ||
parserOptions: parserOptions | ||
}, { | ||
code: [ | ||
'class Foo extends React.Component {', | ||
' changeState() {', | ||
' this.setState({foo: "clicked"});', | ||
' }', | ||
' render() {', | ||
' return <div onClick={this.changeState.bind(this)}>{this.state.foo || "bar"}</div>;', | ||
' }', | ||
'}' | ||
].join('\n'), | ||
parserOptions: parserOptions | ||
}, { | ||
code: [ | ||
'class Foo extends React.Component {', | ||
' doStuff() {', | ||
' this.refs.foo.style.backgroundColor = "red";', | ||
' }', | ||
' render() {', | ||
' return <div ref="foo" onClick={this.doStuff}>{this.props.foo}</div>;', | ||
' }', | ||
'}' | ||
].join('\n'), | ||
parserOptions: parserOptions | ||
} | ||
], | ||
|
||
invalid: [ | ||
{ | ||
code: [ | ||
'class Foo extends React.Component {', | ||
' render() {', | ||
' return <div>{this.props.foo}</div>;', | ||
' }', | ||
'}' | ||
].join('\n'), | ||
parserOptions: parserOptions, | ||
errors: [{ | ||
message: 'Component should be written as a pure function' | ||
}] | ||
} | ||
] | ||
}); |