blue_field

技術的なメモとか読書記録とかいろいろ(の予定

Reactに入門

流行に乗っかって、Reactを学び始めている。

f:id:yusuke_1031:20150503225826p:plain

A JavaScript library for building user interfaces | React

といってもまだほんの少し。 概念を分かりやすく説明してくれているスライドが最近いくつか出ているのでそれを読み、 公式のチュートリアル & Web+DB vol.86 のReact解説記事を見ながら写経したくらい。

WEB+DB PRESS Vol.86

WEB+DB PRESS Vol.86

あと、一人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)

スクショ。

f:id:yusuke_1031:20150503225436p:plain

(上部のテキストボックスにMarkdownを書いてくと、下部にリアルタイムでプレビューが出る)

コンポーネント

Reactでは、UIの要素をコンポーネントに分割して、それを組み合わせることでUIを構築する。 コンポーネントは、React.createClassrenderメソッドを持たせて作成する。 上の例では、Appという名前でたった1つのコンポーネントを作成している

コンポーネントレンダリング

React.renderで行う。 サンプルコードでは、指定したDOM要素(#app-container)にAppコンポーネントレンダリングしている。 このように、コンポーネントはHTMLのタグっぽく書くと参照され、参照されるとrenderメソッドが呼ばれる。

Appコンポーネントrenderでは、textareadivの2つの要素が入ったdivreturnする。 画面のスクショの通り、この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におけるコンポーネントは、StateProps という2つの要素(インターフェース)から成り立っているのだが、まずStateについて。

Stateはその名の通り「状態」を表し、コンポーネントの状態管理を行う。 サンプルコードだと、テキストボックスに入力される文字列が状態を持った値(State)。 これをReactで管理する。

Appコンポーネントの中でgetInitialState()というのがあるが、 これが状態の初期値を返すためのコールバック。 Stateを表すmarkdownという値が空であることを指定している。

また、同じくAppコンポーネントupdateMarkdownプロパティの中にthis.setState()という記述がある。 このsetState()で状態の更新を行う。 サンプルコードでは、textareaonChange属性にこれをセット。 イベントハンドラを指定することでテキストボックス内の変更イベントを監視し、 変更の度に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が更新される。

サンプルコードの流れ

いまさら。

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を学ぶべきタイミングはいつなんだろう。 やっぱ作ってるうちにきつくなってきて、ああいうアーキテクチャを使いたくなるものなのかな??