タオルケット体操

サツバツいんたーねっとでゲームとかガジェットのレビューとかをします

ReactとFluxの入門のためにArdaを使ってみた

Sponsored link

http://facebook.github.io/react/img/logo.svg

FluxもReactもよくわからん状態で入門して今日一日泣きそうになってたのでメモ。
というかいわゆるJavaScriptMVCをガッツリ書いたこと自体がないです。

Arda、指輪物語由来っていうあたりがよさがありますね。
あと全体的に薄いので、FluxとReactの仕組みを学びつつ、他と組み合わせたりいざとなったら捨てたり出来そうなのでよさそうっぽいなっていう雑な考えでいじりはじめました。

また、せっかくなので今回はContext層をTypeScriptにしつつComponentをCoffeeScriptにしてTemplateをreact-jadeに分離する、作者推奨っぽい構成でやってみました。

全体的な構成は mizchi-sandbox/arda-starter-project · GitHub から持ってきた感じなので、ツールの設定など特に言及のない部分は同じだとおもっていただきたい。
TSの型定義はdtsm、タスクはgulp、ビルドだけはWebpackを使ってます。

サンプル

今回書いたコードの配置です。

src
├── entry.ts
├── index.coffee
├── main-components.coffee
├── main-context.ts
├── main-defs.ts
├── main-template.jade
└── setup
    └── _bootstrap.coffee

MVVMチュートリアルにありがちな、テキストボックスに名前を入れると自動的に下の方の名前が同期するっていうTwo-way Bindingなサンプルを作ろうとしたところ、Fluxの一方向にだけイベントが流れるという概念を理解していない & ちょうど良いサンプルの実装がなかったためにハマりまくりました。
適時ハマったところはメモっていきたい。

まずはTypeScriptをビルドするため、型解決のエントリーポイントとなるファイルを作ります。

// resolve typed installed by dtsm
///<reference path='../typings/bundle.d.ts' />
///<reference path='../node_modules/arda/arda.d.ts' />

declare var App: any;

import MainContext = require('./main-context');

これを起点にビルドして、
$(npm bin)/tsc -d -m commonjs --preserveConstEnums -t es5 --sourceMap --outDir temp src/entry.ts
というようにコンパイルして、CoffeeScriptなどからCommonJSスタイルでインポートできるようにビルドする感じですね。

次はアプリケーションのエントリーポイント。

# index.coffe

document.addEventListener 'DOMContentLoaded',  ->
  do require './setup/_bootstrap'

# setup/_bootstrap.coffee

global.Promise   = require 'bluebird'
global.React     = require 'react'
global.Arda      = require 'arda'
global.App ?= {}

MainContext = require '../main-context'

module.exports =  ->
  Promise.resolve require './globals'
  # Init router
  .then ->
    App.router = new Arda.Router Arda.DefaultLayout, document.getElementById('app-main')

  # Initialize databases
  .then ->
    App.router.pushContext MainContext, {userName: 'taro'}

簡単のため、主要な名前だけをglobalに出したり、ArdaのRouter(名前が変わるかもしれんらしい)の初期化をしたりとかそんな感じです。

次がContextとComponent(とTemplate)です。

// main-defs.ts

export interface Props {
  userName: string;
}

export interface State {
  userName: string;
}

export interface ComponentProps {
  userName: string;
}


// main-context.ts
import d = require('./main-defs');

var subscriber = Arda.subscriber<d.Props, d.State>((context, subscribe) => {
  subscribe('main:username-changed', (nvalue) => {
    context.update((s) => {return {userName: nvalue};});
  });
});


class MainContext extends Arda.Context<d.Props, d.State, d.ComponentProps> {
  static component = require('./main-components');
  static subscribers = [
    subscriber
  ];

  public initState(props) {
    return new Promise<d.State>(done => done({userName: props['userName']}));
  }

  public expandComponentProps(props, state): d.ComponentProps {
    return {
        userName: state['userName']
    };
  }
}

export = MainContext;
# main-components.coffee

template = require './main-template'

Actions =
  onClickGoToRoom: (event) ->
    @dispatch 'main:go-to-room'

  handleNameChanged: (event) ->
    @dispatch 'main:username-changed', event.target.value


module.exports = React.createClass
  mixins: [Arda.mixin, Actions]

  render: ->
    template @
//- template.jade (react-jade)

div#main

  h2 Hello Arda

  div#field
    p
      label(for="nick") Enter Your Name
      input#user(type="text" name="user" value=props.userName onChange=handleNameChanged)
    p= 'Hello '  + props.userName + '!'

ここから若干僕の知識も危うくなってくるわけなんですが、まずContextはEventEmitterのラッパー兼Reactとの連携APIを提供してくれるような実装だと思いねぇ。
そんでContextはFluxでいうところのDispatcherに当たる部分で、subscriberがCallbacksかな。

ComponentはもうほぼReactのComponentそのままです。
Arda.Componentを継承するのと、React.createClassしたインスタンスにArda.mixinすることの違いは React v0.13.0 Beta 1 | React を読めばわかります。
継承の方が気持ちがよいですが、actionをmixinで分離して提供出来ることを考えるとしばらくはArda.mixinの方が良さそうかなーと僕はおもいました。

ほぼReactのComponentそのままだと書きましたけども、こいつはdispatchというメソッドを持っていて、Contextに対してイベントを発行できるようになっています。ここら辺の流れはEventEmitterそのままなので、同じ使い勝手で値を渡したりできます。

ここで一旦フレームワークの流れを整理しますが

Router.pushContext → Contextの初期化 → Componentの初期化 → ReactViewの描画 → イベントの発生 → Contextのsubscriberが発火

みたいな感じです。

で、
mizchi-sandbox/arda-starter-project · GitHub のデフォルトであるようなsubscriberからのpushContextで次のContextへ移動……という流れはわかりやすかったのですが、いわゆるデータバインディング的な挙動をさせる方法がわからずに四苦八苦してしまいました。
というのも、ContextのpropsやstateがComponentのpropsと常に同期しているもんだと思い込んでいたからなんですが。

結論からいうとComponentはContextのComponentPropsで公開されている値にだけ関心を持っているべきっぽいです。
Fluxの設計からいっても、Mutableな値の変化はContextが発行するCallbacksからStoreに流れていきますしね。ただし、Storeの変更からReactのStateへの変更通知、そしてexpandComponentPropsへ、という流れの実装自体はContextがもっているようです。

つまり長くなりましたが、データバインディングのサンプル的なことをしたい場合は

Contextのsubscriberが発火 → context.updateでStateの更新 → expandComponentProps → ReactViewの描画 → イベンry……

という実装を行えば良いみたいです。

最初は「わかりずれーしコード量増えるし仮想DOMだのFluxだのなんやねん」とか思ってましたけど、わかってみると変に依存関係作ったりしないし設計が明瞭になるしテスト書きやすそうだし最高なのでは感が出てきましたね。

出来上がりはこんな感じです。

f:id:hachibeechan:20150223200618p:plain

まとめ

というわけでReactもFluxもイマイチな理解度でArdaを触ったわけなのですけれども、良い感じに薄くラッパーしつつFluxな流れを導いている感が学習にちょうどいいなとかそんなかんじでした。
必要充分な感じではありますが、Meta-Flux frameworkと銘打つだけあってここからラッパーを定義してもっと暑くしていっても良さげですね。Routingや、ModelというかStoreの実装なんかはもうちょいなんか組み合わせたい雰囲気を感じますね。

そもそものReactの先行きとか、さらに利用しているバージョンがbetaだったりとだいぶエッジな感じのフレームワークですけども、実装はシンプルだし、僕の魂のステージをあげるためにもこれから仕事で作るプロトタイプに使ってみようとおもいました。

おしまい。