Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Class transform with property initializer #54

Merged
merged 65 commits into from
Jul 8, 2016

Conversation

keyz
Copy link
Member

@keyz keyz commented Jun 13, 2016

How it works

  1. Determine if mixins are convertible. We only transform a createClass call to an ES6 class component when:
    • There are no mixins on the class, or
    • options['pure-component'] is true, the mixins property is an array and it only contains pure render mixin (the specific module name can be specified using options['mixin-module-name'], which defaults to react-addons-pure-render-mixin)
  2. Ignore components that:
    • Call deprecated APIs. This is very defensive, if the script finds any identifiers called isMounted, getDOMNode, replaceProps, replaceState or setProps it will skip the component
    • Explicitly call this.getInitialState() and/or this.getDefaultProps() since an ES6 class component will no longer have these methods
    • Use arguments in methods since arrow functions don't have arguments. Also please notice that arguments should be very carefully used and it's generally better to switch to spread (...args) instead
    • Have inconvertible getInitialState(). Specifically if you have variable declarations like var props = ... and the right hand side is not this.props then we can't inline the state initialization in the constructor due to variable shadowing issues
    • Have non-primitive right hand side values (like foo: getStuff()) in the class spec
  3. Transform it to an ES6 class component
    1. Replace var A = React.createClass(spec) with class A extends React.Component {spec}. If a component uses pure render mixin and passes the mixins test (as described above), it will extend React.PureComponent instead
      • Remove the require/import statement that imports pure render mixin when it's no longer being referenced
    2. Pull out all statics defined on statics plus the few special cased statics like childContextTypes, contextTypes, displayName, getDefaultProps(), and propTypes and transform them to static properties (static propTypes = {...};)
      • If getDefaultProps() is simple (i.e. it only contains a return statement that returns an object) it will be converted to a simple assignment (static defaultProps = {...};). Otherwise an IIFE (immediately-invoked function expression) will be created (static defaultProps = function() { ... }();). Note that this means that the function will be executed only a single time per app-lifetime. In practice this hasn't caused any issues — getDefaultProps should not contain any side-effects
    3. Transform getInitialState()
      • If there's no getInitialState() or the getInitialState() function is simple (i.e., it only contains a return statement that returns an object) then we don't need a constructor; state will be lifted to a property initializer (state = {...};)
        • However, if the object contains references to this other than this.props and/or this.context, we can't be sure about what you'll need from this. We need to ensure that our property initializers' evaluation order is safe, so we defer state's initialization by moving it all the way down until all other property initializers have been initialized
      • If getInitialState() is not simple, we create a constructor and convert getInitialState() to an assignment to this.state
        • constructor always have props as the first parameter
        • We only put context as the second parameter when (one of) the following things happen in getInitialState():
          • It accesses this.context, or
          • There's a direct method call this.x(), or
          • this is referenced alone
        • Rewrite accesses to this.props to props and accesses to this.context to context since the values will be passed as constructor arguments
          • Remove simple variable declarations like var props = this.props; and var context = this.context
        • Rewrite top-level return statements (return {...};) to this.state = {...}
          • Add return; after the assignment when the return statement is part of a control flow statement (not a direct child of getInitialState()'s body) and not in an inner function declaration
    4. Transform all non-lifecycle methods and fields to class property initializers (like onClick = () => {};). All your Flow annotations will be preserved
      • It's actually not necessary to transform all methods to arrow functions (i.e., to bind them), but this behavior is the same as createClass() and we can make sure that we won't accidentally break stuff
  4. Generate Flow annotations from propTypes and put it on the class (this only happens when there's /* @flow */ in your code and options['flow'] is true)
    • Flow actually understands propTypes in createClass calls but not ES6 class components. Here the transformation logic is identical to how Flow treats propTypes
    • Notice that Flow treats an optional propType as non-nullable
      • For example, foo: React.PropTypes.number is valid when you pass {}, {foo: null}, or {foo: undefined} as props at runtime. However, when Flow infers type from a createClass call, only {} and {foo: undefined} are valid; {foo: null} is not. Thus the equivalent type annotation in Flow is actually {foo?: number}. The question mark on the left hand side indicates {} and {foo: undefined} are fine, but when foo is present it must be a number
    • For propTypes fields that can't be recognized by Flow, $FlowFixMe will be used

A few steps left:

  • no --no-super-class option
    • Fix readme
  • Support react-addons-pure-render-mixin
  • Fix flow annotations (the return type of arrow functions is missing for now)
  • Remove var ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin') when transformation happens
    • Check the scope before removing it (for cases where there are multiple components in a single file)

Update: discussed with @spicyj in person and here are the changes/required steps to finish before shipping this:

  • Transform propTypes to Flow annotations (props: ...) (a7f71a3)
    • We want to keep both propTypes and flow annotations so we only make it stricter
    • also transforms properties like count: (12: number), to count: number = 12; (231f629)
  • Support primitives as properties
  • Do not sort properties/methods so that the diff stays clean
  • Transform every method except lifecycle methods to arrow functions to make sure we don't break anything
  • Drop context and/or props in constructor(props, context) whenever possible
  • Add and fix no-use-before-define linter rule

New issues:

  • Fix early returns in getInitialState (b8bb77a)
  • Call super with props when getInitialState uses only this.props or this.props.foo but has no other usage of this (3936f55)

Good to fix:

  • Call super with props and context when getInitialState uses this in any other way (3936f55)
  • Defer state property initializer evaluation when getInitialState has any references to this (4f64cc2)
  • Bail out for dynamic mixins (ada3fb8)
  • Bail out if user calls this.getInitialState() anywhere (e00677f)
  • Bail out if arguments is used inside React methods (59e3671)
  • Fix compile error with const props = this.props in getInitialState (d67b6a8)
  • displayName should not show up twice
  • Handle nullable prop types properly (595bb97)

Backlog:

  • Add support for higher order components (const X = wrap(React.createClass(..)) crashes codemod) (19b611a)
  • Introduce a lint rule after this to prevent from over-binding

@keyz
Copy link
Member Author

keyz commented Jun 14, 2016

Updated to change the existing class codemod instead of creating a new one.

@nathanmarks
Copy link

nathanmarks commented Jun 14, 2016

@keyanzhang Is that the best idea? The benefit I see to keeping the existing class codemod is that it will better support es6 users who aren't using babel (or babel + stage 1). Class properties and initializers are a stage 1 proposal for es7.

@nathanmarks
Copy link

More food for thought -- if you want to combine them, could the stage-1 items such as property initializers be optional?

@keyz
Copy link
Member Author

keyz commented Jun 14, 2016

@nathanmarks Yeah, I think we could keep the both or them. Updating the existing one makes it much easier to review the diff, but I can switch to create new files later if necessary.

@sophiebits
Copy link
Member

For us, I think it makes the most sense to keep the version we're planning to use at Facebook in this repo. If people want the older version without property initializers it's easy to check out an old version – but I don't anticipate that many people will want this codemod script and convert all of the components until property initializers are more stable.

@keyz
Copy link
Member Author

keyz commented Jun 14, 2016

@gaearon: I added a temporary shrinkwrap file to pin recast to my fork and the tests pass locally. As soon as benjamn/recast#289 gets merged it will disappear 😃

@gaearon
Copy link
Member

gaearon commented Jun 15, 2016

👍 I’ll review later today, thank you.

@gaearon
Copy link
Member

gaearon commented Jun 15, 2016

Do we need the --no-super-class option?

No, not anymore. Component base class is required in 15.0+.

Remove var ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin') when transformation happens

I think we might want to also support removing import PureRenderMixin from 'react-addons-pure-render-mixin' and var PureRenderMixin = require('react-addons-pure-render-mixin');.

const hasInconvertibleMixins = classPath => {
if (
ReactUtils.hasMixins(classPath) &&
!ReactUtils.hasSpecificMixins(classPath, ['ReactComponentWithPureRenderMixin'])
Copy link
Member

Choose a reason for hiding this comment

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

For open source, it’s important that we support PureRenderMixin here. This is how we refer to it in the docs.

from the React API (lifecycle methods) and ignores functions that are being
called directly (unless it is both called directly and passed around to
somewhere else).
* TODO When `--no-super-class` is passed it only optionally extends
Copy link
Member

Choose a reason for hiding this comment

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

Don’t forget to remove this TODO 😉

@jide
Copy link

jide commented Jun 30, 2016

Shouldn't constructor() and super() be also passed context as argument ?

@gaearon
Copy link
Member

gaearon commented Jun 30, 2016

It doesn’t have to be passed unless you’re reading this.context in the constructor. React will set those fields automatically. So we opt for not surfacing context whenever possible. See also discussion above on this.

@jide
Copy link

jide commented Jun 30, 2016

Ok, thanks for the explanation !

Now we search and grab `React.createClass` _call expressions_ directly. The only time that we can't simply replace a `createClass` call path with a new class is when the parent of that is a variable declaration:

`var Foo = React.createClass({...});`
needs to be replaced entirely with
`class Foo extends React.Component {...}`

Now we delay checking it and only when we need to replace a path we take a look at `path.parentPath.value.type` to see if it's a variable declarator. With this commit we should be able to mod any kind of anonymous `createClass` calls no matter how they are defined.
@keyz keyz merged commit 8e370b9 into reactjs:master Jul 8, 2016
@gaearon
Copy link
Member

gaearon commented Jul 8, 2016

Congratulations!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants