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

feature: Support pure expressions in transform-react-constant-elements #4812

Merged

Conversation

STRML
Copy link
Contributor

@STRML STRML commented Nov 5, 2016

Q A
Bug fix? yes
Breaking change? no
New feature? yes
Deprecations? no
Spec compliancy? no
Tests added/pass? yes
Fixed tickets None
License MIT
Doc PR None

The Problem

Could describe this as a bugfix or feature, but when a pure expression of any sort is in a JSXExpression, the node is marked as mutable and isn't hoisted. This prevents otherwise eligible elements from being found.

For example, the following types of elements should be hoistable:

<div data-text={
  "Some text, " +
  "and some more too."
} />
function render(offset) {
  return function () {
    return <div tabIndex={offset + 1} />;
  };
}

The Fix

The fix is very simple: just add a check for path.isPure() as an out when determining when to hoist.

The expression will remain in the hoisted code, even though it could be statically evaluated. UglifyJS will take care of that in many cases.

Deopt

As noted in facebook/react#3226, it's not safe to reuse elements with mutable props. So the following should not be hoisted:

<div style={{width: 100}} />

This is done via an additional check to path.isObjectExpression(). Please correct me if this is not the way to find a mutable reference. Unfortunately, this will not catch things like frozen objects.

@STRML STRML force-pushed the jsx-constant-elements-pure-expressions branch from 189820b to 1886a84 Compare November 5, 2016 16:44
@codecov-io
Copy link

codecov-io commented Nov 5, 2016

Codecov Report

Merging #4812 into master will increase coverage by 0.05%.
The diff coverage is 100%.

@@            Coverage Diff             @@
##           master    #4812      +/-   ##
==========================================
+ Coverage   89.41%   89.46%   +0.05%     
==========================================
  Files         204      204              
  Lines        9929     9939      +10     
  Branches     2678     2683       +5     
==========================================
+ Hits         8878     8892      +14     
+ Misses       1051     1047       -4
Impacted Files Coverage Δ
...gin-transform-react-constant-elements/src/index.js 100% <100%> (ø)
packages/babel-traverse/src/scope/index.js 85.49% <ø> (+0.89%)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 4edcd02...a741ece. Read the comment docs.

@STRML STRML changed the title feature: Support pure expressions transform-react-constant-elements feature: Support pure expressions in transform-react-constant-elements Nov 5, 2016
@hzoo hzoo added i: enhancement PR: New Feature 🚀 A type of pull request used for our changelog categories and removed i: enhancement labels Nov 5, 2016
@hzoo
Copy link
Member

hzoo commented Nov 5, 2016

cc @sebmarkbage @gaearon @spicyj

Should note we probably want more deopts given https://github.com/babel/babel/labels/react

@STRML STRML force-pushed the jsx-constant-elements-pure-expressions branch from 1886a84 to 4a8aa71 Compare November 5, 2016 18:56
@STRML
Copy link
Contributor Author

STRML commented Nov 5, 2016

I've updated this to attempt to evaluate the expression passed:

  • If the result of the expression is confidently known and is immutable, we use it. This has the following result:
var Foo = React.createClass({
  render: function () {
    return (
      <div data-text={
        "Some text, " +
        "and some more too."
      } />
    );
  }
});
// becomes
var _ref = <div data-text={"Some text, and some more too."} />;

var Foo = React.createClass({
  render: function () {
    return _ref;
  }
});
  • If the result of the expression is not confidently known, but was deopted by an identifier, we hoist as far as we can, as to support:
function render(offset) {
  return function () {
    return <div tabIndex={offset + 1} />;
  };
}
// becomes
function render(offset) {
  var _ref = <div tabIndex={offset + 1} />;

  return function () {
    return _ref;
  };
}

Going to tackle some of the open react issues separately.

@STRML STRML force-pushed the jsx-constant-elements-pure-expressions branch from 4a8aa71 to 415144a Compare January 16, 2017 19:28
@STRML
Copy link
Contributor Author

STRML commented Jan 16, 2017

Rebased in hopes this will make it in with the rest of the constant-elements fixes @hzoo

@gaearon
Copy link
Member

gaearon commented Jan 16, 2017

Isn't it technically unsafe to hoist any React Element, if props are mutable?

It is safe because we freeze props (in DEV) inside createElement. So it doesn't work in development anyway (and is not supported).

// be mutated after render.
// https://github.com/facebook/react/issues/3226
if (t.isImmutable(resultNode)) {
path.replaceWith(resultNode);
Copy link
Member

Choose a reason for hiding this comment

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

Do we want to actually execute this replacement? This visitor at the moment has no side-effects, and it's not obvious that this gets us anything.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed - this is something for Babili or another uglifier. Have pushed a change.

@STRML STRML force-pushed the jsx-constant-elements-pure-expressions branch from 415144a to 48a7e49 Compare February 13, 2017 03:17
@STRML
Copy link
Contributor Author

STRML commented Feb 13, 2017

Amended to not actually replace the result of a pure expression (this can be done by minifiers). We still need to evaluate it to see if its result is immutable. I'm fairly confident we could safely replace, but it could be a source of bugs and is not really the responsibility of this plugin.

const expressionResult = path.evaluate();
if (expressionResult.confident) {
// We know the result; check its mutability.
const resultNode = t.valueToNode(expressionResult.value);
Copy link
Member

Choose a reason for hiding this comment

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

I just realized valueToNode throws if it is given a type that can't be converted to an AST Node, I'm not entirely sure I'm willing to assume that isPure() will cover all those cases. It likely does, but since they are not related from a code standpoint it makes me hesitant.

Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Technically the only values we can avoid deopting on are primitives; could skip this call and just check if number, bool, string, null, undefined.

Copy link
Member

Choose a reason for hiding this comment

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

Sounds good to me.

@STRML STRML force-pushed the jsx-constant-elements-pure-expressions branch from 48a7e49 to 045d23e Compare February 13, 2017 03:43
@STRML
Copy link
Contributor Author

STRML commented Feb 13, 2017

Amended.

@babel-bot
Copy link
Collaborator

Hey @STRML! It looks like one or more of your builds have failed. I've copied the relevant info below to save you some time.

@STRML STRML force-pushed the jsx-constant-elements-pure-expressions branch from 045d23e to a741ece Compare February 13, 2017 03:50
@loganfsmyth loganfsmyth merged commit 2aa2de8 into babel:master Feb 13, 2017
danez added a commit that referenced this pull request Feb 14, 2017
* Add new flow preset (#5288)

* Fix PathHoister hoisting JSX member expressions on "this". (#5143)

The PathHoister ignored member references on "this", causing it
to potentially hoist an expression above its function scope.

This patch tells the hoister to watch for "this", and if seen,
mark the nearest non-arrow function scope as the upper limit
for hoistng.

This fixes #4397 and is an alternative to #4787.

* Fix PathHoister hoisting before bindings. (#5153)

Fixes #5149 and enables a few additional safe hoists.

* Fix linting error

* feature: Support pure expressions in transform-react-constant-elements (#4812)

* Fix loose for-of with label (#5298)

* Rewrite Hub as interface #5047 (#5050)

* Rewrite Hub as interface #5047

* Update index.js

* Avoid adding unnecessary closure for block scoping (#5246)

When you write

```
for (const x of l) {
  setTimeout(() => x);
}
```

we need to add a closure because the variable is meant to be block-scoped and recreated each time the block runs. We do this.

However, we also add the closure when no loop is present. This isn't necessary, because if no loop is present then each piece of code runs at most once. I changed the transform to only add a closure if a variable is referenced from within a loop.

* Add greenkeeperio-bot to mention-bot blacklist (#5301) [skip ci]

* Upgrade lerna to current beta. (#5300)

* Revert "Upgrade lerna to current beta." (#5303)

* Add charset so tests work with convert-source-map@>1.4 (#5302)

* Add CHANGELOG for 6.23.0 [skip ci] (#5304)

* Update babel-types README from script.

* v6.23.0

* Revert change that lerna force-committed.

* Revert "Rewrite Hub as interface #5047" (#5306)

* v6.23.1

* Revert lerna again
// It is still safe to hoist that, so long as its result is immutable.
// If not, it is not safe to replace as mutable values (like objects) could be mutated after render.
// https://github.com/facebook/react/issues/3226
if (path.isPure()) {

Choose a reason for hiding this comment

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

pure template literals such as

<a className={`ok`}/>

is missed

Copy link
Member

Choose a reason for hiding this comment

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

@yiminghe isnt it covered already by isPure here?

Copy link

@yiminghe yiminghe Oct 18, 2017

Choose a reason for hiding this comment

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

the latest version seems to have template literals check: #5914

while the current stable version babel-traverse@6.26.0 does not @Andarist

@lock lock bot added the outdated A closed issue/PR that is archived due to age. Recommended to make a new issue label Oct 5, 2019
@lock lock bot locked as resolved and limited conversation to collaborators Oct 5, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: react outdated A closed issue/PR that is archived due to age. Recommended to make a new issue PR: New Feature 🚀 A type of pull request used for our changelog categories
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants