かずきのBlog@hatena

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

Reduxってどうなってるんだってばよ

Reduxが難しい。理解できたらいいんだろうけど。 TypeScriptで開発するとき、何をどの順番で作っていけばすっきりするのかわからない。

ということで整理もかねて徒然なるままにBlogに向かいて書いてみたいと思います。

Actions

まず、何はともあれアクションです。Actionは、せっかくのTypeScriptなんでインターフェースを定義しておきたいところですね。

export interface Action<TPayload> {
    type: string;
    payload?: TPayload;
    error?: Error;
}

Actionを識別するための定数ってどこに切るよ?actionTypes.tsというファイルを作ってその中でexport constをするのがセオリーっぽい気もするけど、TypeScriptだと、後々Actionのpayloadの型を定義するから、そこに含めてもいいかもしれない?こんな感じに。

export default class SetAnswerPayload {
    static TYPE = 'SET_ANSWER_ACTION';

    constructor(public answer: number) { }
}

constじゃないのが嫌な感じだけど。まぁ書き換えないっしょ。

Action Creatorsどうしましょう?っていうかこれは普通に関数をexportするだけですよね?

import * as actions from './index';
import SetAnswerAction from './SetAnswerPayload';
import assign = require('object-assign');
import * as Redux from 'redux';

function setAnswer(answer: number): actions.Action<SetAnswerAction> {
    return {
        type: SetAnswerAction.TYPE,
        payload: new SetAnswerAction(answer)
    };
}

export function add(x: number, y: number) {
    return (dispatch: Redux.Dispatch) => {
        fetch('api/Calc?x=' + x + '&y=' + y)
            .then(x => {
                if (x.status !== 200) {
                    throw new Error();
                }
                return x.json();
            }).then((x: number) => {
                dispatch(setAnswer(x));
            }).catch(_ => {
                
            });
    };
}

んで、実践的な奴だとWebAPI呼び出すからこんな感じにredux-thunk使いますよね。

んで、このAction Creatorsを1ファイルにどれくらいまとめてしまうのか悩ましい。OOPでいうところの1クラスぶんを1ファイルにまとめてしまうくらいの感覚でいいのだろうか。

アプリのモデル

JavaScriptだとreducerを書いていけばいいんだけど、そこで使うデータモデルの設計しないといけないですよね。TypeScriptだとクラスの定義。どんなモデルが必要なのかさくっと定義するといいんでしょう。

export default class Calc {
    answer: number;
}

Reducers

んで、アクションをディスパッチするReducersですよね。このReducersで嫌いなswitch文の嵐。アクションで1ファイルにまとめたのと同じぶんだけ1ファイルにまとめたReducersを作ればいいのかしら。

import Calc from '../models/Calc';
import * as actions from '../actions';
import SetAnswerPayload from '../actions/SetAnswerPayload';
import assign = require('object-assign');

function setAnswer(calc: Calc, p: SetAnswerPayload) {
    return assign({}, calc, {
        answer: p.answer
    } as Calc);
}

export default function calcApp(calc = new Calc(), a: actions.Action<any>): Calc {
    switch (a.type) {
        case SetAnswerPayload.TYPE:
            if (a.error) {
                // TODO
                return calc;
            }
            return setAnswer(calc, a.payload as SetAnswerPayload);
        default:
            return calc;
    }
}

アプリのStoreの型定義

これをよく忘れる。(というか今作りながらやってて忘れてた)

reducerをまとめ上げるところで一緒に定義してやるといい感じだと思う。

import * as Redux from 'redux';
import CalcReducers from './CalcReducers';
import Calc from '../models/Calc';

export interface AppState {
    calc: Calc;
}

export const appReducer = Redux.combineReducers({
    calc: CalcReducers
});

コンポーネント

そしたら、コンポーネントを作ればいい? このコンポーネントもconnectしないといけないというのを忘れがち。

import * as React from 'react';
import * as Redux from 'redux';
import * as ReactRedux from 'react-redux';
import Calc from '../models/Calc';
import * as calcActionCreators from '../actions/calcActionCreators';
import * as reducers from '../reducers';

interface IndexPageProps extends React.Props<{}> {
    dispatch?: Redux.Dispatch;
    calc?: Calc;
}

interface IndexPageState {
}

class IndexPage extends React.Component<IndexPageProps, IndexPageState> {

    private handleSubmit(e: React.SyntheticEvent) {
        e.preventDefault();
        var x = parseInt((this.refs['x'] as HTMLInputElement).value);
        var y = parseInt((this.refs['y'] as HTMLInputElement).value);
        this.props.dispatch(calcActionCreators.add(x, y));
    }

    render() {
        return (
            <form onSubmit={this.handleSubmit.bind(this)}>
                <input type='text' ref='x' />
                &nbsp;
                +
                &nbsp;
                <input type='text' ref='y' />
                &nbsp;
                <input type='submit' value='=' />
                &nbsp;
                <span>{this.props.calc.answer}</span>
            </form>
        );
    }
}

function select(state: reducers.AppState): IndexPageProps {
    return {
        calc: state.calc
    };
}

export default ReactRedux.connect(select)(IndexPage);

まとめ上げよう

最後に、appクラス。 storeを作ってProviderに渡す。

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import * as Redux from 'redux';
import {Provider} from 'react-redux';
import IndexPage from './components/IndexPage';
import * as reducers from './reducers';
import thunk = require('redux-thunk');

class App extends React.Component<{}, {}> {
    render() {
        return (
            <div>
                <h1>Calc app</h1>
                <hr />
                <IndexPage />
            </div>
        );
    }
}

const createStoreWithMiddleware = Redux.applyMiddleware(thunk as any)(Redux.createStore);
const store = createStoreWithMiddleware(reducers.appReducer);

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('content'));

実行すると動いた。

まとめると

Actions → Action Creators → Models → Reducers → Store → Componentsの流れで一回作って、次からはActions → Action Creators → Model(必要に応じて) → Reducers → Store(必要に応じて) → Componentsの流れで作ればいいのか。メモメモ