blog.koba04.comkoba04's twitter accountkoba04's GitHub account

React.js v0.13 changes

2015/03/05 @koba04

React.js v0.13のRC2がリリースされたのでまとめてみます。

今回のバージョンで何か大きく変更があるというよりもv0.14でやりたいことに向けての布石が多いように感じます。

試すときはこの辺りから。

npm install react@0.13.0-rc2 
npm install react-tools@0.13.0-rc2 
http://fb.me/react-0.13.0-rc2.js
http://fb.me/react-0.13.0-rc2.min.js
http://fb.me/react-with-addons-0.13.0-rc2.js
http://fb.me/react-with-addons-0.13.0-rc2.min.js
http://fb.me/JSXTransformer-0.13.0-rc2.js

Propを変更するとwarninngが出ます (Breaking Change)

development環境でPropをelement作成後に変更することはdeprecatedになってwarningが出るようになりました。 つまりimmutableなものとして扱う必要があります。

var element = <Foo bar={false} />;
if (shouldUseFoo) {
  element.props.foo = 10;
  element.props.bar = true;
}

これまでの問題点

  • Propを直接変更してしまうと元の値を破棄してしまうのでdiffがなくなってしまいます。この場合、shouldComponentUpdateを実装している場合に比較時に差分を検出出来なくてDOM構造に差分があるはずなのに実際には反映されない可能性がありました。
  • またPropが変更されることがあるためcreateElementの時点でPropTypesのValidationも出来ず、それによってエラー時のstacktraceが深くなったりFlowによる静的解析にとっても都合がよくなかったりという面もありました。

それに対しての提案

  • 動的にしたい場合は↓のような形で書くことでも可能です。
if (shouldUseFoo) {
  return <Foo foo={10} bar={true} />;
} else {
  return <Foo bar={false} />;
}

var props = { bar: false };
if (shouldUseFoo) {
  props.foo = 10;
  props.bar = true;
}
return <Foo {...props} />;
  • 現時点ではネストしたオブジェクトについては変更してもwarningは出ません。基本的にはimmutable.jsなどを使って完全にimmutableに扱った方がいいですが、mutableなオブジェクトは多くの場面で便利だし今回はネストしたオブジェクトはwarningの対象外となりました。
return <Foo nestedObject={this.state.myModel} />;
  • PropTypesのwarningをReactElementの作成時に行うなうようになりました。Propを変更するために↓のようにcloneしてReactElementにPropに値を追加するのは正しい方法です。
var element1 = <Foo />; // extra prop is optional
var element2 = React.addons.cloneWithProps(element1, { extra: 'prop' });

statics内のメソッドに対してautobindingされなくなりました (Breaking Change)

staticsに定義したメソッドをonClickなどにバインドした時にcomponentをバインドしなくなりました。

var Hello = React.createClass({
  statics: {
    foo () {
      this.bar();  // v0.13では呼べない
    },
    bar() {
      console.log("bar");
    }
  },
  render() {
    return <div>hello <button onClick={Hello.foo}>click</button></div>;
  }
});

refを設定する処理の順番が変わりました (Breaking Change)

refに指定されたcomponentのcomponentDidMountが呼ばれた後になります。 これは親componentのcallbackをcomponentDidMountの中で読んでいる場合だけ気にする必要があります。そもそれもこれはアンチパターンなので避けるべきですが...。

  • componentDidMountは子componentから順番に呼ばれるので下記のrefDivはChildのcomponentDidMountの時点では設定されていません。
var Hello = React.createClass({
  foo() {
    console.log(this.refs.refDiv);
  },
  render() {
    return (
      <div>
        <Child foo={this.foo} />
        <div ref="refDiv">hello</div>
      </div>
    );
  }
});

var Child = React.createClass({
  componentDidMount() {
    this.props.foo(); // v0.13 "undefined"
  },
  render() {
    return <div>child</div>;
  }
});

this.setState()が第1引数に関数を受け取れるようになりました

this.setState((state, props) => ({count: state.count + 1}));

のようにすることでthis._pendingStateを使うことなくトランザクションが必要とされるstateの更新を行うことが出来ます。

console.log(this.state.count) // 0
this.setState({ count: this.state.count + 1 })
this.setState({ count: this.state.count + 1 })
// state.count will render as 1

console.log(this.state.count) // 0
this.setState(function(state, props) { return { count: state.count + 1 } });
this.setState(function(state, props) { return { count: state.count + 1 } });
// state.count will render as 2

setStateの呼び出しが常に非同期になります (Breaking Change)

ライフサイクルメソッドの中でのsetStateの呼び出しが常に非同期でバッチとして処理されます。以前は最初のマウント時の呼び出しは同期的に行われていました。

componentDidMount() {
  console.log(this.state.count) // 0
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
  console.log(this.state.count) // v0.13 is 0 (v0.12 is 2)
}

setStateとforceUpdateをunmountされたcomponentに対して呼んだ時に、エラーではなくwarningが出るようになりました (Breaking Change)

非同期処理の結果をsetStateして反映させるときに、isMountedでブロックしなくてもよくなったのはいいですね。

privateなプロパティが整理されました (Breaking Change)

this._pendingStatethis._rootNodeIDなどのprivateなプロパティが削除されました。

ES6 classesによるReactComponentの作成がサポートされました

これについては↓に書きましたが、ES6 classesによって作成されたcomponentにはcreateClassにはあるgetDOMNodesetPropsreplaceStateが含まれていなかったりmixinが指定出来ないなど注意点がいくつかあります。

React.findDOMNode(component)のAPIが追加されました

これは既存のcomponent.getDOMNode()を置き換えるAPIです。 getDOMNode()はES6 classesによって作成されたcomponentでは提供されていません。

refがcallbackスタイルで指定できるようになりました。

<Photo ref={(c) => this._photo = c} />

この変更はこの後で書くownerの扱いの変更に関係しています。

childrenにiteratorやimmutable-jsのsequenceを指定出来るようになりました

immutable-jsを使っている人にとってはいいですね。

ComponentClass.typeはdeprecatedになりました

代わりにComponentClassをそのまま使ってください。

ownerベースのcontextを使っていてparentベースのcontextと一致しない場合にwarningが出るようになります

そもそもowner? parent?という感じかと思うので簡単に説明します。

owner and parent

Reactは"parent"と"owner"を持っています。"owner"はReactElementを作ったcomponentです。

class Foo {
  render() {
    return <div><span /></div>;
  }
}

この場合、spanのownerはFooでparentはdivになります。

context

これはdocument化されてないfeatureですが、"owner"から子や孫に渡すことが出来る"context"というものがあります。

簡単にコードを書くとこんな感じです。見てもらえればどんなfeatureなのかわかるかと思います。

var Parent = React.createClass({
    childContextTypes: {
      name: React.PropTypes.string,
      age: React.PropTypes.number
    },
    getChildContext: function() {
      return {
        name: "parent",
        age: 50
      };
  },
  render: function() {
    return <Child />;
  }
});

var Child = React.createClass({
    contextTypes: {
      name: React.PropTypes.string,
      age: React.PropTypes.number
    },
    componentDidMount: function() {
      console.log("Child",this.context); // {name: "parent", age: 50}
    },
    render: function() {
      return <GrandChild />;
    }
});

var GrandChild = React.createClass({
    contextTypes: {
      name: React.PropTypes.string
    },
    componentDidMount: function() {
      console.log("GrandChild",this.context); // {name: "parent"}
    },
    render: function() {
      return <div>hello</div>;
    }
});

React.render(<Parent />, document.body);

react-routerではparentベースのcontextに依存していたので対応が大変そうでした。

問題点

  • ownerは密かにReactElementに追加されているので気づかないうちに挙動が変わることが発生します。↓の場合はそれぞれのinputのownerが異なりますし、React.addons.cloneWithPropsを使った場合もownerが変わります。
var foo = <input className="foo" />;
class Component {
  render() {
    return bar ? <input className="bar" /> : foo;
  }
}
  • ownerは実行時のstackによって決定します。↓の場合、spanのonwerは実際はBAではありません。これはcallbackが実行されたタイミングに依存するからです。
class A {
  render() {
    return <B renderer={text => <span>{text}</span>} />;
  }
}
class B {
  render() {
    return this.props.renderer('foo');
  }
}
  • また、JSXが書いているscope内にReactが必要なのは、Reactが現在のownerを保持していてJSXの変換がそれに依存しているからという意外なところに影響があったりもします。

それに対する提案

  • ownerベースのcontextの代わりにparentベースのcontextの導入を考えているのでそれを使うことです。ほとんどのケースはparentベースのcontextでも問題ないです。
  • ownerベースのcontextが必要になる場合はほとんどないはずだしコードを見直すべきです。

未解決

  • refはまだownerベースのままで、これについてはまだ完全に解決出来ていません。
    • v0.13ではcallbackでもrefが定義出来るようなりましたがこれまでの宣言的な定義方法も残されています。宣言的な定義方法に代わる何かいい方法がない限りこのAPIは廃止されません。

{key: element}(Keyed Object)の形式でchildに渡すとwarningが出るようになりました

v0.12では{key: element}の形式でkeyが指定したらelementを渡すことが出来ましたが、これはあまり使われてないし問題となる場合があるので使うべきではないのでwarningが出るようになりました。

<div>{ {a: <span />, b: <span />} }</div>

問題点

  • 列挙される順番はkeyに数値を指定した場合を除いては仕様として定義されてないので実装次第になってしまいます。
  • 一般的にobjectをmapとして扱うことは型システムやVMの最適化やコンパイラーにとって好ましくないし、さらにセキュリティ上のリスクもあって↓のような場合にもしitem.title === '__proto__' を指定されたら....
var children = {};
items.forEach(item => children[item.title] = <span />);
return <div>{children}</div>;

それに対する解決

  • ほとんどの場合、keyを設定したReactElementの配列にすれば問題ないはずです。
var children = items.map(item => <span key={item.title} />);
<div>{children}</div>
  • this.props.childrenを使った場合など、keyを指定することが出来ない場合もあるかもしれません。その場合はv0.13で追加されたReact.addons.createFragmentを使うことでKeyed ObjectからReactElementを作成することが出来ます。
    • 注意として、これはまだrenderの戻り値として直接渡せるものではないので
      などでラップしてあげる必要があります。
<div>{React.addons.createFragment({ a: <div />, b: this.props.children })}</div>

React.cloneElementが追加されました

これはこれまでReact.addons.cloneWithPropsと似たAPIです。 異なる点としては、styleclassNameのmergeが行われなかったりrefが保持される点があります。 cloneWithPropsを使ってchildrenを複製した時にrefが保持されなくて問題となるという報告が多くあったのでこのAPIではrefを保持するようになりました。 cloneElement時にrefを指定すると上書きされます。

var newChildren = React.Children.map(this.props.children, function(child) {
  return React.cloneElement(child, { foo: true })
});

このAPIはv0.13でPropがimmutableなものとして扱われるようになったことで、Propを変更するためにelementをcloneする機会が増えたため必要となりました。 React.addons.cloneWithPropsはそのうちdeprecateになりますが今回のタイミングではなりません。

React.addons.classSetがdeprecatedになりました

必要な場合はclassnamesなどを使用してください。

jsxコマンドで--targetoptionとしてECMAScript versionを指定出来るようになりました。 (Breaking Change)

es5がデフォルトです。 es3はこれまでの挙動ですが追加で予約語を安全に扱うようになりました(eg this.staticthis['static']にIE8での互換性のために変換されます)。

jsxコマンドでES6 syntaxで変換した際にclassメソッドがdefaultではenumerableではなくなりました

Object.definePropertyを使用しているため、IE8などをサポートしたい場合は--target es3optionを渡す必要があります。

  • Original
class Hello extends React.Component {
  foo() {
    console.log("foo");
  }
  render() {
    return <div>hello</div>;
  }
}
Hello.static = {
  bar() {
    console.log("bar");
  }
};
  • ES5
var ____Class0=React.Component;for(var ____Class0____Key in ____Class0){if(____Class0.hasOwnProperty(____Class0____Key)){Hello[____Class0____Key]=____Class0[____Class0____Key];}}var ____SuperProtoOf____Class0=____Class0===null?null:____Class0.prototype;Hello.prototype=Object.create(____SuperProtoOf____Class0);Hello.prototype.constructor=Hello;Hello.__superConstructor__=____Class0;function Hello(){"use strict";if(____Class0!==null){____Class0.apply(this,arguments);}}
  Object.defineProperty(Hello.prototype,"foo",{writable:true,configurable:true,value:function() {"use strict";
    console.log("foo");
  }});
  Object.defineProperty(Hello.prototype,"render",{writable:true,configurable:true,value:function() {"use strict";
    return React.createElement("div", null, "hello");
  }});

Hello.static = {
  bar:function() {
    console.log("bar");
  }
};
  • ES3
var ____Class0=React.Component;for(var ____Class0____Key in ____Class0){if(____Class0.hasOwnProperty(____Class0____Key)){Hello[____Class0____Key]=____Class0[____Class0____Key];}}var ____SuperProtoOf____Class0=____Class0===null?null:____Class0.prototype;Hello.prototype=Object.create(____SuperProtoOf____Class0);Hello.prototype.constructor=Hello;Hello.__superConstructor__=____Class0;function Hello(){"use strict";if(____Class0!==null){____Class0.apply(this,arguments);}}
  Hello.prototype.foo=function() {"use strict";
    console.log("foo");
  };
  Hello.prototype.render=function() {"use strict";
    return React.createElement("div", null, "hello");
  };

Hello["static"] = {
  bar:function() {
    console.log("bar");
  }
};

JSXによる変換でharmony optionを有効にすることでspread operatorを使えるようになりました

JSXの中ではこれまでもspread attributesとしてサポートしていましたが、JSのコード内でも使えるようになりました。

var [a, b, ...other] = [1,2,3,4,5];

JSXのparseに変更があります (Breaking Change)

elementの内側に> or } を使った時に以前は文字列として扱われましたがparseエラーになるようになりました。

render() {
  return <div>} or ></div>; // parse error!
}

v0.14に向けて

今回の変更を踏まえてReact v0.14では静的な要素においていくつかの最適化が可能になります。 これらの最適化は以前はtemplate-baseなフレームワークでのみ可能でしたが、ReactでもJSXとReact.createElement/Factoryのどちらでも可能になります。

詳細は下記のissueにあります。 まだ議論もされてないので変わる可能性は大きいと思いますが。

Reuse Constant Value Types

これは静的なelementに変更できないものとして扱うことでdiffのコストを減らすというものです。

例えばこんな感じにするとか

function render() {
   return <div className="foo" />;
}var foo = <div className="foo" />;
function render() {
   return foo;
}

Tagging ReactElements

これはReactElementにtag付けをしてそれを使ってdiffアルゴリズムを最適化するというもののようです。

Inline ReactElements

これはproductionビルドのときに、React.createElementではなくてinline objectに変換することでReact.createElementのコストを削減するというものです。

こんな感じ

<div className="foo">{bar}<Baz key="baz" /></div>{ type: 'div', props: { className: 'foo', children:
  [ bar, { type: Baz, props: { }, key: 'baz', ref: null } ]
}, key: null, ref: null }

こうするとReact.createElementの時に行っているPropTypesやkeyに対するvalidationが出来ないので、developmentビルドの時には適用しないことを想定しているようです。


というわけで、React v0.13をダラダラと書いてみました。