Skip to content

Commit

Permalink
Add prefer-stateless-function rule (fixes #214)
Browse files Browse the repository at this point in the history
  • Loading branch information
yannickcr committed Mar 1, 2016
1 parent 1b0caca commit 0830226
Show file tree
Hide file tree
Showing 5 changed files with 289 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ The plugin has a [recommended configuration](#user-content-recommended-configura
* [no-string-refs](docs/rules/no-string-refs.md): Prevent using string references in `ref` attribute.
* [no-unknown-property](docs/rules/no-unknown-property.md): Prevent usage of unknown DOM property (fixable)
* [prefer-es6-class](docs/rules/prefer-es6-class.md): Enforce ES5 or ES6 class for React Components
* [prefer-stateless-function](docs/rules/prefer-stateless-function.md): Enforce stateless React Components to be written as a pure function
* [prop-types](docs/rules/prop-types.md): Prevent missing props validation in a React component definition
* [react-in-jsx-scope](docs/rules/react-in-jsx-scope.md): Prevent missing `React` when using JSX
* [require-extension](docs/rules/require-extension.md): Restrict file extensions that may be required
Expand Down
53 changes: 53 additions & 0 deletions docs/rules/prefer-stateless-function.md
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>;
}
}
```
3 changes: 2 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ module.exports = {
'forbid-prop-types': require('./lib/rules/forbid-prop-types'),
'prefer-es6-class': require('./lib/rules/prefer-es6-class'),
'jsx-key': require('./lib/rules/jsx-key'),
'no-string-refs': require('./lib/rules/no-string-refs')
'no-string-refs': require('./lib/rules/no-string-refs'),
'prefer-stateless-function': require('./lib/rules/prefer-stateless-function')
},
configs: {
recommended: {
Expand Down
140 changes: 140 additions & 0 deletions lib/rules/prefer-stateless-function.js
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 = [];
93 changes: 93 additions & 0 deletions tests/lib/rules/prefer-stateless-function.js
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'
}]
}
]
});

0 comments on commit 0830226

Please sign in to comment.