読者です 読者をやめる 読者になる 読者になる

かずきのBlog@hatena

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

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の流れで作ればいいのか。メモメモ