このページの本文へ

Cycle.jsがReactよりも優れていると私が思うこれだけの理由

2017年08月30日 17時00分更新

文●Ivan Jovanovic

  • この記事をはてなブックマークに追加
本文印刷
いま、もっとも人気のあるReactですが、私は検討の結果Cycle.jsを選びました。Cycle.jsの概要や使い方、魅力をお伝えします。

多くの開発者はアプリ開発時になんらかのフレームワークを利用しているでしょう。フレームワークは複雑なアプリ構築の手間をなくし、時間を節約します。利用者が多いからか、最高のフレームワークや、フレームワークの何を学ぶべきかなどの話題があふれています。

Reactは現在もっとも人気のあるフロントエンドフレームワークで、大きなコミュニティもあります。支持する人もいれば、言われているほどには良くないという人もいまが、私はReactでWebアプリに対する考え方や開発方法が変わりました。

Webアプリの開発方法の検討さえせずReactを使い始めましたが、じわじわと人気が出ている新しいリアクティブ・フレームワーク「Cycle.js」を試すことにしました。そこで学んだリアクティブ・プログラミングや、Cycle.jsの役割、なぜReactよりも優れているかを説明します。

リアクティブ・プログラミングとは

リアクティブ・プログラミング(RP)は非同期データストリーム(ページ更新無しの非同期で往来するデータ)を扱うプログラミングです。すでにWebアプリを開発したことがあれば、リアクティブ・プログラミングの経験があるでしょう。たとえば非同期データストリームで、クリックイベントを監視して、クリック発生時に処理を施すのもRPです。RPの根底には、クリックに限らずどんなところからでもデータストリームを発生させ、操作できることがあります。同じ効果や処理をするにしても、メソッド等の抽象は使いやすく、維持やテストがしやすくなります。

リアクティブ・プログラミングなら、統一されて一貫性のあるコードになります。クリックイベント、HTTPリクエスト、Web Socketなど、どんなデータでも、同じように記述しすべてデータストリームとして扱うので、適切な実装方法について考える必要はありません。さらに、mapやfilterなど便利な関数が多数用意されています。関数は新たなストリームを返し、そのストリームでまた別の処理をします。

リアクティブ・プログラミングはコードをさらに抽象化します。優れたユーザー体験が実現し、ビジネスロジックの構築に集中できるのです。

reactive-clicks

JavaScriptのリアクティブ・プログラミング

JavaScriptにはデータストリームを扱うための優秀なライブラリーがいくつかあります。有名なライブラリーにRxJSがあります。ReactiveXの拡張モジュールで、データストリームを監視して非同期プログラミングができるAPIです。観察対象(observable)であるデータストリームを作成し、さまざまな関数で処理をします。

ほかにもMost.jsがあります。性能比較からパフォーマンスがいいことが分かります。

Cycle.jsの開発者がCycle.jsのために用意した、軽量かつ高速なライブラリーxstreamは、メソッド数がたったの26個で、サイズはわずか約30KBです。JavaScriptのリアクティブ・プログラミング用ライブラリーでは高速の部類です。

以下の例ではxstreamライブラリーを使用します。Cycle.jsは軽量フレームワークなので、加えるリアクティブライブラリーも軽量にしました。

Cycle.jsとは

Cycle.jsは関数型かつリアクティブなJavaScriptフレームワークです。開発するアプリは関数main()として抽象化されます。関数型プログラミングの関数は入力と出力のみなので、関数の引数と戻り値以外が変化したり、外部との通信などの副作用はありません。main()関数の入力(ソース)は外部からの読み込み、出力(シンク)は外部への書き出しに該当します。外部への入出力はドライバー(DOM効果やHTTP効果、Webソケットなどを扱うプラグイン群)を通して管理します。

Cycle.js

出典:Cycle.jsの公式サイト(https://cycle.js.org/

ユーザーインターフェイスの作成とテスト、再利用可能なコード記述が簡単にできます。各コンポーネントは独立した純粋な関数です。コアAPIは関数runです。

run(app, drivers);

2つの引数appとdriversがあります。appはアプリのメイン関数で、driversは副作用を扱うためのプラグインです。

Cycle.jsは追加の機能を小さなモジュールとして分離しています。

  • @cycle/dom:DOM操作用のドライバー群。バーチャルDOMライブラリーsnabdomがベースのDOMドライバーとHTMLドライバー
  • @cycle/history:History API用ドライバー
  • @cycle/httpsuperagentベースの、HTTPリクエスト用ドライバー
  • @cycle/isolate:任意のスコープを付与したデータフローコンポーネントを作成するための関数
  • @cycle/jsonp:JSONPによるHTTPリクエスト用ドライバー
  • @cycle/most-run:mostによる、アプリのrun関数
  • @cycle/run:xstreamによる、アプリのrun関数
  • @cycle/rxjs-run:rxjsによる、アプリのrun関数

Cycle.jsのコード

Cycle.jsのコードを解説します。動作を解説するための簡潔なアプリとして「昔ながらのカウンター(計数機)」を作成します。Cycle.jsでのDOMのイベントの扱い方やDOMの再描画が分かります。

2つのファイルindex.htmlmain.jsを作成します。index.htmlは、アプリのすべてのロジックが入っているmain.jsを参照するだけです。また、package.jsonファイルを以下のコマンドで作成します。

npm init -y

続いて、メインの依存オブジェクトをインストールします。

npm install @cycle/dom @cycle/run xstream --save

@cycle/dom、@cycle/xstream-run、xstreamをインストールしました。babel、browserify、mkdirpも必要なのでインストールします。

npm install babel-cli babel-preset-es2015 babel-register babelify browserify mkdirp --save-dev

Babelを使うために、.babelrcファイルを作成します。

{
  "presets": ["es2015"]
}

package.jsonファイルに以下のスクリプトを加えます。

"scripts": {
  "prebrowserify": "mkdirp dist",
  "browserify": "browserify main.js -t babelify --outfile dist/main.js",
  "start": "npm install && npm run browserify && echo 'OPEN index.html IN YOUR BROWSER'"
}

Cycle.jsアプリを実行するためにはnpm run startコマンドを使います。

準備は完了です。コーディングを開始します。index.htmlに若干のHTMLを書き加えます。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <title>Cycle.js counter</title>
</head>
<body>
    <div id="main"></div>
    <script src="./dist/main.js"></script>
</body>
</html>

id名mainのDIV領域を作成しました。Cycle.jsを紐づけて、アプリの内容をこの領域に描画します。main.jsから変換・生成されバンドルされたJavaScriptファイルのdist/main.jsファイルも読み込みました。

Cycle.jsのコードを書きます。main.jsファイルを開き、必要な依存オブジェクトを導入します。

import xs from 'xstream';
import { run } from '@cycle/run';
import { div, button, p, makeDOMDriver } from '@cycle/dom';

xstream、run、makeDOMDriver、それとバーチャルDOM(div、button、)操作のための関数を読み込みました。

続いて、main関数を書きます。

function main(sources) {
  const action$ = xs.merge(
    sources.DOM.select('.decrement').events('click').map(ev => -1),
    sources.DOM.select('.increment').events('click').map(ev => +1)
  );

  const count$ = action$.fold((acc, x) => acc + x, 0);

  const vdom$ = count$.map(count =>
    div([
      button('.decrement', 'Decrement'),
      button('.increment', 'Increment'),
      p('Counter: ' + count)
    ])
  );

  return {
    DOM: vdom$,
  };
}

run(main, {
  DOM: makeDOMDriver('#main')
});

sourcesを受け取ってsinksを返します。sources(ソース、源)はDOMストリーム、sink(シンク、受信側)はバーチャルDOMです。順を追って説明します。

const action$ = xs.merge(
  sources.DOM.select('.decrement').events('click').map(ev => -1),
  sources.DOM.select('.increment').events('click').map(ev => +1)
);

2つのストリームを、ストリーム「action$」に統合します。ストリームを含む変数の名前の最後には$を付けるのが慣習です。decrement(1減らす)ボタンのクリックとincrement(1増やす)ボタンのクリックのストリームです。2つのイベントをそれぞれ-1と+1とします。これらを統合すると、action$ストリームの中身は以下の通りです。

----(-1)-----(+1)------(-1)------(-1)------

次のストリームはcount$です。

const count$ = action$.fold((acc, x) => acc + x, 0);

fold関数はaccumulateseed、2つの引数を受け取ります。はじめのイベントが来るまではseedの状態です。次のイベントではaccumulate関数に基づいて計算しseedと合算します。ストリームのreduce()に当たります。

count$ストリームの初期値は0を受け取り、action$ストリームから新しい値を常に合計してcount$ストリームの現在値になります。

サイクルを動作するため、main関数の下にあるrun関数を実行します。

最後にバーチャルDOMを作ります。

const vdom$ = count$.map(count =>
  div([
    button('.decrement', 'Decrement'),
    button('.increment', 'Increment'),
    p('Counter: ' + count)
  ])
);

count$ストリームのデータを振り分けて、ストリームの各データに対し、バーチャルDOMを返します。バーチャルDOMに含まれるのは、メインであるDIVラッパーが1つと、2つのボタン、1つの段落です。Cycle.jsはDOM操作にJavaScriptの関数を使っていますが、JSXも使用できます

このmain関数はバーチャルDOMを返します。

return {
  DOM: vdom$,
};

このmain関数と、ID名「main」のDIV領域に結び付けられたDOMドライバーを渡して、DIVからイベントストリームを取得します。以上で、Cycle.jsアプリの完成です。
以下のように動きます。

Animated GIF demonstrating the counter being incremented and decremented

以上、DOMストリームの扱い方です。

すべてのコードをGithubリポジトリに掲載しました。ローカル環境で実行してください。

ReactからCycle.jsへ乗り換える理由

リアクティブ・プログラミングの基本を理解し、Cycle.jsの簡単な実例を理解したところで、私がなぜCycle.jsを採用するのかお伝えします。

Webアプリを設計する際の問題は、膨大なコードベースおよび外部から来る膨大な量のデータをどう取り扱うかでした。私はReactが好きでたくさんのプロジェクトに使ってきましたが、Reactではこの問題は解決しませんでした。

データを描画すること、アプリのステートを変更することはReactが適しています。コンポーネントの概念はすばらしく、テストや維持もやりやすい優れたコードが書けました。でも、いつもなにかが足りなかったのです。

Reactの代わりにCycle.jsを使う場合の良い点・悪い点をまとめました。

良い点

1.大きなコードベース

Reactは100個のコンテナ内に、100個のコンポーネントがあり、それぞれが独自のスタイル、メソッド、テストを持っています。無数のフォルダーの中の無数のファイルの中に、何行にもわたるコードがあるわけです。アプリが肥大化すると、これらをかき分けていくのは骨の折れます。

Cycle.jsならプロジェクトを、副作用の無い、隔離されていてテストも可能な独立したコンポーネントに分割するので、大きなコードベースを扱えます。Reduxも副作用もない、純粋なデータストリームなのです。

2.データフロー

Reactの最大の悩みはデータフローです。Reactはデータフローを念頭に置いて設計されていないため、コアにはありません。開発者は対処するために多くのライブラリーや方法論を編み出しました。一例はReduxですが完璧ではありません。設定に時間を費やし、データフローを扱うためのコードを書く必要があります。

Cycle.jsは、データフローが扱えるフレームワークとして設計されています。データを取り扱う関数を書くだけでいいのです。

3.副作用

Reactアプリで副作用についての一貫した方法はありません。対処するためのツールはたくさんありますが、準備し使い方を覚える必要があります。人気のツールはredux-sagaredux-effectsredux-side-effectsredux-loopと、たくさんあります。ライブラリーを選んで、コードベースに実装するのは時間も労力もかかります。

Cycle.jsは必要なドライバー(DOM、HTTP、その他)を読み込んで使うだけです。ドライバーは、記述した関数にデータを送るので、処理して送り返すことで再びドライバーが反映します。ドライバーはCycle.js公式で標準化されているので、サードパーティ製に依存する必要はありません。シンプルですね。

4.関数型プログラミング

Reactの製作者はReactは関数型プログラミングを使っていると主張しますが、実際は異なります。たくさんのオブジェクト、クラス、thisキーワードを使用し、正しく使わなければ悩まされます。Cycle.jsは関数型プログラミングの概念を基に作られています。外部の状態に一切依存しない関数でできています。クラスや、準ずるものもありません。テストや保守が簡単です。

悪い点

1.コミュニティ

Reactは人気のフレームワークで、あらゆる場面で使われています。Cycle.jsは違います。人気とは言えず、予期せぬ状況に陥り、コードの解決策がインターネット上にも見つからず、自力で解決することもあります。サイドプロジェクトで時間がたっぷりあるならいいのですが、締め切りに追われる企業であればコードのデバッグに時間を取られます。

この状況は変わりつつあります。Cycle.jsのユーザーが増え、問題を議論し、力を合わせ解決しています。Cycle.jsには優れたドキュメント類やサンプルが多く掲載されているため、デバッグできないほど複雑な問題には遭遇したことがありません。

2.新たなパラダイムを覚える

リアクティブ・プログラミングは異なるパラダイムなので、学ぶのにはある程度時間を必要とします。一度覚えれば以降はすべてが楽になりますが、もしいま締め切りに追われている状況ならば、新しいことの学習に時間を費やすのは苦しいかもしれません。

3.アプリによってはリアクティブである必要はない

ブログ、マーケティングサイト、初期表示ページ、そのほか限られたやり取りしかしない静的なサイトなら、リアクティブでなくても大丈夫です。リアルタイムに行き交うデータはなく、フォームやボタンも少ししかありません。この場合リアクティブ・フレームワークはかえって遠回りです。WebアプリにCycle.jsが必要かどうか吟味したほうが良いでしょう。

最後に

理想的なフレームワークは、開発者が機能の作成と実装に集中できることです。ひな形のコード入力を強いるべきではありません。Cycle.jsは開発者がより良いコードを書くことと機能を実現することを追求できます。ただしこの世に完璧なものは無く、改善の余地は常にあります。

本記事はMichael Wanyoikeが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。

(原文:Why I’m Switching from React to Cycle.js

[翻訳:西尾 健史/編集:Livit

Web Professionalトップへ

WebProfessional 新着記事