React.js v0.13 changes
2015/03/05 @koba04
React.js v0.13のRC2がリリースされたのでまとめてみます。
- http://facebook.github.io/react/blog/2015/02/24/react-v0.13-rc1.html
- http://facebook.github.io/react/blog/2015/03/03/react-v0.13-rc2.html
- http://facebook.github.io/react/blog/2015/02/24/streamlining-react-elements.html
今回のバージョンで何か大きく変更があるというよりも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._pendingState
やthis._rootNodeID
などのprivateなプロパティが削除されました。
ES6 classesによるReactComponentの作成がサポートされました
これについては↓に書きましたが、ES6 classesによって作成されたcomponentにはcreateClass
にはあるgetDOMNode
、setProps
、replaceState
が含まれていなかったり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は実際はB
でA
ではありません。これは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です。 異なる点としては、style
やclassName
の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コマンドで
--target
optionとしてECMAScript versionを指定出来るようになりました。 (Breaking Change)es5
がデフォルトです。es3
はこれまでの挙動ですが追加で予約語を安全に扱うようになりました(egthis.static
はthis['static']
にIE8での互換性のために変換されます)。jsxコマンドでES6 syntaxで変換した際にclassメソッドがdefaultではenumerableではなくなりました
Object.defineProperty
を使用しているため、IE8などをサポートしたい場合は--target es3
optionを渡す必要があります。- 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をダラダラと書いてみました。
- 注意として、これはまだrenderの戻り値として直接渡せるものではないので