Reduxが難しい。理解できたらいいんだろうけど。 TypeScriptで開発するとき、何をどの順番で作っていけばすっきりするのかわからない。
ということで整理もかねて徒然なるままにBlogに向かいて書いてみたいと思います。
Actions
まず、何はともあれアクションです。Actionは、せっかくのTypeScriptなんでインターフェースを定義しておきたいところですね。
exportinterface Action<TPayload> { type: string; payload?: TPayload; error?: Error; }
Actionを識別するための定数ってどこに切るよ?actionTypes.tsというファイルを作ってその中でexport constをするのがセオリーっぽい気もするけど、TypeScriptだと、後々Actionのpayloadの型を定義するから、そこに含めてもいいかもしれない?こんな感じに。
exportdefaultclass 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) }; }exportfunction add(x: number, y: number) {return (dispatch: Redux.Dispatch) => { fetch('api/Calc?x=' + x + '&y=' + y) .then(x => {if (x.status !== 200) {thrownew Error(); }return x.json(); }).then((x: number) => { dispatch(setAnswer(x)); }).catch(_ => {}); }; }
んで、実践的な奴だとWebAPI呼び出すからこんな感じにredux-thunk使いますよね。
んで、このAction Creatorsを1ファイルにどれくらいまとめてしまうのか悩ましい。OOPでいうところの1クラスぶんを1ファイルにまとめてしまうくらいの感覚でいいのだろうか。
アプリのモデル
JavaScriptだとreducerを書いていけばいいんだけど、そこで使うデータモデルの設計しないといけないですよね。TypeScriptだとクラスの定義。どんなモデルが必要なのかさくっと定義するといいんでしょう。
exportdefaultclass 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); }exportdefaultfunction calcApp(calc = new Calc(), a: actions.Action<any>): Calc {switch (a.type) {case SetAnswerPayload.TYPE: if (a.error) {// TODOreturn 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'; exportinterface AppState { calc: Calc; }exportconst 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 }; }exportdefault 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の流れで作ればいいのか。メモメモ