Compconv

Sung

sung.io

Write better code by automatically switching between function and class components

  1. Problem
  2. Demo
  3. How It Works

Problem

Tedious to translate between function and class component

import React from 'react';

class Home extends React.Component {
  render() {
    return (
      
Home
) } }
import React from 'react';

function Home() {
  return (
    
Home
) }

function => class

  1. To use lifecycle methods
  2. To keep states

class => function

  1. To reduce footprint
  2. For performance

Smaller footprint

import React from 'react';

class Home extends React.Component {
  render() {
    return (
      
Home
) } }
import React from 'react';

function Home() {
  return (
    
Home
) }

babel output for class

'use strict';

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _react = require('react');

var _react2 = _interopRequireDefault(_react);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var Home = function (_React$Component) {
    _inherits(Home, _React$Component);

    function Home() {
        _classCallCheck(this, Home);

        return _possibleConstructorReturn(this, (Home.__proto__ || Object.getPrototypeOf(Home)).apply(this, arguments));
    }

    _createClass(Home, [{
        key: 'render',
        value: function render() {
            return _react2.default.createElement(
                'div',
                null,
                'Home'
            );
        }
    }]);

    return Home;
}(_react2.default.Component);

babel output for function

'use strict';

var _react = require('react');

var _react2 = _interopRequireDefault(_react);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function Home() {
  return _react2.default.createElement(
    'div',
    null,
    'Home'
  );
}

Performance

Functional components are faster to render

function renderApp(i) {
  if (i === 1000) {
    const t2 = performance.now();
    console.log("Took", t2 - t1, "milliseconds");
    return;
  }

  ReactDOM.render(<Home />, document.getElementById("root"), () => {
    renderApp(i + 1);
  });
}

const t1 = performance.now();
renderApp(1);
type mean min max stddev
function 546 473 712 78
class 577 (-5%) 510 674 57

React 16.4.2 on Firefox 61.0.1 on macOS 10.12.5

But It's too hard to convert components

So we make excuses and write inefficient code

Demo

  • 102 keystrokes
  • 28 seconds
  • 0 keystrokes
  • 3 seconds

Comparison

method keystrokes time (s)
manual 102 (+∞%) 28 (+833%)
compconv 0 3

How it works

  1. Build an abstract syntax tree
  2. Collect information by walking the tree
  3. Output a new format
export default function(code) {
  // 1. build ast
  const ast = babylon.parse(code, {
    sourceType: "module",
    plugins: ["jsx"]
  });

  // 2. walk the tree
  const context = {
    namedExport: null,
    defaultExport: null,
    identifier: null,
    type: null,
    propTree: {},
    jsxBodyTree: null
  };

  walkTree(ast.program, context);

  // 3. output a new format
  return output(context);
}

1. Build an abstract syntax tree

A tree representation of a source code

function Home() {
  const { foo, bar } = this.props;

  return (
    
hello { foo } { bar }
) }
const babylon = require("babylon");

const sample = `function Home() {
  const { foo, bar } = this.props;

  return (
    
hello { foo } { bar }
) }`; const ast = babylon.parse(sample, { sourceType: "module", plugins: ["jsx"] }); console.log(JSON.stringify(ast, null, 2));
function Home() {
  const { foo, bar } = this.props;

  return (
    
hello { foo } { bar }
) }
{
  "type": "File",
  "start": 0,
  "end": 119,
  "loc": {
    "start": {
      "line": 1,
      "column": 0
    },
    "end": {
      "line": 9,
      "column": 1
    }
  },
  "program": {
    "type": "Program",
    "start": 0,
    "end": 119,
    "loc": {
      "start": {
        "line": 1,
        "column": 0
      },
      "end": {
        "line": 9,
        "column": 1
      }
    },
    "sourceType": "module",
    "body": [
      {
        "type": "FunctionDeclaration",
        "start": 0,
        "end": 119,
        "loc": {
          "start": {
            "line": 1,
            "column": 0
          },
          "end": {
            "line": 9,
            "column": 1
          }
        },
        "id": {
          "type": "Identifier",
          "start": 9,
          "end": 13,
          "loc": {
            "start": {
              "line": 1,
              "column": 9
            },
            "end": {
              "line": 1,
              "column": 13
            },
            "identifierName": "Home"
          },
          "name": "Home"
        },
        "generator": false,
        "expression": false,
        "async": false,
        "params": [],
        "body": {
          "type": "BlockStatement",
          "start": 16,
          "end": 119,
          "loc": {
            "start": {
              "line": 1,
              "column": 16
            },
            "end": {
              "line": 9,
              "column": 1
            }
          },
          "body": [
            {
              "type": "VariableDeclaration",
              "start": 20,
              "end": 52,
              "loc": {
                "start": {
                  "line": 2,
                  "column": 2
                },
                "end": {
                  "line": 2,
                  "column": 34
                }
              },
              "declarations": [
                {
                  "type": "VariableDeclarator",
                  "start": 26,
                  "end": 51,
                  "loc": {
                    "start": {
                      "line": 2,
                      "column": 8
                    },
                    "end": {
                      "line": 2,
                      "column": 33
                    }
                  },
                  "id": {
                    "type": "ObjectPattern",
                    "start": 26,
                    "end": 38,
                    "loc": {
                      "start": {
                        "line": 2,
                        "column": 8
                      },
                      "end": {
                        "line": 2,
                        "column": 20
                      }
                    },
                    "properties": [
                      {
                        "type": "ObjectProperty",
                        "start": 28,
                        "end": 31,
                        "loc": {
                          "start": {
                            "line": 2,
                            "column": 10
                          },
                          "end": {
                            "line": 2,
                            "column": 13
                          }
                        },
                        "method": false,
                        "shorthand": true,
                        "computed": false,
                        "key": {
                          "type": "Identifier",
                          "start": 28,
                          "end": 31,
                          "loc": {
                            "start": {
                              "line": 2,
                              "column": 10
                            },
                            "end": {
                              "line": 2,
                              "column": 13
                            },
                            "identifierName": "foo"
                          },
                          "name": "foo"
                        },
                        "value": {
                          "type": "Identifier",
                          "start": 28,
                          "end": 31,
                          "loc": {
                            "start": {
                              "line": 2,
                              "column": 10
                            },
                            "end": {
                              "line": 2,
                              "column": 13
                            },
                            "identifierName": "foo"
                          },
                          "name": "foo"
                        },
                        "extra": {
                          "shorthand": true
                        }
                      },
                      {
                        "type": "ObjectProperty",
                        "start": 33,
                        "end": 36,
                        "loc": {
                          "start": {
                            "line": 2,
                            "column": 15
                          },
                          "end": {
                            "line": 2,
                            "column": 18
                          }
                        },
                        "method": false,
                        "shorthand": true,
                        "computed": false,
                        "key": {
                          "type": "Identifier",
                          "start": 33,
                          "end": 36,
                          "loc": {
                            "start": {
                              "line": 2,
                              "column": 15
                            },
                            "end": {
                              "line": 2,
                              "column": 18
                            },
                            "identifierName": "bar"
                          },
                          "name": "bar"
                        },
                        "value": {
                          "type": "Identifier",
                          "start": 33,
                          "end": 36,
                          "loc": {
                            "start": {
                              "line": 2,
                              "column": 15
                            },
                            "end": {
                              "line": 2,
                              "column": 18
                            },
                            "identifierName": "bar"
                          },
                          "name": "bar"
                        },
                        "extra": {
                          "shorthand": true
                        }
                      }
                    ]
                  },
                  "init": {
                    "type": "MemberExpression",
                    "start": 41,
                    "end": 51,
                    "loc": {
                      "start": {
                        "line": 2,
                        "column": 23
                      },
                      "end": {
                        "line": 2,
                        "column": 33
                      }
                    },
                    "object": {
                      "type": "ThisExpression",
                      "start": 41,
                      "end": 45,
                      "loc": {
                        "start": {
                          "line": 2,
                          "column": 23
                        },
                        "end": {
                          "line": 2,
                          "column": 27
                        }
                      }
                    },
                    "property": {
                      "type": "Identifier",
                      "start": 46,
                      "end": 51,
                      "loc": {
                        "start": {
                          "line": 2,
                          "column": 28
                        },
                        "end": {
                          "line": 2,
                          "column": 33
                        },
                        "identifierName": "props"
                      },
                      "name": "props"
                    },
                    "computed": false
                  }
                }
              ],
              "kind": "const"
            },
            {
              "type": "ReturnStatement",
              "start": 56,
              "end": 117,
              "loc": {
                "start": {
                  "line": 4,
                  "column": 2
                },
                "end": {
                  "line": 8,
                  "column": 3
                }
              },
              "argument": {
                "type": "JSXElement",
                "start": 69,
                "end": 113,
                "loc": {
                  "start": {
                    "line": 5,
                    "column": 4
                  },
                  "end": {
                    "line": 7,
                    "column": 10
                  }
                },
                "openingElement": {
                  "type": "JSXOpeningElement",
                  "start": 69,
                  "end": 74,
                  "loc": {
                    "start": {
                      "line": 5,
                      "column": 4
                    },
                    "end": {
                      "line": 5,
                      "column": 9
                    }
                  },
                  "attributes": [],
                  "name": {
                    "type": "JSXIdentifier",
                    "start": 70,
                    "end": 73,
                    "loc": {
                      "start": {
                        "line": 5,
                        "column": 5
                      },
                      "end": {
                        "line": 5,
                        "column": 8
                      }
                    },
                    "name": "div"
                  },
                  "selfClosing": false
                },
                "closingElement": {
                  "type": "JSXClosingElement",
                  "start": 107,
                  "end": 113,
                  "loc": {
                    "start": {
                      "line": 7,
                      "column": 4
                    },
                    "end": {
                      "line": 7,
                      "column": 10
                    }
                  },
                  "name": {
                    "type": "JSXIdentifier",
                    "start": 109,
                    "end": 112,
                    "loc": {
                      "start": {
                        "line": 7,
                        "column": 6
                      },
                      "end": {
                        "line": 7,
                        "column": 9
                      }
                    },
                    "name": "div"
                  }
                },
                "children": [
                  {
                    "type": "JSXText",
                    "start": 74,
                    "end": 87,
                    "loc": {
                      "start": {
                        "line": 5,
                        "column": 9
                      },
                      "end": {
                        "line": 6,
                        "column": 12
                      }
                    },
                    "extra": null,
                    "value": "\n      hello "
                  },
                  {
                    "type": "JSXExpressionContainer",
                    "start": 87,
                    "end": 94,
                    "loc": {
                      "start": {
                        "line": 6,
                        "column": 12
                      },
                      "end": {
                        "line": 6,
                        "column": 19
                      }
                    },
                    "expression": {
                      "type": "Identifier",
                      "start": 89,
                      "end": 92,
                      "loc": {
                        "start": {
                          "line": 6,
                          "column": 14
                        },
                        "end": {
                          "line": 6,
                          "column": 17
                        },
                        "identifierName": "foo"
                      },
                      "name": "foo"
                    }
                  },
                  {
                    "type": "JSXText",
                    "start": 94,
                    "end": 95,
                    "loc": {
                      "start": {
                        "line": 6,
                        "column": 19
                      },
                      "end": {
                        "line": 6,
                        "column": 20
                      }
                    },
                    "extra": null,
                    "value": " "
                  },
                  {
                    "type": "JSXExpressionContainer",
                    "start": 95,
                    "end": 102,
                    "loc": {
                      "start": {
                        "line": 6,
                        "column": 20
                      },
                      "end": {
                        "line": 6,
                        "column": 27
                      }
                    },
                    "expression": {
                      "type": "Identifier",
                      "start": 97,
                      "end": 100,
                      "loc": {
                        "start": {
                          "line": 6,
                          "column": 22
                        },
                        "end": {
                          "line": 6,
                          "column": 25
                        },
                        "identifierName": "bar"
                      },
                      "name": "bar"
                    }
                  },
                  {
                    "type": "JSXText",
                    "start": 102,
                    "end": 107,
                    "loc": {
                      "start": {
                        "line": 6,
                        "column": 27
                      },
                      "end": {
                        "line": 7,
                        "column": 4
                      }
                    },
                    "extra": null,
                    "value": "\n    "
                  }
                ],
                "extra": {
                  "parenthesized": true,
                  "parenStart": 63
                }
              }
            }
          ],
          "directives": []
        }
      }
    ]
  }
}

2. Collect information by walking the tree

const context = {
  namedExport: null,
  defaultExport: null,
  identifier: null,
  type: null,
  propTree: {},
  jsxBodyTree: null
};

walkTree(ast.program, context);
// walkTree traverses an abstract syntax tree
// and modifies the given context
function walkTree(node, ctx) {
  switch (node.type) {
    case "Program": {/* ... */}
    case "ExportDefaultDeclaration": {/* ... */}
    case "ClassDeclaration": {/* ... */}
    case "ClassBody": {/* ... */}
    case "BlockStatement": {/* ... */}
    case "VariableDeclaration": {/* ... */}
    case "JSXExpressionContainer": {/* ... */}
  }

  return ctx;
}

3. Output a new format

function outputClass(ctx, code) {
  let id;
  if (ctx.identifier) {
    id = ctx.identifier;
  } else {
    id = "MyComponent";
  }

  let ret = `class ${id} extends React.Component {
  render() {
    ${destructureProps(ctx.propTree)}

    return (
${indentCode(code, "      ")}
    )
  }
}`;

  return ret;
}

Using this core library,
we can make editor plugins!

Repositories

Still WIP. Let's build together.

Write better code by automatically switching between function and class components

Sung

sung.io