かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

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'));