かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

TypeScript JSXでReactのFluxのHello worldしてみた

Fluxとは。

facebook.github.io

Reactで色々柔軟にできるけど、アプリ内のデータの流れを一方向に整理してみましたということみたいですね。Fluxはアーキテクチャであって、フレームワークやライブラリではないというけれど、一応公式で小さなライブラリがあります。

今回はこれを使ってみようと思います。

下準備

下準備としてライブラリと型定義を入れます。打ち込んだコマンドはこんな感じです。

npm install react react-dom flux --save
tsd install react react-dom flux

Dispatcherで投げられる型の定義

DispatcherでActionsからStoreへ投げる型を定義しておきます。とりあえずActionの識別子とデータが入る入れ物って感じのクラスにしました。

class Payload {
    constructor(public action: string, public data: any) { }
}

アクションの定義

ここでWebAPIにアクセスしたりしてから、DispatcherでStoreにデータ投げるっぽいですが、今回はちょっとデータ加工してDispatcherに投げます。

class Actions {
    static greet(name: string) {
        AppDispatcher.dispatch(new Payload('greet', "Hello, " + name));
    }
}

AppDispatcherは後で定義するDispatcherの定数です。

ストアの作成

データの入れ物とビジネスロジックも書くらしいストアです。今回はデータの入れ物として。適当なクラスを用意しておいて、ReduceStoreっていうのを使うとお手軽っぽいです。getInitialStateで初期状態を返してreduceでPayloadを受け取って処理して結果を返すという感じです。

これだけで、変更通知もしてくれるというのでありがたいですね。

class State {
    constructor(public message: string) { }
}

class Store extends FluxUtils.ReduceStore<State> {
    constructor(d: Flux.Dispatcher<Payload>) {
        super(d);
    }

    getInitialState() {
        return new State('');
    }

    reduce(state: State, action: Payload) {
        switch (action.action) {
            case 'greet':
                return new State(action.data as string);
            default:
                throw Error('invalid operation');
        }
    }

}

定数の定義

DispatcherとStoreはグローバルな感じで。割り切りなんですかね。

const AppDispatcher = new Flux.Dispatcher<Payload>();
const AppStore = new Store(AppDispatcher);

コンポーネントの作成

あとは、入力欄とボタンと出力を持った簡単なコンポーネントを作りました。

class App extends React.Component<{}, AppState> {
    private listenerSubscription: { remove: Function };

    constructor(props: {}) {
        super(props);
        this.state = { name: '', message: '' } as AppState;
    }

    private getState(): AppState {
        return {
            name: this.state.name,
            message: AppStore.getState().message
        };
    }

    private handleChange() {
        this.setState(this.getState());
    }

    private handleClick() {
        Actions.greet(this.state.name);
    }

    private handleNameChange(e: React.SyntheticEvent) {
        var name = (e.target as HTMLInputElement).value;
        this.setState({
            name: name
        } as AppState);
    }

    componentDidMount() {
        this.listenerSubscription = AppStore.addListener(this.handleChange.bind(this));
    }

    componentWillUnmount() {
        this.listenerSubscription.remove();
    }

    render() {
        return <div>
                <div>{this.state.message}</div>
                <input type='text' onChange={this.handleNameChange.bind(this)} />
                <button onClick={this.handleClick.bind(this)}>Click</button>
            </div>;
    }
}

動作確認

これを動かすと、こんな感じで名前入れてボタンを押したらメッセージ出ます。

f:id:okazuki:20151229174805p:plain

コード全体

app.tsx全体を置いておきます。

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import * as Flux from 'flux';
import * as FluxUtils from 'flux/utils';

class Payload {
    constructor(public action: string, public data: any) { }
}


class Actions {
    static greet(name: string) {
        AppDispatcher.dispatch(new Payload('greet', "Hello, " + name));
    }
}

class State {
    constructor(public message: string) { }
}

class Store extends FluxUtils.ReduceStore<State> {
    constructor(d: Flux.Dispatcher<Payload>) {
        super(d);
    }

    getInitialState() {
        return new State('');
    }

    reduce(state: State, action: Payload) {
        switch (action.action) {
            case 'greet':
                return new State(action.data as string);
            default:
                throw Error('invalid operation');
        }
    }

}


const AppDispatcher = new Flux.Dispatcher<Payload>();
const AppStore = new Store(AppDispatcher);

interface AppState {
    message: string;
    name: string;
}

class App extends React.Component<{}, AppState> {
    private listenerSubscription: { remove: Function };

    constructor(props: {}) {
        super(props);
        this.state = { name: '', message: '' } as AppState;
    }

    private getState(): AppState {
        return {
            name: this.state.name,
            message: AppStore.getState().message
        };
    }

    private handleChange() {
        this.setState(this.getState());
    }

    private handleClick() {
        Actions.greet(this.state.name);
    }

    private handleNameChange(e: React.SyntheticEvent) {
        var name = (e.target as HTMLInputElement).value;
        this.setState({
            name: name
        } as AppState);
    }

    componentDidMount() {
        this.listenerSubscription = AppStore.addListener(this.handleChange.bind(this));
    }

    componentWillUnmount() {
        this.listenerSubscription.remove();
    }

    render() {
        return <div>
                <div>{this.state.message}</div>
                <input type='text' onChange={this.handleNameChange.bind(this)} />
                <button onClick={this.handleClick.bind(this)}>Click</button>
            </div>;
    }
}

ReactDOM.render(
    <App />,
    document.getElementById('content'));