22
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

仮想DOM/FluxAdvent Calendar 2015

Day 16

HaxeとReact

Last updated at Posted at 2015-12-15

HaxeからJavaScriptを出力するというのは、静的型付き言語が好きだとか、JavaScriptはそれほど好きではないがWebコンテンツは作りたいという人にはとても良い選択肢だと思います。

とはいえ、HaxeはTypeScriptほどJavaScriptに近くないので、HaxeとJavaScriptライブラリの相性というのはとても悩ましい問題です。jQueryを使うとHaxeを使ううまみが減るとか、変数名に"$"が使えないのでAngularJSが使えないだとか、そういったことです。

悩んだ結果、Dynamic型やuntypedキーワードを使って動的言語的な書き方をしてライブラリを使うか、もうJS製のライブラリを使うのをあきらめてgetElementByIdべた書きで頑張るとか、そういった選択を迫られてきました。

では今、流行りのReactの場合はどうでしょう?

実際につかってみた感想は、**最高だ…**って感じでした。

これだけHaxeから使いやすいJavaScriptのライブラリがあったことに、かなり驚いています。

haxe-reactを使う

HaxeからJavaScriptのライブラリを使う場合は、ライブラリのjsファイルと合わせてexternと呼ばれる型定義ファイルを使います。

Reactの場合はhaxe-reactというプロジェクトがすでにあるので、こちらのものを使います。

haxelib install react

でインストールして、コンパイルオプションに-lib reactを追加して使います。

そうすることで、以下のようにReactを使用することができます。

import api.react.ReactComponent;
import api.react.React;
import js.Browser;
import api.react.ReactDOM;

class App extends ReactComponent {

    static public function main() {
        ReactDOM.render(React.createElement(App), Browser.document.getElementById('app'));
    }

    public function new() {
        super();
    }

    override function render() {
        var cname = 'foo';
        return React.createElement('div', {className:cname}, [/*children*/]);
    }
}

JSXも動く

haxe-reactの注目すべきポイントはここです。React.jsにはJSXという仮想DOMのツリーを表現するDSLが提供されていますが、haxe-reactでもjsx関数を使うことでJSXの文法を使って仮想DOMを記述することができます。

import api.react.ReactComponent;
import api.react.ReactDOM;
import js.Browser;
import api.react.ReactMacro.jsx;

class App extends ReactComponent {
    static public function main() {
        ReactDOM.render(jsx('<App/>'), Browser.document.getElementById('app'));
    }

    public function new() {
        super();
    }

    override function render() {
        var cname = 'foo';
        var children = '';
        return jsx('<div className=$cname>${children}</div>');
    }
}

jsxの関数はマクロで実装されています。つまり、Haxeのコンパイルと同時にJSXのコンパイルが行われて、最初のサンプルコードと同様のJavaScriptのコードが出力されます。

haxe-reactには、元のReactのJSXコンパイラとは別のJSXコンパイラの実装があるわけです。

上記のコードをよく見ると元々のJSXの{}内に変数を書く構文とは異なり、$から始まるHaxeの文字列内変数の記法が使われています。これはHaxeの元々の文法に近い書き方をすることで、IDE上で$以降のHaxeの変数の補完が利くようにするものです。もちろん、{}で書く方法も使えます。

その他の記述方法

JSXで記述できるとは書きましたが、個人的にはJSXを使って書く方法はあまり好きじゃありません。理由は、XMLの記述にあまり補完が利かないからです。補完無しでXMLを記述するのは面倒ですし、自作のコンポーネントの記述も面倒です。

例えば、foo.bar.FormというクラスパスのReactComponentを作った場合、

jsx('<foo.bar.Form />');

というようにフルパスを記述するか、あらかじめimport foo.bar.Formを記述しておくかする必要があります。どちらも、IDEの自動importの機能との相性がよくありません。

Haxeを使うメリットの1つは強力なIDEの支援ですが、JSXを使うとそこのメリットが受けられません。

とはいえ最初の例の書き方もまどろっこしいので、別の記述方法も紹介します。

using api.react.React

以下のような、記述方法がIDEの補完も利きやすく書きやすいと感じました。

import api.react.ReactComponent;
import api.react.ReactDOM;
import js.Browser;
using api.react.React; // importではなくusingなのがポイント

class App extends ReactComponent {

    static public function main() {
        ReactDOM.render(App.createElement(), Browser.document.getElementById('app'));
    }

    public function new() {
        super();
    }

    override function render() {
        var cname = 'foo';
        return 'div'.createElement({className:cname}, [/*children*/]);
    }
}

usingというのは静的拡張と呼ばれる機能です。これでapi.react.Reactstatic関数のcreateElementを第1引数の型の拡張のように使うことができます。

ファイルツリーを実装してみる

Haxeらしいサンプルコードを実装してみたいと思います。何かというとファイルツリーの実装です。

フォルダをクリックすると空いたり閉じたりします。

Folder.gif

アイコンはFugue Icons(CC BY 3.0)を使いました。

実際のコードはこんな感じです。

import api.react.ReactComponent;
import api.react.ReactDOM;
import js.Browser;
using api.react.React;

class Sample {
    // エントリーポイントになるメイン関数。
    public static function main() {

        // ファイルツリーの構造体を作成
        var tree = FileTree.Folder(
            "トップ",
            true, // 開
            [
                Folder(
                    "デスクトップ",
                    true, // 開
                    [
                        File("メモ.txt"),
                        Folder(
                            "大事なもの",
                            true, // 開
                            [
                                File("写真1.png"),
                                File("写真2.png"),
                            ]
                        )
                    ]
                ),
                Folder(
                    "ミュージック",
                    false, // 閉
                    [
                        File("洋楽.mp3"),
                        File("チップチューン.mp3"),
                    ]
                )
            ]
        );

        // ファイルツリーの表示の開始
        ReactDOM.render(
            FileComponent.createElement({tree: tree}),
            Browser.document.getElementById('app')
        );
    }
}

// パラメータ付きのEnumとしてファイルのツリー構造の型を定義
enum FileTree
{
    File(name:String);
    Folder(name:String, open:Bool, children:Array<FileTree>);
}

// ファイルの状態の型定義
typedef FileProps =
{
    tree: FileTree,
}

// フォルダの型定義
typedef FolderProps =
{
    ?name:String,
    ?open:Bool,
    ?children:Array<FileTree>,
}

// ファイル要素。Propsの型を、型パラメータで指定
class FileComponent extends ReactComponentOfProps<FileProps>
{
    public function new (props:FileProps)
    {
        super();
    }

    public override function render():ReactComponent {
        // switchした結果を返す
        return switch (state.tree)
        {
            // ファイルのときは単なるdiv要素
            case File(name):
                'div'.createElement(
                    {className: "file"},
                    name
                );

            // フォルダのときはFolderComponent
            case Folder(name, open, children):
                FolderComponent.createElement({
                    open: open,
                    name: name,
                    children: children,
                });
        }
    }
}

// フォルダ要素。PropsとStateの型を、型パラメータで指定
class FolderComponent extends ReactComponentOfPropsAndState<FolderProps, FolderProps>
{
    public function new (props:FolderProps)
    {
        super();
        // propsをそのままstateとして使用
        state = props;
    }

    public function onClick():Void
    {
        // クリックされたら開閉を切り替え
        setState({ open: !state.open });
    }

    public override function render():ReactComponent {
        var label = 'div'.createElement(
            {
                // ファイルが開いてる場合と、閉じてる場合でアイコンを変える。
                className: if (state.open) 'opened-folder' else 'closed-folder',

                // クリックされたときのハンドラ
                onClick: onClick,
            },
            state.name
        );

        return 'div'.createElement(
            {},
            if (state.open) {
                [
                    label,
                    // 開いている場合、子を表示
                    'div'.createElement(
                        {
                            className: 'children',
                        },
                        [
                            // 配列内包表記で、子要素の配列を初期化
                            for (tree in state.children) {
                                FileComponent.createElement({ tree: tree });
                            }
                        ]
                    )
                ];
            } else {
                label;
                // 閉じてる場合、子は表示しない。
            }
        );
    }
}

ファイルツリーのデータ構造を引数付きのEnumで表現して、それをそのままReactで描画するということが実現できています。

まとめ

Haxeの強い静的型付け、IDEの支援、強力なマクロといったメリットは、Reactを使っても損なわれることはなく十分に生かすことができます。パラメータ付きのEnumが状態を表現するためのデータ構造としてかなり強力であるというのも相性が良い点です。

そのため、ファイルツリーのようなjQueryなどでは実装難易度の高いようなコンポーネントも素直なコードで実装できました。

ReactがAltJSの機能を阻害しないというのは、Haxeに限った話ではなく他のAltJSユーザーからもそのような声は上がっています。

Reactと関数型AltJS

Fluxについて

ただ、Fluxをどう実装するかというのは問題として残ります。

JavaScriptのライブラリでexternが作られているものは特になさそうですし、JavaScriptのライブラリを使うとするとまたHaxeとの相性の問題になってしまいます。

Dispatcherに送られるEventなど強く型付けされていてほしいものは多いので、Haxeで自前で実装してしまうのが賢明だと思います。

haxe-reactのサンプルコードのTODOアプリでは、msignalという非常に簡素なイベントディスパッチャのライブラリのみを使用してFluxライクな実装を行っているので、こちらを参考にすると良さそうです。

22
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?