1.準備
簡単なデザインと、JSONデータを準備する。
設計書もよいが、テスト駆動型でかつアジャイルで開発するほうが、よりよいものができる
2.UI をコンポーネントの階層構造にする
モックを形作っている各コンポーネント(構成要素)を四角で囲んで、それぞれに名前をつけていくことです。
JSON のデータモデルをユーザに向けて表示することはよくありますので、モデルを正しく構築されていれば、UI(つまりコンポーネントの構造)にもうまくマッピングされるということが分かるでしょう。これは、UI とデータモデルが同じ 情報の構造 を持つ傾向があるためです。UI を分割して、それぞれのコンポーネントがデータモデルの厳密に一部分だけを表現するよう、落とし込みましょう。
5 種類のコンポーネントがこのアプリの中にあることが見て取れます。
- FilterableProductTable(オレンジ色): このサンプル全体を含む
- SearchBar(青色): すべてのユーザ入力を受け付ける
- ProductTable(緑色): ユーザ入力に基づくデータの集合を表示・フィルタする
- ProductCategoryRow(水色): カテゴリを見出しとして表示する
- ProductRow(赤色): 各商品を 1 行で表示する
ProductTable を見てみると、表のヘッダ(「Name」や「Price」のラベルを含む)が単独のコンポーネントになっていないことがわかります。
これは好みの問題で、コンポーネントにするかしないかは両論あります。今回の例でいえば、ヘッダを ProductTable の一部にしたのは、データの集合を描画するという ProductTable の責務の一環として適切だったからです。
しかしながら、将来ヘッダーが肥大化して複雑になった場合(例えばソート機能を追加した場合など)は、ProductTableHeader のようなコンポーネントにするのが適切になるでしょう。
さて、モック内にコンポーネントを特定できましたので、階層構造に並べてみましょう。モックで他のコンポーネントの中にあるコンポーネントを、階層構造でも子要素として配置すればいいのです。次のようになります。
- FilterableProductTable
- SearchBar
- ProductTable
- ProductCategoryRow
- ProductRow
3.Reactで静的なバージョンを作成する
データモデルを描画するだけの機能を持った静的なバージョンのアプリを作る際には、他のコンポーネントを再利用しつつそれらに props を通じてデータを渡す形で、自分のコンポーネントを組み上げます。
props は親から子へとデータを渡すための手段です。
state はユーザ操作や時間経過などで動的に変化するデータを扱うために確保されている機能です。今回のアプリは静的なバージョンなので、state は必要ありません。
コンポーネントはトップダウンで作っても、ボトムアップで作っても問題ありません。
つまり、高い階層にあるコンポーネント(例えば FilterableProductTable)から作り始めても、低い階層にあるコンポーネント(ProductRow など)から作り始めても、どちらでもいいのです。シンプルなアプリでは通常トップダウンで作った方が楽ですが、大きなプロジェクトでは開発をしながらテストを書き、ボトムアップで進める方がより簡単です。
ここまでのステップを終えると、データモデルを描画する再利用可能なコンポーネントのライブラリが手に入ります。このアプリは静的なバージョンなので、コンポーネントは render() メソッドだけを持つことになります。
階層構造の中で最上位のコンポーネント(FilterableProductTable)が、データモデルを props として受け取ることになるでしょう。元となるデータモデルを更新して再度 ReactDOM.render() を呼び出すと、UI が更新されることになります。このやり方なら、複雑なことをしていないので、UI がどのように更新されて、どこを変更すればよいか、理解できることでしょう。React の単方向データフロー(あるいは単方向バインディング)により、すべてがモジュール化された高速な状態で保たれます。
4.UI 状態を表現する state を決定
UI をインタラクティブなものにするためには元となっているデータモデルを更新できる必要があります。これは React なら state を使うことで実現できます。
適切に開発を進めていくにあたり、そのアプリに求められている更新可能な状態の最小構成を、最初に考えておいたほうがよいでしょう。
ここで重要なのは、DRY (don’t repeat yourself)の原則です。アプリケーションが必要としている最小限の状態を把握しておき、他に必要なものが出てきたら、そのとき計算すればよいのです。
例えば、TODO リストを作る場合、TODO の各項目を配列で保持するだけにし、個数のカウント用に別の state 変数を持たないようにします。その代わりに、TODO の項目数を表示したいのであれば、配列の length を使えばよいのです。
今回は、次のようなデータがあるとする
- 元となる商品のリスト
- ユーザが入力した検索文字列
- チェックボックスの値
- フィルタ済みの商品のリスト
それぞれについて見ていき、どれが state になりうるのかを考えてみます。
各データについて、考えましょう。
- 親から props を通じて与えられたデータでしょうか?
もしそうなら、それは state ではありません - 時間経過で変化しないままでいるデータでしょうか?
もしそうなら、それは state ではありません - コンポーネント内にある他の props や state を使って算出可能なデータでしょうか? もしそうなら、それは state ではありません
- 元となる商品のリストは props から渡されるので、これは state ではありません。
- 検索文字列とチェックボックスは時間の経過の中で変化し、また、算出することもできないため、state だと思われます。
- 最後に、フィルタ済みの商品のリストは state ではありません。何故ならば、元となる商品のリストと検索文字列とチェックボックスの値を組み合わせることで、フィルタ済みの商品のリストを算出することが可能だからです。
というわけで、state と呼べるのは次の 2 つです。
- ユーザが入力した検索文字列
- チェックボックスの値
5.state をどこに配置すべきか
アプリの各 state について、次の各項目を確認していきます。
- その state を使って表示を行う、すべてのコンポーネントを確認する
- 共通の親コンポーネントを見つける(その階層構造の中で、ある state を必要としているすべてのコンポーネントの上位にある単一のコンポーネントのことです)
- 共通の親コンポーネントか、その階層構造でさらに上位の別のコンポーネントが state を持っているべきである
- もし state を持つにふさわしいコンポーネントを見つけられなかった場合は、state を保持するためだけの新しいコンポーネントを作り、階層構造の中ですでに見つけておいた共通の親コンポーネントの上に配置する
それでは、例に適用してみましょう。
- ProductTable は商品リストをフィルタする必要があり、SearchBar は検索文字列とチェック状態を表示する必要がある
- 共通の親コンポーネントは FilterableProductTable である
- 概念的にも、検索文字列とチェック状態が FilterableProductTable に配置されることは妥当である
state を FilterableProductTable の中に配置することが決まりました。では早速、インスタンス変数として this.state = {filterText: '', inStockOnly: false} を FilterableProductTable の constructor に追加して、初期状態をアプリに反映しましょう。その次は、filterText と inStockOnly を ProductTable と SearchBar に props として渡します。最後に、これらの props を使って ProductTable のフィルタ処理を行い、SearchBar のフォームにも値を埋めます。
6.逆方向のデータフローを追加
現在のバージョンのサンプルで文字を打ち込んだり、チェックボックスを切り替えてみると、React がその入力を無視することがわかります。これは意図的な挙動で、input の value props が、常に FilterableProductTable から渡された state と同じ値になるようにセットしてあるのです。
それでは、どんな挙動になってほしいのかを考えてみましょう。ユーザがフォームを変更するたびに、ユーザ入力を反映するように state を更新したいですね。コンポーネントの state を更新できるのは自分自身だけであるべきなので、FilterableProductTable は SearchBar にコールバックを渡しておいて、state を更新したいときに実行してもらうようにします。入力のたびに呼び出される onChange イベントを利用するとよいでしょう。このコールバックを実行された FilterableProductTable は、setState() を呼び出し、その結果としてアプリが更新されます。