Edited at

ReactのComponents, Elements, Instances

More than 1 year has passed since last update.

https://facebook.github.io/react/blog/2015/12/18/react-components-elements-and-instances.html

概念を理解するためにこのページをサクッと和訳してみました。

※折を見て読みやすい日本語に書き換えます。

Reactビギナーはcomponents, instances, elementsの違いに混乱する。なぜ、画面に描画される「何か」に言及する用語が3種類もあるのでしょうか?


Managing the Instances(インスタンスの管理)

もしあなたが初めてReactに触れるなら、多分以前にcomponent classesとinstancesだけは触ったことあるだろう。例えば、classを作成してButtonコンポーネントを宣言したことがあるだろう。アプリケーションが動いているときに画面上に表示されたこのコンポーネントの幾つかのインスタンスがあり、各インスタンスがlocal stateを持っている。これは伝統的なオブジェクト指向のUIプログラミングである。なぜ、elementsを導入したのか?

下記の伝統的なUIモデルにおいて、子コンポーネントのインスタンスを作ったり壊したりするのはあなたに任せられている。FormコンポーネントがButtonコンポーネントをレンダリングしたい場合、そのインスタンスを作成し、手動で最新の状態に保たなければならない。

class Form extends TraditionalObjectOrientedView {

render() {
// Read some data passed to the view
const { isSubmitted, buttonText } = this.attrs;

if (!isSubmitted && !this.button) {
// Form is not yet submitted. Create the button!
this.button = new Button({
children: buttonText,
color: 'blue'
});
this.el.appendChild(this.button.el);
}

if (this.button) {
// The button is visible. Update its text!
this.button.attrs.children = buttonText;
this.button.render();
}

if (isSubmitted && this.button) {
// Form was submitted. Destroy the button!
this.el.removeChild(this.button.el);
this.button.destroy();
}

if (isSubmitted && !this.message) {
// Form was submitted. Show the success message!
this.message = new Message({ text: 'Success!' });
this.el.appendChild(this.message.el);
}
}
}

これは擬似コードであるが、多かれ少なかれBackboneのようなライブラリを使ってオブジェクト指向の方法で一貫して振る舞う複合的なUIコードを書いたことがあるだろう。

各コンポーネントのインスタンスはそれ自身のDOMノードと子コンポーネントのインスタンスへの参照を保持する必要があり、然るべきタイイングでそれらをcreate, update, destroyしなけらばならない。コードの行数はコンポーネントが持ちうるstateの状態に応じて指数関数的に増えてしまう。親は子コンポーネントへの直接アクセスをするため、将来的に切り離すことが難しくなる。

では、Reactは何が違うのか?


Elements Describe the Tree

Reactではelementsが助けに来てくれる。あるelementはコンポーネントのインスタンスまたはDOMノードを説明するプレーンなオブジェクトで、プロパティを持つ。elementはコンポーネントタイプ(例えば、Button)、プロパティ(例えばcolor)、内側に含んでいる子エレメントに関する情報のみを持っている。

elementは実際にはインスタンスではない。むしろ、「あなたが画面上で見たいもの」をReactnい伝える方法といったほうが良い。それは2つのフィールド=type: (string | ReactClass)props: Objectを持つImmutableな記述のオブジェクトである。


DOM Elements

elementsのtypeがstringのとき、タグ名を伴ってDOMノードを表現し、propsがその属性と一致する。これはReactがレンダリングするものの例である。

{

type: 'button',
props: {
className: 'button button-blue',
children: {
type: 'b',
props: {
children: 'OK!'
}
}
}
}

これはプレーンオブジェクトとしてHTMLで表現されたelementである。

<button class='button button-blue'>

<b>
OK!
</b>
</button>

どのようにelementがネストされるか注意しよう。しきたりによって、element treeを生成したいとき、それらが含むelementのchildren propとして1つまたは複数の子elementsを明示的に記述する。

大切なことは子と親のコンポーネントの両方が実際にはインスタンスでなく単なる説明(description)ということだ。何の影響もなくそれらを作ったり捨てたりできる。

React elementsは簡単に横断(再利用?)することができ、parseする必要がない。もちろんそれらは実際のDOM elementに比べて断然軽量である。ただのobjectだからね!


Component Elements

しかしながら、elementのtypeは関数やReactコンポーネントにもなり得る。

{

type: Button,
props: {
color: 'blue',
children: 'OK!'
}
}

これはReactのコアとなる概念である。

DOMノードを説明したelementと同じように、コンポーネントを説明したelementもまたelementである。それらは互いにネストしたりミックスしたりできる。

この特徴によって、特定のcolorプロパティの値を持ったButtonとして、DangerButtonコンポーネントを定義することができる。Button<button>, <div>などのDOMをレンダリングすることは気にしなくてよい。

const DangerButton = ({ children }) => ({

type: Button,
props: {
color: 'red',
children: children
}
});

1つのelement treeの中で、DOMやcomponent elementsを組み合わせたりマッチさせたりすることもできる。

const DeleteAccount = () => ({

type: 'div',
props: {
children: [{
type: 'p',
props: {
children: 'Are you sure?'
}
}, {
type: DangerButton,
props: {
children: 'Yep'
}
}, {
type: Button,
props: {
color: 'blue',
children: 'Cancel'
}
}]
});

JSXが好きならそれでもOK。

const DeleteAccount = () => (

<div>
<p>Are you sure?</p>
<DangerButton>Yep</DangerButton>
<Button color='blue'>Cancel</Button>
</div>
);

この組み合わせとマッチングは互いのコンポーネントから切り離す助けになる。下記の構成を通して、排他的にis-a, has-a関係をどちらも表現することができる。


  • Button is a DOM <button> with specific properties.

  • DangerButton is a Button with specific properties.

  • DeleteAccount contains a Button and a DangerButton inside a <div>.


Components Encapsulate Element Tree

functionまたはclassのtypeを持elementをReactが認識したときに、渡されたpropsと一致するレンダリングされようとしているelementは何なのか、そのコンポーネントに問い合わせることが知られている。

下記のelementをReactが認識したとき

{

type: Button,
props: {
color: 'blue',
children: 'OK!'
}
}

ReactはButtonに対して、何がレンダリングされるのかを問い合わせる。Buttonは下記のelementを返す。

{

type: 'button',
props: {
className: 'button button-blue',
children: {
type: 'b',
props: {
children: 'OK!'
}
}
}
}

Reactはページ上の全てのコンポーネントに対して基となるDOMタグelementに到達するまでにこのプロセスを繰り返す。

Reactは世界中のあらゆることが理解できるまで、全ての"X is Y"に対して"what is Y"を尋ねている子供のようなものだ。

先に出てきたFormの例を思い出してほしい。Reactでは下記のように記述することができる。

const Form = ({ isSubmitted, buttonText }) => {

if (isSubmitted) {
// Form submitted! Return a message element.
return {
type: Message,
props: {
text: 'Success!'
}
};
}

// Form is still visible! Return a button element.
return {
type: Button,
props: {
children: buttonText,
color: 'blue'
}
};
};

これだけ!

Reactコンポーネントにおいて、propsは入力でelement treeは出力である。

返されるelement treeはelementとdescribing DOM nodesの両方を含むことができ、elementsは他のコンポーネント説明する。これによって内部のDOM構造に依存せずに独立したUIパーツを組み合わせることができる。

我々はReactにインスタンスのcreate, update, destroyをさせる。コンポーネントから返されるelementとともに、Reactがインスタンスを管理することを説明する。


Components Can Be Classes or Function

上記のコードにあるForm, Message, ButtonはReactコンポーネントである。それらは上記にあるように関数として書くこともできるしReact.Componentを継承するクラスとしても書くことができる。これら3種類の宣言方法はほとんど等価である。

// 1) As a function of props

const Button = ({ children, color }) => ({
type: 'button',
props: {
className: 'button button-' + color,
children: {
type: 'b',
props: {
children: children
}
}
}
});

// 2) Using the React.createClass() factory
const Button = React.createClass({
render() {
const { children, color } = this.props;
return {
type: 'button',
props: {
className: 'button button-' + color,
children: {
type: 'b',
props: {
children: children
}
}
}
};
}
});

// 3) As an ES6 class descending from React.Component
class Button extends React.Component {
render() {
const { children, color } = this.props;
return {
type: 'button',
props: {
className: 'button button-' + color,
children: {
type: 'b',
props: {
children: children
}
}
}
};
}
}

コンポーネントがクラスとして定義されたとき、関数コンポーネントよりも幾分強力である。クラスで定義されたコンポーネントは複数のローカルstateを保持することができ、DOM nodeが生成/破棄された際にカスタムロジックを動作させることができる。

関数コンポーネントは強力ではないが、よりシンプルでrender()のみを持つクラスコンポーネントと同じように振る舞う。classコンポーネントのみで実現できる機能が必要でない限り、関数コンポーネントを使うことをオススメする。

しかしながら、関数でもクラスでも基本的にそれらは全てReactコンポーネントである。入力としてpropsを受け取り、出力としてelementsを返すことは同じである。


Top-Down Reconciliation

ReactDOM.render({

type: Form,
props: {
isSubmitted: false,
buttonText: 'OK!'
}
}, document.getElementById('root'));

このようにコールしたときにReactはこれらのpropsを渡すとFormコンポーネントにどんなelement treeが返されるかを問い合わせる。

あなたのcomponent treeをよりシンプルでプリミティブなものへと理解を洗練していく。

// React: You told me this...

{
type: Form,
props: {
isSubmitted: false,
buttonText: 'OK!'
}
}

// React: ...And Form told me this...
{
type: Button,
props: {
children: 'OK!',
color: 'blue'
}
}

// React: ...and Button told me this! I guess I'm done.
{
type: 'button',
props: {
className: 'button button-blue',
children: {
type: 'b',
props: {
children: 'OK!'
}
}
}
}

これは、あなたがReactDOM.render()setState()をコールしたときに開始されるreconciliationをReactが呼び出すプロセスの一例である。

reconciliatioのが終わるまでに、ReactはDOM treeの結果を知り、DOMノードを更新するために必要な変更の最小のセットを適用するreact-domreact-native(React Nativeの場合はプラットフォームで明示されたビュー)をレンダリングする。

この緩やかな洗練のプロセスもまた、Reactアプリケーションが容易に最適化できる理由である。もしあなたのコンポーネントtreeの一部が、Reactが効率的にアクセスするには大きすぎになってしまった場合は、関係するpropsが変更されていないかどうか、特定のtreeの一部の差分をとってrefiningすることをスキップすることができる。

これは、propsがimmutableに変更されているときに計算がとても速いので、Reactとimmutableな変更は非常に効果的に働き、最小の努力で大きく最適化することができる。

あなたはこのブログエントリーがお伝えするcomponentsとelements、多くないけどinstancesに関する情報を受け取ることができる。実のところ、オブジェクト指向UIフレームワークと比べると、Reactにおけるinstencesはさほど重要ではない。

classとして宣言されたコンポーネントのみがインスタンスを持ち、インスタンスは直接生成することができない。というのも、Reactがあなたの代わりにそれを行うからだ。親コンポーネントのインスタンスが子コンポーネントのインスタンスにアクセスするメカニズムが存在する限り、それらは命令のアクションのみで使われ(フィールドへのフォーカスのセットのように)、一般的には避けるべきだ。

Reactは全てのclass componentについてインスタンスの生成を司るため、あなたはローカルstateとメソッドを伴ったオブジェクト指向な方法でコンポーネントを記述することができる。それ以外は、instancesはReactのプログラミングモデル無いでは重要でなく、React自身によって管理される。


Sumamry

elementはあなたがスクリーンに表示したいDOMノードまたはその他のコンポーネントを説明するプレーンなオブジェクトである。elementはそれらのpropsに含まれる他のelementを含むことができる。React elementを作ることは簡単だ。一度elementを作れば変化することがない。

コンポーネントは異なる幾つかの方法で定義することができる。コンポーネントンはrender()メソッドを持っている。代わりに、シンプルなケースの場合は関数として定義することもできる。どちらのケースでも、入力がporpsで出力が返されるelement treeである。

今ポーエントが入力としてinputを受け取ったとき、特定の親コンポーネントがそれ自身のtypeとpropsを伴ったelementを返却するからだ。

これが、人々がReactのpropsフローが一方通行(親から子)と呼ぶ理由だ。

instanceはcomponent内でthisで参照することができる。強いローカルstateとライフサイクルイベントに対して便利である。

関数コンポーネントはインスタンスを持たない。クラスコンポーネントはインスタンスを持つが、あなたは直接的にインスタンスを生成する必要はない。Reactが勝手にやってくれる。

最後に、elementを生成するために、React.createElementかJSX、またはelement factory helperを使う。コード中にプレーンのオブジェクトとしてelementを書かないように。