2016年5月18日水曜日

Reactを使うとなぜjQueryが要らなくなるのか


はじめに

React (React.js) を全く知らない、あるいは幾つか記事を見たけどなんなのかピンと来ていない、という人のために、少し前にピンと来たばかり(のつもり)の人が書いています。
「jQueryくらいしか知らないけど、それで十分やっていけてるし…」くらいの人が対象であり、すでにやる気がある人向けのチュートリアルではありません。やる気が出ればドキュメントを読んで手を動かせばあっという間ですので、そこまでの興味が出ることを目標にしています。
イメージ優先で乱暴で割り切った書き方をしていたり、最近(2016年4月現在)の記法をあたかも昔からそうであるかのように語ったりしている部分がありますが、ご了承ください。
ES2015 (ES6) の文法(アロー関数とか)は普通に使っています。Reactを使う以上、どうせ何らかのトランスパイラは必須ですし、だいたい今どきBabelかTypeScriptか使ってない人とかおらんやろ(煽り)。その部分が怪しい人は先にそちらの復習からどうぞ。

Reactがやること

Reactがやることは非常にシンプルなので、APIも数えるほどしかありません(むしろバージョンアップ毎にAPIが減りつつある)。「記憶力を使って覚えることの少なさ」に関してはピカイチだと思います。そのわずかなAPIに、みんな慣れ親しんできたjQueryのメソッドの大部分を駆逐してしまうほどの威力があります
Reactがやれることを3行で説明すると、こうなります。
  1. 「ページ状態を保持しているプレーンなJSのオブジェクト」に、
  2. 「テンプレート的な関数」を作用させて、「仮想DOM」と呼ばれるDOMの設計図を取り出し、
  3. その設計図を使って本物のDOMを構築する。
ここでいう「プレーンなJSのオブジェクト」とは、JSONをパースするだけで取得できる類の、シンプルでクリーンなデータを保持しているJSオブジェクトと思ってください。そこには、Web APIから読み取ってきた商品リストのデータであるとか、現在アクティブなタブのインデックスであるとか、入力中のフォームデータであるとかといった、「ページの状態」にあたるものがまとめて入っているイメージです(Fluxを聞いたことがあるならStoreに相当)。
Web APIなどからプレーンなJSオブジェクトを(JSONを使って)取り出し、そこからDOMを作り出すことはjQueryでよくやる作業です。例えば「商品データを一覧表示」なら、愚直にやるとこんな感じ。
$.getJSON('/api/items').then(data => {
    const ul = $('ul.item-list').empty();
    data.items.forEach(item => {
        const li = $('<li>').addClass('item').appendTo(ul);
        if (item.stock === 0) li.addClass('soldout');
        $('<div>').addClass('item-name').text(item.name).appendTo(li);
        $('<div>').addClass('item-price').text(item.price).appendTo(li);
        $('<button>').text('注文').addClass('btn').appendTo(li)
            .on('click', orderClickHandler);
    });
});
難しいことはしていませんが、見た目に直感的ではない感じはします。
同じことをReactで書くと、こういう全く違う見た目になります(行数が増えているのは関数を分割したりしているから)。
// ItemListのコンポーネント定義(実体は関数)
const ItemList = props => {
    return <ul className="item-list">
        {props.items.map(item => <ItemDetail item={item} />)}
    </ul>;
};

// ItemDetailのコンポーネント定義
const ItemDetail = props => {
    const item = props.item;
    return <li className={'item' + item.stock === 0 ? ' soldout' : ''}>
        <div className="item-name">{item.name}</div>
        <div className="item-price">{item.price}</div>
        <OrderButton onClick={orderClickHandler.bind(null, item.id)} />
    </li>;
};

// OrderButtonのコンポーネント定義(ワンライナー)
const OrderButton = props => <button className="btn" onClick={props.onClick}>注文</button>;

$.getJSON('/api/items').then(data => {
    ReactDOM.render(
        <ItemList items={data.items} />, // これを
        document.getElementById('container') // ここにレンダリングしろ
    );
});
え~ととりあえず、面妖な見た目のHTMLタグっぽいものを見て速攻逃げ帰りたくなった人がいますよね。なんとReactでは、このようにJavaScriptとHTML(?)が悪魔合体しているのが基本作法です。太古の黒歴史であるonClick が華麗に復活しているのも誤植ではありません。まあ少し我慢してお付き合いください。とりあえず、この「タグ」は実際には単なる固定の関数呼び出しであり、Babelなどに通すと機械的に関数呼び出しに変換される1ので、これはあくまでJavaScriptです。あと「$.getJSON()はjQueryやろ!」とツッコんだ人は、鋭いですね。$.getJSON便利ですもんね。(…代替は最後に提示します)
:information_source: 筆者もちょっと前までこの壮絶な外見を嫌悪して逃げていたクチですが、1日経たずにあっさり寝返りました。後述します。特に気になるであろうonClickの類にも、Reactでは実害はありません。onClick などの属性は、こういう新しい記法なのだと思ってください。実際は内部でイベントバブリングを使った最適化がちゃんとされています。
まあともかく、DOMを直接切ったり貼ったりする部分はなくなり、代わりに3つのコンポーネント(と言うか関数)を定義しています。それぞれの関数はなにやら「HTMLタグっぽいもの」を返しており、その中でJavaScriptの変数が埋め込まれています。このタグっぽいものが、巷で話題の仮想DOMとか呼ばれているアレです。
ReactのAPIを露骨に呼び出しているのは最後の ReactDOM.render() だけ。しかもこれがこの記事に出てくる唯一のReactのAPIです。データを表示するだけならとりあえずこれさえあれば作れます。
:information_source: 1年以上前の記事で React.createClass という関数を使ってコンポーネントを定義している記事を見て、不格好でタイプ量が多すぎと感じた人がいるかもしれませんが、いまや大部分のコンポーネントはこのように単純な関数で記述するのが基本です。
ぱっと見、サーバサイド言語でよく使われている、いわゆる「テンプレート」に似ている気がしませんか。ただしテンプレートがあくまで文字列ベースの処理なのに対し、Reactは 仮想DOMと呼ばれる「DOMの設計図」 をベースに処理をします。ItemListItemDetailといった関数が返している「タグっぽいもの」はDOMではなく、あくまで設計図であり、つまりは非常に軽量なJavaScriptのオブジェクトです。
古典的なテンプレートエンジンでは、開発者はそのエンジン独自のテンプレート構文(ループとか)を覚え、テンプレートファイルを書いていきます。その代わりにReactでは、開発者はDOMの設計図をreturnする関数をJavaScriptで書いていきます。このHTMLタグっぽい記法以外に、独自規則はありません。ループや条件分岐は、見ての通り普段JavaScriptで使っている三項演算子や Array.prototype.map をそのまま使うだけです。
タグっぽい記述(の関数呼び出し)を通して仮想DOMを作ったら、ReactDOM.render が、それに対応する本物のDOM要素を自動的に構築していきます。 これが、開発者が手でいちいち appendTo() とか removeClass() とかtext() とか val() とかのjQuery的なDOM操作を書かなくて済む理由です 2
Reactとは基本これだけをするライブラリです。んー、大したことない気がしませんか。というか、これってサーバサイド言語(PHPとかJSPとか)で使ってきたテンプレートを、ちょっと面倒くさくしただけではないでしょうか。テンプレートと比べて何が嬉しいのでしょう。
なぜこれだけのライブラリが、一部界隈で、まるで世界を革命する力であるかのように言われているのでしょう? 以下で述べていきます。とりあえず最初は表面的なところから。

理由: JSXが便利で見やすい

既に見たとおり、Reactでは JSX と呼ばれる新しい記法をJavaScriptに導入しています。これが必要な理由はごく単純で、普通のJavaScriptの構文だけで「DOMの設計図」を過不足なしに記述するのは、人間にはとんでもなく辛いからです。JSON的なデータを記述するにはJavaScriptの構文は最強なのですが、「要素があって属性があって、子にはテキストノードや別の要素があって」というHTML/XML的なデータ構造を、素のJavaScriptは効率的に表現できるようにできていません。
JSX/XML/HTMLで書くと <textarea name="message">Hello <b>World</b></textarea> で直感的に表現できる情報は、もしJSON的に書けと言われると例えばこうなってしまうでしょう。
{ element: 'textarea',
  attributes: { name: 'message' },
  content: [ 'Hello ', { element: 'b', content: ['World'] } ] }
Babel や TypeScript は既に普通にJSXをサポートしています3ので、これらに馴染みがあれば導入のハードルは高くないはずです。少なくとも、素のjQueryよりは読みやすいはず。JSX自体は汎用性の高い記法なので(単なる関数呼び出しのシンタックスシュガー)、他のライブラリでも採用されつつあるそうです。
慣れ親しんだテンプレートと比べると可読性はわずかに落ちますが、タグの対応がとれていないなどのシンプルな構文ミスはコンパイル時にチェックされますし、TypeScriptを使えば厳密な型チェックも入ります。本質的にJavaScriptでありJavaScriptの表現力をすべて使えるので、特定のテンプレートの構文を覚えるよりは遥かに応用性があると思います。あとテンプレートと違い、見た目だけ構築して後でjQueryで別にイベントハンドラをくっつけるといった2段階作業も不要になります。
:information_source: JSXは最初は違和感が強いでしょうが、実はこれは、XMLが今よりずっとメジャーだった時代に提案され、一部実装もされていたE4X記法とそっくりです。まだJSONがなかった昔、今のJSONの位置にXMLを使う未来が想像されていた時代があり、その時代には文字列リテラルを""で囲むのと同じ感覚で、XMLリテラルを<hoge></hoge>で囲んでJavaScriptで書くのが自然と思われていたのです。それを知っていれば、JSXはJavaScriptのごく自然な拡張に見えてくるかも。
そして、このようにHTMLとJavaScriptが悪魔合体していることは、禁忌どころか、大事なメリットなのだと考えましょう。「HTMLとJavaScriptは分離せよ」は長い間の鋼の掟でしたが、それは、HTMLこそが単独でも成立する主役ドキュメントであり、サーバサイドでHTML内に実データを頑張って埋め込んでおり、JavaScriptがおまけだった時代の話です。SPAで殆どの実データがAPI経由でやってきて、あらゆるものが動的に構築されるようになると、HTML部分は「中身のない<ul>」「カレンダーやスライダに変身させられるのを待っているだけの<div>」「<form action=...>と結びつかないフォーム要素」「見出し行だけのテーブル」「クリックしてもどこにも飛ばない<a><button>」等々、JavaScriptなしには意味を持たない“抜け殻”が静的に陳列されるだけの場と化していきます。CSSの表現力向上もHTMLタグの重要性を下げていきます(少し前まで角丸ボーダーにテーブルレイアウトが必要でした)。ある一線を越えたらもう、抜け殻のHTMLタグだけを遠くの別ファイルで独立してメンテし、遠距離狙撃で .bind() や .datepicker() や .val() を撃ちまくることは、重要でも効率的でもないのです。むしろ機能的に関連するタグと動作を、名前付きでまとめて短く記述できるReactの記述方法こそが楽、と思います。
:information_source: とはいえ行数が増えていくと可読性はあっという間に落ちていくので、常に小さな粒度の関数(コンポーネント)を作ることを心がけましょう。普通のJavaScriptが普通に書ける人なら普通は問題ないはずです。私見ですが、2重より深いタグのネストがあったら恐らく関数を分割した方がいいです。見た目がなんぼドラスティックでも、この「タグ」は単なる関数呼び出しに過ぎないということは改めて強調しておきます。

理由: テンプレートより圧倒的に速い差分描画

何らかの理由(ユーザ操作など)でページの状態が変わり画面を書き直す場合、ReactはゼロからDOMを毎回作り直すのではありません。画面を更新する際、Reactは新旧の設計図を見比べて差分を計算し、パッチ的にDOM操作を適用します。
開発者がやることは、新しい状態が含まれた新しいプレーンなJavaScriptオブジェクトを準備して、さっきのReactDOM.render() で再描画するだけです。あとはReactが以前の設計図と見比べ、勝手にjQueryでいうところのremove() や val() や prop() や addClass() などなどに対応する操作に置き換えて実行します。これらを人間が手で書かなくても大丈夫。
この差分描画はとても高速なため、ウェブページ全体をReact化して適用してしまうことが普通に可能です。1キーストロークごとに、アプリケーション丸ごとの状態が含まれたプレーンなJavaScriptオブジェクトでアプリケーション全体を「再描画」しても、割と快適に動作します(とても複雑なページだと最適化が必要)。これは文字列ベースのテンプレートでは遅すぎて到底できない芸当です。仮想DOMの比較はJavaScript内のみで完結する軽い演算なので、本物のDOM操作と比べると圧倒的に速いわけです。
ページ丸ごとを更新できるので、「ロード中はこのボタンとこのボタンを一時的に無効化してここにインジケータを表示して…」とか「このモードに入ったらこのDIVが非表示でこの要素にこのクラスが設定され…」みたいな面倒なコードが一掃され、勝手にページ全体の一貫性が保たれます。サンプルレベルでは実感できませんが、実用的なサイズのアプリならめっちゃ楽で、バグの心配が激減します。
更に、ページ全体の情報が1箇所に集まっているため、「ブラウザをリロードされてもすぐ状態が復元できる」「元に戻す/やり直す操作が簡単に実装できる」といったメリットもあります。

理由: 超軽量なコンポーネント

Reactでは、「部品」「コンポーネント」的なものがアホみたいに簡単に書けます。それを組み合わせるのも非常に簡単です。コンポーネントを使う際にも、標準のHTML要素と独自タグの境界はほとんどありません。コンポーネントといっても何しろ単なる関数ですから、ワンライナーでもよい。
// コンポーネントの定義
const Heading = props => <h1>{props.title} <em>{props.subtitle}</em></h1>;

// コンポーネントの使用
const virtualDOM = <Heading title="Hello" subtitle="React" />;
これは極端にシンプルな例ではありますが、実際にアプリケーションを作るにあたって、大部分のコンポーネントはこのように極力単純な関数で書き、それを組み合わせてアプリにしていくことが推奨されます。数行で済む「ヘッダーコンポーネント」「目次コンポーネント」とかを何十個も書いて、最終的に1個のアプリを作り上げていくイメージ。この手軽さは、ちょっとjQuery UIとかのコンポーネントには真似できません。SubversionのブランチがGitのブランチに変わったくらいのインパクトがあります。
ただしこんな芸当が可能であるために、「コンポーネントが原則として内部状態を持たない(ステートレス)」であることがとても重要です。というわけで次の話。

理由: 原則的にステートレスなコンポーネント

コンポーネントがステートレスである、とは、返すDOMの設計図が外部から与えられたパラメータ(props)のみで一意に決まるということです(数学的な意味での関数に近い)。ステートレスコンポーネントは見てきたとおり、単なるJavaScriptの関数で書けます。
Reactを使うにあたって、jQueryから一番考え方を変えないといけないのがこの部分でしょう。jQueryのコンポーネント(カレンダーでも、スライダーでも)は内部状態(プロパティ/ステート)として「現在の入力値」とか「disabledかどうか」とかを持っているのが当たり前でしたし、それの初期化と出し入れで記述が長くなる傾向にありました。Reactでは原則、状態は外部に置かれます。コンポーネントとは外部からの情報をDOM設計図に変換するプリズムに過ぎません。そう心得て、可能な限りそのように作るべきです。
:information_source: …と書きましたが、必要ならステート付きのコンポーネントも書けます。多少記述は長くなりますが、最近はES6のクラス構文が使えるので、少なくともjQuery UIとかよりは遥かにクリーンな見た目になります。
基本的に「外部から絶対利用しないプライベートな内部状態」はステートにしてしまってもよいでしょう。「ドロップダウンメニューが今ドロップダウンされているかどうか」とか。
「外部に状態がある」というのはカスタムコンポーネントだけの話ではありません。Reactでは<input>などの標準フォーム要素ですらその発想が徹底しており、ステートレスな振る舞いをします(JSFiddleでのデモ)。
// valueが固定なので、このinput要素はリードオンリーになり、中身を変更できない!!
ReactDOM.render(
  <input type="text" value="Can you edit this?" />,
  document.getElementById('container')
);
編集可能にしたければどうするのかというと、例の「ページ状態を管理しているプレーンなJavaScriptオブジェクト」を使います。テキストボックス内で入力操作が起きた場合、内部状態が変わってからイベントが発火するのではなく、コールバック関数経由で「外部の状態」を先に変更してから、画面を再描画します。こんなイメージ(JSFiddleでのデモ)。
let user = { name: '', age: 25 }; // 外部にある唯一の状態オブジェクト

const change = (key, newValue) => {
    user[key] = newValue; // 外部の状態を変更
    render(); // あたらしいデータでざっくり再描画
}

const submit = () => {
  alert(JSON.stringify(user));
  /* userは最新版のフォームの状態を常に反映している! */
};

const isValidUser = () => (user.name.length > 0 && user.age >= 20 && user.checked);

// コンポーネント定義
const UserForm = props => <div>
    <p>名前<input type="text" value={user.name} onChange={ev => change('name', ev.target.value)} /></p>
    <p>年齢<input type="number" value={user.age} onChange={ev => change('age', ev.target.value)} /></p>
    <p><input type="checkbox" checked={user.checked} onClick={() => change('checked', !user.checked)} /> 規約読みました</p>
    <button onClick={submit} disabled={!isValidUser()}>20歳以上なので登録</button>
</div>;

const render = () => {
    ReactDOM.render(<UserForm user={user} />, document.getElementById('container'));
};

render();
最初は回りくどいと思うでしょうが、綺麗な元データが外部の1カ所にしかないので、複雑なフォーム・アプリになればなるほど楽さが際立ちます。jQueryで設定画面などのフォームを書いていると、val() や text() を駆使して、「生データの状態をDOMに反映させる」のと、逆の「DOMの状態から生データを再構築する」のを両方書かないといけません。この構造ならそういう作業は要りません。
例えばこのくらいの複雑さのフォームになると、ReactならjQuery UIの半分の行数で実装できる感じ(6個のステートレスコンポーネントの組み合わせ。ステート付きコンポーネントは自力では一切書いていませんが、React-Bootstrap を使用しています)。
condition-image
このデータの流れをもっと整理して「外部の状態」を綺麗に管理するためのノウハウがFluxやらreduxやらです。が、とりあえず、要はこの考え方を進めてAPIにしたものという理解でいいと思います。小さなアプリなら既存のFluxフレームワークを使わずにコールバックだけで済ますのも全然アリだと思います。

Reactが向かない場合

Reactが万能なわけではありません。Reactの弱点、Reactが向かない場合もあります。
  • React自体の理解は全く難しくないけど、Babel/TypeScriptのようなトランスパイラとビルドシステムの知識は必須であり、Webpack/Browserifyのようなモジュールバンドルシステムの知識も(絶対ではないけど)ほぼ必須。そこまで至っていない人にとっては学習コストが高い。さっさと『旧石器時代のJavaScript』から抜け出しましょう :-)
  • あくまでJavaScriptベースのライブラリなので、デザイナ系の人の理解と協力が必要。
  • 大抵の場合はサーバ側の協力も必要(基本的にはサーバ側は楽になる方向だけど)。純なReactアプリではHTMLは数行くらいの固定ファイルになり、あらゆる実データはJSONのAPI経由などで来る。サーバサイドに必要なのはAPIであり、テンプレートでHTMLにデータを埋め込むことではない。逆にHTMLが主役でありつづけ、動的な要素が少ない場合はjQueryでいい。
  • React最新版ではIE8以下は切り捨て。も、もういいよね…
  • ちゃんと最適化したjQueryよりは若干遅いので、本当にパフォーマンスが必要な場合(ゲームなど)ではしっかり検討する必要がある(はず)。
  • ReactにとってDOMとはあくまで外部状態を反映した映像に過ぎないので、逆にDOMからデータを取ってくるような作業は一切行えない(スクレイピングはできない)。

結論: You Don't Need jQuery?

最近You Don't Need jQueryという記事が話題になっていましたが、はてブとかの反応を見る限り、「むしろjQueryの使いやすさが良く分かった」という人が予想外に多い感じでした。
でもその記事の冒頭部分にある通り、その記事は「DOMを直接操作することはアンチパターンとなりました」という大前提で書かれています。「(頑張れば標準APIでもjQueryと同じことを書けるから)もうjQueryは必要ない」ではなく、もっと根本的に「(jQuery的なDOM操作なんか人間がやることじゃないから)もうjQueryは必要ない」です。jQueryを使うことで標準APIより数文字節約できるような作業の大部分は、そもそも1行も書かなくてよくなったのです。
DOM操作系が軒並み不要で、$.proxy とか $.each とかの「昔は便利だった」jQueryのユーティリティメソッドも、今はほぼ標準で代替可能です。となると、jQueryが真に有用な場面はかなり少ないです。今でもjQueryがないと多少辛いのは $.extend (Object.assignがIE11でも未サポート)と $.ajax 系くらいですが、そのくらいならもっとコンパクトな代替品を探せます4。そう気づいてしまい、残ったjQuery依存を消し去りたいという衝動に駆られたら、はじめて、ああいう記事の出番なわけです。わずか数カ所の数文字のタイプ数増加で、100KB超のライブラリを捨て去れるなら、Webでのメリットは大きいです。
元記事にもリンクされていますが、個人的にはYou Might Not Need jQueryというサイトがまとまっていてお勧め。

註: お陰様で、はてブ等で様々なコメントを頂いたので、それらを元にけっこう表現の修正や加筆を行いました。今見当違いのように見えるコメントを見ても、大半は私が加筆したせいです。React使わない人を煽るような表現は当初から全くないつもりなのですが(想定読者様ですし…)、「仕様になったES6の文法や、モジュール分割の方法を一生学ばない」という選択肢はない5と信じているので、そこの部分で煽るのは止めないよ!

  1. <Element a="b" c={d}>Text</Element> が React.createElement(Element, {a: 'b', c: d}, 'Text')になる)。 
  2. ただし裏で、イベントの標準化であるとかブラウザ間の差の吸収であるとかをやって、開発者が細かいことを気にしないような処理をいろいろしているようです。この辺はjQueryと一緒。 
  3. この1年でReactの世界に起きたありがたい変化のひとつです。以前は「専用のJSX変換ツールを使え」というツラい世界であり、「ReactはaltJSと相性が悪い」とまで言われていました。 
  4. $.extend の代用品は個人的にはこれのコピペ、 $.ajax/$.getJSON 等の代用品としてはsuperagent(定番)かaxios(新しくてPromise対応)を使っています。 
  5. 仮にその選択肢があるとしたら、恐らくその時点でReactの要らないプロジェクトです、安心してjQueryを使いましょう(←ダメ押しの煽り)。 

0 コメント:

コメントを投稿