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' /> + <input type='text' ref='y' /> <input type='submit' value='=' /> <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の流れで作ればいいのか。メモメモ