Reactに入門
流行に乗っかって、Reactを学び始めている。
A JavaScript library for building user interfaces | React
といってもまだほんの少し。 概念を分かりやすく説明してくれているスライドが最近いくつか出ているのでそれを読み、 公式のチュートリアル & Web+DB vol.86 のReact解説記事を見ながら写経したくらい。
あと、一人React.js Advent Calendar 2014 - Qiitaをちょこちょこ見てる。 (分かりやすい記事を一人で1ヶ月分書いてらっしゃってほんとにすごいです。。)
会社のプロダクトのjQueryゴリゴリのコードの管理に限界を感じていて、 Vueなどのフレームワークも触ったりしたのだけど、より面白いアプローチで問題解決するライブラリが出てきたなという印象。 当然ながらコードの見通し・管理という面では大きなメリットがあるので、 もう少し触って、うまくやれそうならリプレイスしていきたいなーなんて考えている。
というわけで、Web+DBの記事の備忘録というか頭の整理目的でメモ。 naoya_itoさんの解説がいつもの如くすごく分かりやすい。
ReactはViewのみ提供する
AngularなどのMVCフレームワークとは異なり、ReactはViewのみを提供する。フルスタックではない。 Reactのコンポーネント外のデータモデルで状態管理をしたくなったら?というところで、Facebookの提唱するFluxアーキテクチャが登場する、と。
Flux | Application Architecture for Building User Interfaces
(まだFluxには全く手を出してない😓)
サンプルコード1(Markdownプレビュー)
以下の様なReactのコードで、Markdownを入力するとリアルタイムでプレビューされるやつができる。
<!-- markdown.html --> <html> <head> <title>Markdown by React</title> </head> <body> <div id="app-container"></div> <script src="build/markdown.js"></script> </body> </html>
// src/markdown.jsx var React = require('react'); var marked = require('marked'); var App = React.createClass({ getInitialState: function() { return { markdown: "" }; }, updateMarkdown: function(e) { this.setState({ markdown: e.target.value }); }, render: function() { var html = marked(this.state.markdown); return ( <div> <textarea onChange={this.updateMarkdown}></textarea> <div dangerouslySetInnerHTML={{__html: html}}></div> </div> ); } }); React.render( <App />, document.getElementById("app-container") );
画面(markdown.html)
スクショ。
(上部のテキストボックスにMarkdownを書いてくと、下部にリアルタイムでプレビューが出る)
コンポーネント
Reactでは、UIの要素をコンポーネントに分割して、それを組み合わせることでUIを構築する。
コンポーネントは、React.createClass
にrender
メソッドを持たせて作成する。
上の例では、Appという名前でたった1つのコンポーネントを作成している
コンポーネントのレンダリング
React.render
で行う。
サンプルコードでは、指定したDOM要素(#app-container)にAppコンポーネントをレンダリングしている。
このように、コンポーネントはHTMLのタグっぽく書くと参照され、参照されるとrender
メソッドが呼ばれる。
Appコンポーネントのrender
では、textarea
とdiv
の2つの要素が入ったdiv
をreturn
する。
画面のスクショの通り、この2つの要素が、
Markdownを入力するテキストボックスとそのプレビューを表示するエリアとなる。
JSX
JavaScript syntax extension。 JSの中に直接HTMLタグを記述してる箇所がJSXの記述。 ちょっと気持ち悪いと最初だけ思ったけど、すぐ慣れた。 このJSX形式のファイルを通常のJSに変換してHTMLファイルで読み込む。
JSXのコードは以下のように変換される。
変換後のJSの中にReact.createElement
というのがあるが、これは名前の通り(Reactの)要素を作る。
このJSコードをJSXでは直感的に書けるようにしてくれている。
// jsx
<div>
<h1>Hello, React</h1>
</div>
// js React.createElement("div", null, React.createElement("h1", null, "Hello, React") )
ちなみにDeNAが出してるやつかと勘違いしてた、全く別物だった
JSX | XML-like syntax extension to ECMAScript
State
Reactにおけるコンポーネントは、State と Props という2つの要素(インターフェース)から成り立っているのだが、まずStateについて。
Stateはその名の通り「状態」を表し、コンポーネントの状態管理を行う。 サンプルコードだと、テキストボックスに入力される文字列が状態を持った値(State)。 これをReactで管理する。
Appコンポーネントの中でgetInitialState()
というのがあるが、
これが状態の初期値を返すためのコールバック。
Stateを表すmarkdown
という値が空であることを指定している。
また、同じくAppコンポーネントのupdateMarkdown
プロパティの中にthis.setState()
という記述がある。
このsetState()
で状態の更新を行う。
サンプルコードでは、textarea
のonChange
属性にこれをセット。
イベントハンドラを指定することでテキストボックス内の変更イベントを監視し、
変更の度にupdateMarkdown
の中で指定した関数が呼び出され、状態が更新される。
つまり、テキストボックスへの入力値がmarkdown
に渡される。
そしてここがReactの特徴の一つ。
コンポーネントの状態に変更があると、コンポーネントの再描画が自動で行われる。
先にも述べたとおり、Appコンポーネントは、
入力用テキストボックスとそのプレビュー表示エリアを含んだdiv
を描画するわけだが、
このレンダリングがテキスト入力(コンポーネントの状態変更)の度に実行される。
コンポーネントの状態更新だけを行えばよく、自分でDOMの操作を行う必要はない。
Virtual DOM
状態更新の度に全て描画するとか効率悪くね?という話になるわけだが、ここで出てくるのがVirtual DOM。 Reactは描画の際に生のDOMを直接操作するのではなく、Virtual DOMというメモリ上の仮想構造体を操作する。 詳しく書かないが、ReactはこのVirtual DOMをまるっと更新して変更前との差分を計算する。 差分を埋める操作だけが本物のDOMに対して行われるため、パフォーマンスがすごく悪いということはない。
コンポーネントの状態を更新すれば、Reactが良きに計らってDOMを更新してくれる。
「このイベントが起きたら、あそこのli
の後にもう1個li
をappendして...」みたいなjQuery的発想から脱却できる。
変更後の姿を指定してDOMをまるっと更新するように書けば、Virtual DOMの力で差分だけが反映される。
いわゆる「富豪的」な開発スタイルになる。
参考: VirtualDom - なぜ仮想DOMという概念が俺達の魂を震えさせるのか - Qiita
JSXは、Virtual DOM用の要素を生成する記述(React.createElement
)だったというわけか。
サンプルコード2(Markdownプレビュー)
さっきのMarkdownプレビューのコードを少し変更する。
// src/markdown.jsx var React = require('react'); var marked = require('marked'); var App = React.createClass({ getInitialState: function() { return { markdown: "" }; }, updateMarkdown: function(markdown) { this.setState({ markdown: markdown }); }, render: function() { return ( <div> <TextInput onChange={this.updateMarkdown} /> <Markdown markdown={this.state.markdown} /> </div> ); } }); var TextInput = React.createClass({ propTypes: { onChange: React.PropTypes.func.isRequired }, _onChange: function(e) { this.props.onChange(e.target.value); }, render: function() { return ( <textarea onChange={this._onChange}></textarea> ); } }); var Markdown = React.createClass({ propTypes: { markdown: React.PropTypes.string.isRequired }, render: function() { var html = marked(this.props.markdown); return ( <div dangerouslySetInnerHTML={{__html: html}}></div> ); } }); React.render( <App />, document.getElementById("app-container") );
コンポーネントの組み合わせでUIを構築
Reactでは、UIの要素をコンポーネントに分割して、それを組み合わせることでUIを構築する。 最初のサンプルではコンポーネントは1つだけだったが、App, TextInput, Markdownの3つのコンポーネントを作成した。 「役割」を考えながら分割していけば良いのかな?
TextInputがメッセージを入力するコンポーネント、Markdownが入力値をプレビューするコンポーネント。
Appコンポーネントではこれらをrender
しており、2つを束ねるような存在になっている。
このようにコンポーネントには親子関係を持たせることができる。 今回のケースでは、Appという親にTextInput, Markdownという子がぶら下がっている、ということになる。
Props
Reactにおける、Stateと並ぶもう一つの要素がProps。 Propsは名前の通り「属性」を表し、コンポーネントの属性(プロパティ)を扱う仕組み。
Propsを通して親 => 子に値が渡される。 サンプルコードを見てみると、例えばAppコンポーネントの中に以下のような記述がある。
<Markdown markdown={this.state.markdown} />
これは、子のMarkdownコンポーネントのmarkdown
というPropsに、
親のAppコンポーネントのmarkdown
というStateが渡されていることを示す。
Markdownコンポーネントの中でthis.props.markdown
という記述があるが、
こうやって書くことでAppコンポーネントから渡された値を参照している。
Propsはimmutableに
原則として、Propsを通して値を受け取った子コンポーネントにおいては、値を更新しない(immutableにしておく)。 子は値を使うのみで、状態の管理は親に任せる。
こうしてデータの扱いを親に集約することで、子側では状態管理を考えなくて済む。 ビューの状態遷移の管理をする際の見通しが良くなる。
子 => 親への状態更新処理移譲
上でも書いたように、子は親のStateを直接更新することはできない。
子コンポーネントがイベントを受け取って状態を更新したい、となったらどうするのか。
こういったとき、親はPropsを使って子にsetState()
を含む関数を渡し、
子はその関数を呼び出し引数として値を渡す。
関数は親側で呼び出されるので、Stateの更新処理は親コンポーネントの中に閉じ込めることが出来る。
サンプルコードで説明するとこんな感じだろうか。
- 親のAppコンポーネントは子のTextInputコンポーネントに
onChange
というPropsを渡す - その値は
updateMarkdown
というプロパティ(呼び出すとsetState
を実行)である - TextInputコンポーネントは、
render
するtextinput
タグのonChange
属性に_onChange
というプロパティをセット - テキストが入力されたらイベントが発火し、
_onChange
で指定した関数が実行される - その関数の中では、親から受け取った
onChange
(=setState
を実行する関数)を呼び出し、引数としてe.target.value
(テキストボックスへの入力値)を渡す - Appコンポーネントで実際に
setState
が実行され、子が渡した引数がmarkdown
の値になってStateが更新される。
サンプルコードの流れ
いまさら。
- 親であるAppコンポーネントは、TextInput, Markdownという2つのコンポーネントを
div
タグの中に入れてレンダリングする - TextInputコンポーネントは、テキストボックスをレンダリングする
- App <=> TextInput のデータの流れは上で書いたとおり
- テキストボックスへの入力によって、AppコンポーネントのState(
markdown
)が更新される
- Markdownコンポーネントは、親のStateをPropsとして受け取っている
- 受け取った
markdodwn
(文字列)に対してパースを行い(by marked.js)、HTMLとしてdiv
タグ内に展開してレンダリングする
- 受け取った
- Stateを更新する毎にレンダリングは行われるので、リアルタイムでMarkdownプレビューを行うことが可能
propTypes
propTypes
というプロパティで、親から渡ってくるPropsの型の検証を行える。
上記の例でいえば、TextInput内のonChange
の値は関数、
Markdown内のmarkdown
の値は文字列。
Propsとしてどういうインターフェースを公開しているのか明示できるので、書いといたほうが良さそう。
Browserifyとか
特に触れてないけど、Browserify + reactifyを使ってJSXをビルドしている(src/markdown.jsx
=> build/markdown.js
)。
Browserifyを使うの初めてだった。
Reactの説明見てても、Node.js(npm
)周りの知識を普通に知ってる前提で説明書いてあったりするし、
普段Rails使ってるとはいえ、こっち側もそろそろ知ってないとマズイなと思った。
おわり
というわけで長くなったけどメモでした。 (基礎的な)概念は掴めたので、あとは実際に書いていくしかないだろうな。 会社のハッカソンで作った小さいRailsアプリがあるので、まずはそのあたりにReactを組み込んでみようかと思う。
Fluxを学ぶべきタイミングはいつなんだろう。 やっぱ作ってるうちにきつくなってきて、ああいうアーキテクチャを使いたくなるものなのかな??