Skip to content
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,15 @@ jscodeshift -t react-codemod/transforms/manual-bind-to-arrow.js <path>

#### `pure-component`

Converts ES6 classes that only have a render method, only have safe properties
(statics and props), and do not have refs to Stateless Functional Components.

Option `useArrows` converts to arrow function. Converts to `function` by default.
Option `destructure` will destructure props in the argument where it is safe to do so.
Note these options must be passed on the command line as `--useArrows=true` (`--useArrows` won't work)

```sh
jscodeshift -t react-codemod/transforms/pure-component.js <path>
jscodeshift -t react-codemod/transforms/pure-component.js <path> [--useArrows=true --destructure=true]
```

#### `pure-render-mixin`
Expand Down
36 changes: 36 additions & 0 deletions transforms/__testfixtures__/pure-component-destructure.input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use strict';

var React = require('React');

function doSomething(props) { return props; }

class ShouldDestsructure extends React.Component {
render() {
return <div className={this.props.foo} />;
}
}

class ShouldDestructureAndRemoveDuplicateDeclaration extends React.Component {
render() {
const fizz = { buzz: 'buzz' };
const bar = this.props.bar;
const baz = this.props.bizzaz;
const buzz = fizz.buzz;
return <div className={this.props.foo} bar={bar} baz={baz} buzz={buzz} />;
}
}

class UsesThisDotProps extends React.Component {
render() {
doSomething(this.props);
return <div className={this.props.foo} />;
}
}

class DestructuresThisDotProps extends React.Component {
// would be nice to destructure in this case
render() {
const { bar } = this.props;
return <div className={this.props.foo} bar={bar} />;
}
}
36 changes: 36 additions & 0 deletions transforms/__testfixtures__/pure-component-destructure.output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use strict';

var React = require('React');

function doSomething(props) { return props; }

function ShouldDestsructure(
{
foo,
},
) {
return <div className={foo} />;
}

function ShouldDestructureAndRemoveDuplicateDeclaration(
{
bar,
bizzaz,
foo,
},
) {
const fizz = { buzz: 'buzz' };
const baz = bizzaz;
const buzz = fizz.buzz;
return <div className={foo} bar={bar} baz={baz} buzz={buzz} />;
}

function UsesThisDotProps(props) {
doSomething(props);
return <div className={props.foo} />;
}

function DestructuresThisDotProps(props) {
const { bar } = props;
return <div className={props.foo} bar={bar} />;
}
3 changes: 2 additions & 1 deletion transforms/__tests__/pure-component-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@

const defineTest = require('jscodeshift/dist/testUtils').defineTest;
defineTest(__dirname, 'pure-component');
defineTest(__dirname, 'pure-component', {useArrows: true}, 'pure-component2');
defineTest(__dirname, 'pure-component', { useArrows: true }, 'pure-component2');
defineTest(__dirname, 'pure-component', { destructure: true }, 'pure-component-destructure');
102 changes: 80 additions & 22 deletions transforms/pure-component.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module.exports = function(file, api, options) {
const ReactUtils = require('./utils/ReactUtils')(j);

const useArrows = options.useArrows || false;
const destructureEnabled = options.destructure || false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const silenceWarnings = options.silenceWarnings || false;
const printOptions = options.printOptions || {
quote: 'single',
Expand Down Expand Up @@ -85,31 +86,83 @@ module.exports = function(file, api, options) {
return identifier;
};

const canDestructure = path => path
.find(j.Identifier, {
name: 'props'
})
.filter(p => p.parentPath.parentPath.value.type !== 'MemberExpression')
.size() === 0;

const createShorthandProperty = j => prop => {
const property = j.property('init', j.identifier(prop), j.identifier(prop));
property.shorthand = true;
return property;
};

const isDuplicateDeclaration = path => {
if (path && path.value && path.value.id && path.value.init) {
return path.value.id.name === path.value.init.name;
}
return false;
};

const destructureProps = body => {
const toDestructure = body.find(j.MemberExpression, {
object: {
name: 'props'
}
});
if (toDestructure) {
const propNames = new Set();
toDestructure.replaceWith(path => {
const propName = path.value.property.name;
propNames.add(propName);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this approach (adding names to the set while replacing) is efficient, it introduces some potential shadowing issues. For example:

Input:

class Shadowing extends React.Component { render() { let style = { color: 'black' }; return <div style={{...style, ...this.props.style}} />; } }

Output:

function Shadowing( { style, }, ) { let style = { color: 'black' }; return <div style={{...style, ...style}} />; }

I think a better way here is to scan the whole function twice (using forEach), once for identifiers and once for prop property accesses; then we can compare the two sets to see if it's possible to apply destructuring here.

return j.identifier(propName);
});
if (propNames.size > 0) {
const assignments = body.find(j.VariableDeclarator);
const duplicateAssignments = assignments.filter(isDuplicateDeclaration);
duplicateAssignments.remove();
return j.objectExpression(Array.from(propNames).map(createShorthandProperty(j)));
}
}
return false;
};

const findPropsTypeAnnotation = body => {
const property = body.find(isPropsProperty);

return property && property.typeAnnotation.typeAnnotation;
};

const buildPureComponentFunction = (name, body, typeAnnotation) =>
j.functionDeclaration(
j.identifier(name),
[buildIdentifierWithTypeAnnotation('props', typeAnnotation)],
body
);

const buildPureComponentArrowFunction = (name, body, typeAnnotation) =>
j.variableDeclaration(
'const', [
j.variableDeclarator(
j.identifier(name),
j.arrowFunctionExpression(
[buildIdentifierWithTypeAnnotation('props', typeAnnotation)],
body
)
),
]
);
const build = ({ functionType }) => (name, body, typeAnnotation, destructure) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: unnecessary to use a named parameter here

const identifier = j.identifier(name);
const propsIdentifier = buildIdentifierWithTypeAnnotation('props', typeAnnotation);
const propsArg = [(destructure && destructureProps(j(body))) || propsIdentifier];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it drops existing type annotations built by buildIdentifierWithTypeAnnotation.

Input:

class PureWithTypes extends React.Component { props: { foo: string }; render() { return <div className={this.props.foo} />; } }

Output:

function PureWithTypes( { foo, }, ) { return <div className={foo} />; }
if (functionType === 'fn') {
return j.functionDeclaration(
identifier,
propsArg,
body
);
} else {
return j.variableDeclaration(
'const', [
j.variableDeclarator(
identifier,
j.arrowFunctionExpression(
propsArg,
body
)
),
]
);
}
};

const buildPureComponentFunction = build({ functionType: 'fn' });

const buildPureComponentArrowFunction = build({ functionType: 'arrow' });

const buildStatics = (name, properties) => properties.map(prop => (
j.expressionStatement(
Expand Down Expand Up @@ -154,17 +207,22 @@ module.exports = function(file, api, options) {
const renderBody = renderMethod.value.body;
const propsTypeAnnotation = findPropsTypeAnnotation(p.value.body.body);
const statics = p.value.body.body.filter(isStaticProperty);
const destructure = destructureEnabled && canDestructure(j(renderMethod));

replaceThisProps(renderBody);
if (destructureEnabled && !destructure) {
console.warn(`Unable to destructure ${name} props. Render method references \`this.props\`.`);
}

replaceThisProps(renderBody);

if (useArrows) {
return [
buildPureComponentArrowFunction(name, renderBody, propsTypeAnnotation),
buildPureComponentArrowFunction(name, renderBody, propsTypeAnnotation, destructure),
...buildStatics(name, statics)
];
} else {
return [
buildPureComponentFunction(name, renderBody, propsTypeAnnotation),
buildPureComponentFunction(name, renderBody, propsTypeAnnotation, destructure),
...buildStatics(name, statics)
];
}
Expand Down