読者です 読者をやめる 読者になる 読者になる

JavaScript初心者だけどReactにさわってみたよ! エピソード2

いきなり前回のあらすじ

というわけで、残念ながらお仕事で使う機会は今のところないにも関わらず React に手を出してしまいました。

前回の更新から1ヶ月以上も空いてしまったけれど気にしない。

ちなみに本日の内容も相変わらず低レベルです。 Flux とか Redux とかに到達する以前の問題です。 仕事でバリバリ React を使っているようなプロフェッショナルにとっては何一つ有益な情報が書かれていないので回れ右!


React 勉強記

おそらく日本語で読める唯一の入門書『入門 React ―コンポーネントベースのWebフロントエンド開発』を買いました。しかもだいぶ前に

React は触っていてかなり楽しいけれど、独自のルールのおかげで戸惑うことがしばしばあります。

まず前提として、React には以下の掟があります。

これは今時点での僕の理解なので、もしも間違っていたらごめんなさい。

コンポーネントで発生したイベントを親側はどうやって検知すればいいの問題

React は「データの流れが親から子へ一方通行」という制約のおかげで、イベントまわりの書き方に一癖あります。

たとえば、あるコンポーネントのイベントのせいで、他のコンポーネントに影響が及ぶシチュエーションを考えてみましょう*1

常識的に考えて、必ずしもイベントの発生元が常に親コンポーネントであるとは限りません。もしも子から親にイベントの発生を通知する必要がある場合は、ちょっとした技巧が必要となります。

入門 React ―コンポーネントベースのWebフロントエンド開発』の §6.2〜6.3 から、当該問題についての記述を引用します:

 注意深い読者の方はすでにお気づきかもしれませんが、このコンポーネントは実は重要な要素を欠いています。それは何かと言うと、値が変更されたことを親コンポーネントに伝える機能が実装されていないのです。
  (中略)
 子コンポーネントが親コンポーネントと通信する最も簡単な方法は props を使用することです。親がコールバック関数を props として渡して、子が適宜それを呼び出すといった形です。

が、個人的にこれは悪手あくしゅだと思っています。

このやり方は、コンポーネントの階層が深くなると、間にいるコンポーネントたちは自分自身となんの関係もないコールバック関数をひたすら中継するようになるからです。イメージとしてはこんな感じ。

コンポーネントAコンポーネントZコーバック関数コンポーネントAのコーバック関数コンポーネントAのコーバック関数コンポーネントAのコーバック関数
上図は、コンポーネントZの変化をコンポーネントAに検知させるために、間にいるコンポーネントたちは自分にとって不必要なコールバック関数をいちいち中継する必要が生じます。これは少し気持ち悪い。

そんなこんなでもやもやしていたところ、 Qiita の「Reactを使うとなぜjQueryが要らなくなるのか」という記事を見つけました。

実はこの記事、僕が React に手を出す前(5月末頃)に一度目を通していたのですが、当時はいまひとつピンとこないままでした。

ところが今になって読み返すとかなりの良記事。結論から言うと、子コンポーネントのイベント云々問題の解もここに書いてありましたし、ついでに前回僕が載せたコーディングスタイルはもはや時代遅れであることも発覚しました。

まぁそんなわけで Hello, World からやり直しです。

いまふたたびの Hello, World

前回の書き方


今回の書き方


変わったところ

前回はReact.createClass()を用いてコンポーネントを作成していましたが(下図左)、多くの場合、アロー関数式で簡潔に書き換え可能だそうです(下図右)。ちょっと構文が新しすぎて JSFiddle のシンタックスハイライトがうまく作用してくれませんでした

これがこんなに!var Hello = React.createClass({ render: function() { var name = "Tercel"; return <div>Hello {name}.</div>; }});ReactDOM.render( <Hello />, document.getElementById('hello'));let user = { name: 'Tercel'};const Hello = props => <div>Hello {props.name}.</div>;const render = () => ReactDOM.render(<Hello name={user.name} />, document.getElementById('hello'));render();BeforeAfter

で、ここがポイントなのですが、構文をアロー関数式に切り替えたことで、コンポーネントstate内部状態を持てなくなりました

状態を表す変数はコンポーネント内部に持たせるのではなく、ひとところに集めて一元的に集中管理するのが今時の正義らしいです。

あともうひとつのポイントが、再描画用の ReactDOM.render()いつでも何度でも呼べるようrender という定数にしておきます。

メタボ化した props をすっきりさせる

ここでいよいよ本題に移りたいと思います。

前置きが長すぎてすっかりメインテーマを忘れかけていますが、「複数階層に渡るコンポーネント間でイベントをいい感じに扱う話」なのでした。

要件としては以下2点です。

  • イベントの影響を受けるすべてのコンポーネントが矛盾なく書き換わってほしい
  • そのためのコードはできるだけ簡潔に書きたい

そこで、とりあえずの題材として、テキストボックスを変更すると、挨拶文が連動するという実につまらない前回のサンプルを改めて引っ張り出してきました。動き自体は果てしなくショボいのですが、以下の特徴を兼ね備えているのでよしとします。

  • 階層間のデータのやり取りが発生する
  • イベント発生時に、直接の親子関係にないコンポーネントが影響を受ける

実際に動くサンプルがこちら。

前回のサンプル(実はすでにリファクタリング済み版)

これ、もともとは子コンポーネントの状態変化を親が監視observeできるよう、親コンポーネントが自身のコールバック関数を propsプロパティとして子コンポーネントに引き渡していました。

ところが今回、Qiita のソースを パクって 参考にして

  • シングルトンな状態変数を導入
  • 状態変数が変化したら再描画関数を実行

という新たな思想を導入することにより、親コンポーネントが子の状態変化を監視する必要そのものがなくなりました。

プログラムの内部動作も下図のように変わっています。

ルートコンポーネントテキストボックスラベルコーバック関数ルートコンポーネントのコーバック関数Stateーザによ入力操作 ルートコンポーネントの  コーバック関数が呼ばれる コーバック関数が、  内部状態を更新する 更新された情報を  子に伝達するルートコンポーネントテキストボックスラベルonChangeーザによ入力操作 変更アクション(関数)が呼ばれる 必要に応じて  子コンポーネントも  再描画される状態変数変更アクション再描画関数 状態変数が更新される ルートコンポーネントの再描画処理を行うAfterBefore

本当はリファクタリングの内容をステップバイステップで書こうかと思いましたがいい加減面倒になったのでやめました。

ちなみにさっきのサンプルのソースは既に今回の内容に合わせてアップデートしてあります。props からわずらわしいコールバック関数が消え去っているのがチャームポイント。「あぁ、そういうことか」と理解できた瞬間はうれしい。

本日のまとめ

Qiita にだいたいのことが書いてあるのでそっちを見ればよいと思う。なげやり。

あと周りに React やっている友達がいなくてさみしい。

*1:具体的には、「ボタンが押されたらテキストエリアの入力値をチェックして、内容に不備がある場合はエラーメッセージを表示する」といった仕掛けなどを思い浮かべてください。

Copyright (c) 2012 @tercel_s, @iTercel, @pi_cro_s.