Skip to content

Commit

Permalink
Add new rule for preventing this in SFCs
Browse files Browse the repository at this point in the history
This rule will report errors when a stateless functional component is
attempting to use `this`.
  • Loading branch information
jomasti committed Nov 20, 2017
1 parent e2ca690 commit 80fb96c
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -108,6 +108,7 @@ Finally, enable all of the rules that you would like to use. Use [our preset](#
* [react/no-set-state](docs/rules/no-set-state.md): Prevent usage of `setState`
* [react/no-typos](docs/rules/no-typos.md): Prevent common casing typos
* [react/no-string-refs](docs/rules/no-string-refs.md): Prevent using string references in `ref` attribute.
* [react/no-this-in-sfc](docs/rules/no-this-in-sfc.md): Prevent using `this` in stateless functional components
* [react/no-unescaped-entities](docs/rules/no-unescaped-entities.md): Prevent invalid characters from appearing in markup
* [react/no-unknown-property](docs/rules/no-unknown-property.md): Prevent usage of unknown DOM property (fixable)
* [react/no-unused-prop-types](docs/rules/no-unused-prop-types.md): Prevent definitions of unused prop types
Expand Down
61 changes: 61 additions & 0 deletions docs/rules/no-this-in-sfc.md
@@ -0,0 +1,61 @@
# Prevent `this` from being used in stateless functional components (react/no-this-in-sfc)

When using a stateless functional component (SFC), props/context aren't accessed in the same way as a class component or the `create-react-class` format. Both props and context are passed as separate arguments to the component instead. Also, as the name suggests, a stateless component does not have state on `this.state`.

Attempting to access properties on `this` can be a potential error if someone is unaware of the differences when writing a SFC or missed when converting a class component to a SFC.


## Rule Details

The following patterns are considered warnings:

```jsx
function Foo(props) {
return (
<div>{this.props.bar}</div>
);
}
```

```jsx
function Foo(props, context) {
return (
<div>
{this.context.foo ? this.props.bar : ''}
</div>
);
}
```

```jsx
function Foo(props) {
if (this.state.loading) {
return <Loader />;
}
return (
<div>
{this.props.bar}
</div>
);
}
```

The following patterns are **not** considered warnings:

```jsx
function Foo(props) {
return (
<div>{props.bar}</div>
);
}
```

```jsx
function Foo(props, context) {
return (
<div>
{context.foo ? props.bar : ''}
</div>
);
}
```
1 change: 1 addition & 0 deletions index.js
Expand Up @@ -55,6 +55,7 @@ const allRules = {
'no-string-refs': require('./lib/rules/no-string-refs'),
'no-redundant-should-component-update': require('./lib/rules/no-redundant-should-component-update'),
'no-render-return-value': require('./lib/rules/no-render-return-value'),
'no-this-in-sfc': require('./lib/rules/no-this-in-sfc'),
'no-typos': require('./lib/rules/no-typos'),
'no-unescaped-entities': require('./lib/rules/no-unescaped-entities'),
'no-unknown-property': require('./lib/rules/no-unknown-property'),
Expand Down
42 changes: 42 additions & 0 deletions lib/rules/no-this-in-sfc.js
@@ -0,0 +1,42 @@
/**
* @fileoverview Report "this" being used in stateless functional components.
*/
'use strict';

const Components = require('../util/Components');

// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------

const ERROR_MESSAGE = 'Stateless functional components should not use this';

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

module.exports = {
meta: {
docs: {
description: 'Report "this" being used in stateless components',
category: 'Possible Errors',
recommended: false
},
schema: []
},

create: Components.detect((context, components, utils) => ({
MemberExpression(node) {
const component = components.get(utils.getParentStatelessComponent());
if (!component) {
return;
}
if (node.object.type === 'ThisExpression') {
context.report({
node: node,
message: ERROR_MESSAGE
});
}
}
}))
};
9 changes: 8 additions & 1 deletion lib/util/Components.js
Expand Up @@ -319,6 +319,13 @@ function componentRule(rule, context) {
break;
case 'ArrowFunctionExpression':
property = 'body';
if (node[property] && node[property].type === 'BlockStatement') {
node = utils.findReturnStatement(node);
if (!node) {
return false;
}
property = 'argument';
}
break;
default:
node = utils.findReturnStatement(node);
Expand Down Expand Up @@ -430,7 +437,7 @@ function componentRule(rule, context) {
return null;
}
// Return the node if it is a function that is not a class method and is not inside a JSX Element
if (isFunction && !isMethod && !isJSXExpressionContainer) {
if (isFunction && !isMethod && !isJSXExpressionContainer && utils.isReturningJSX(node)) {
return node;
}
scope = scope.upper;
Expand Down
106 changes: 106 additions & 0 deletions tests/lib/rules/no-this-in-sfc.js
@@ -0,0 +1,106 @@
/**
* @fileoverview Report "this" being used in stateless functional components.
*/
'use strict';

// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------

const ERROR_MESSAGE = 'Stateless functional components should not use this';

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

const rule = require('../../../lib/rules/no-this-in-sfc');
const RuleTester = require('eslint').RuleTester;

const parserOptions = {
ecmaVersion: 8,
sourceType: 'module',
ecmaFeatures: {
experimentalObjectRestSpread: true,
jsx: true
}
};

const ruleTester = new RuleTester({parserOptions});
ruleTester.run('no-this-in-sfc', rule, {
valid: [{
code: `
function Foo(props) {
const { foo } = props;
return <div bar={foo} />;
}`
}, {
code: `
class Foo extends React.Component {
render() {
const { foo } = this.props;
return <div bar={foo} />;
}
}`
}, {
code: `
const Foo = createReactClass({
render: function() {
return <div>{this.props.foo}</div>;
}
});`
}, {
code: `
const Foo = React.createClass({
render: function() {
return <div>{this.props.foo}</div>;
}
});`,
settings: {react: {createClass: 'createClass'}}
}, {
code: `
function Foo (bar) {
this.bar = bar;
this.props = 'baz';
this.getFoo = function() {
return this.bar + this.props;
}
}
`
}],
invalid: [{
code: `
function Foo(props) {
const { foo } = this.props;
return <div>{foo}</div>;
}`,
errors: [{message: ERROR_MESSAGE}]
}, {
code: `
function Foo(props) {
return <div>{this.props.foo}</div>;
}`,
errors: [{message: ERROR_MESSAGE}]
}, {
code: `
function Foo(props) {
return <div>{this.state.foo}</div>;
}`,
errors: [{message: ERROR_MESSAGE}]
}, {
code: `
function Foo(props) {
const { foo } = this.state;
return <div>{foo}</div>;
}`,
errors: [{message: ERROR_MESSAGE}]
}, {
code: `
function Foo(props) {
function onClick(bar) {
this.props.onClick();
}
return <div onClick={onClick}>{this.props.foo}</div>;
}`,
errors: [{message: ERROR_MESSAGE}, {message: ERROR_MESSAGE}]
}]
});

0 comments on commit 80fb96c

Please sign in to comment.