Edited at

関東最速でReact+Redux+TypeScriptなアプリを作る

とりあえず関東最速でReactアプリをTypeScriptで作るためのレシピです

注: このサンプルではReduxを使いますが、もしImmutabilityを必要としないのであればUnduxを代替に検討してみてください! Reduxは関数型エッセンスを守るために非常にファイル数が多くなります。 →【Reduxに疲れた人のための】Undux入門


create-react-app

最速で作るにはcreate-react-app一択です。--typescriptオプションが利用できるので、それを使います


create-react-appを使って雛形を生成

$ npx create-react-app ts-react-app --typescript



redux

まずreduxをインストール


Reduxを導入

$ npm install --save redux react-redux


型定義ファイルを入れるのも忘れずに


react-reduxの型定義ファイルのインストール

$ npm install -D @types/react-redux



typescript-fsa

最速で作るにあたってReduxのボイラープレート・コードを書くのはダルすぎます。

なのでtypescript-fsaを入れましょう。


typescript-fsaを導入

$ npm install --save typescript-fsa typescript-fsa-reducers


これを使うことでFSA(Flux Standard Action)1にも準拠できます。楽ですね。

さて早速コードを書いていきましょう


Action実装

src/actionsでディレクトリを切ってこんな感じでActionを定義


hogeAction.ts

import actionCreatorFactory from 'typescript-fsa';

const actionCreator = actionCreatorFactory();

export const hogeActions = {
updateName: actionCreator<string>('ACTIONS_UPDATE_NAME'),
updateEmail: actionCreator<string>('ACTIONS_UPDATE_EMAIL')
};



Reducer実装

src/statesでディレクトリを切ってこんな感じでReducerを定義


hogeState.ts

import { reducerWithInitialState } from 'typescript-fsa-reducers';

import { hogeActions } from '../actions/hogeActions';

export interface HogeState {
name: string;
email: string;
}

const initialState: HogeState = {
name: '',
email: ''
};

export const hogeReducer = reducerWithInitialState(initialState)
.case(hogeActions.updateName, (state, name) => {
return Object.assign({}, state, { name });
})
.case(hogeActions.updateEmail, (state, email) => {
return Object.assign({}, state, { email });
});



Store実装

store.tsみたいなファイルをsrc直下にでも置いて、createStoreでストアを定義。


store.ts

import { createStore, combineReducers } from 'redux';

import { hogeReducer, HogeState } from './states/hogeState';

export type AppState = {
hoge: HogeState
};

const store = createStore(
combineReducers<AppState>({
hoge: hogeReducer
})
);

export default store;



コンポーネント実装

今回はPresentational/Containerコンポーネント2のパターンで実装を進めます。

PresentatonalとContainerは責務の観点から粗結合性を担保するためにビューをふたつのレイヤに分けるものです。具体的には以下のような違いがあります。

Container
Presentational

役割
振る舞い」を実装
見た目」を実装

状態
持つ
持たない

Reduxへの依存
ある
ない

すなわち、ContainerでReduxとの依存(ステートの状態やアクションの実行)などをラップし、ComponentはContainerによってラップされた振る舞いのみを知るだけでよくなります。


Container実装

src/containersでディレクトリを切ってこんな感じで実装


hogeContainer.ts

import { Action } from 'typescript-fsa';

import { Dispatch } from 'redux';
import { connect } from 'react-redux';
import { AppState } from '../store';
import { hogeActions } from '../actions/hogeActions';
import { HogeComponent } from '../components/hogeComponent';

export interface HogeActions {
updateName: (v: string) => Action<string>;
updateEmail: (v: string) => Action<string>;
}

function mapDispatchToProps(dispatch: Dispatch<Action<string>>) {
return {
updateName: (v: string) => dispatch(hogeActions.updateName(v)),
updateEmail: (v: string) => dispatch(hogeActions.updateEmail(v))
};
}

function mapStateToProps(appState: AppState) {
return Object.assign({}, appState.hoge);
}

export default connect(mapStateToProps, mapDispatchToProps)(HogeComponent);


mapDispatchToPropsの中でbindActionCreatorsを使うのをやめましょう。それを使ってしまっては直接コンポーネントのなかでdispatchを呼んでいるのと同じで、Containerレイヤを置く意味がありません。もしそうしたいのであれば、Mobxなどを使ったほうがよいです。


Component実装

Presentationalなコンポーネントには状態を与えるのをやめましょう。ロジックはContainer側で定義すべきです。

コードはsrc/componentsみたいなディレクトリを切ってそこへ入れましょう。ここは原則として状態を持たない場所なので、React.SFCを使うのが適切です。


hogeComponent.tsx

import * as React from 'react';

import { HogeState } from '../states/hogeState';
import { HogeActions } from '../containers/hogeContainer';

interface OwnProps {}

type HogeProps = OwnProps & HogeState & HogeActions;

export const HogeComponent: React.SFC<HogeProps> = (props: HogeProps) => {
return (
<div>
<div className="field">
<input
type="text"
placeholder="name"
value={props.name}
onChange={(e) => props.updateName(e.target.value)}
/>
</div>
<div className="field">
<input
type="email"
placeholder="email"
value={props.email}
onChange={(e) => props.updateEmail(e.target.value)}
/>
</div>
</div>
);
};



App.tsxへの追加

src以下にApp.tsxというファイルがあるので、そこでHogeComponentを表示するようにしましょう


App.tsx

import * as React from 'react';

import HogeContainer from '../src/containers/hogeContainer';

class App extends React.Component {
render() {
return (
<div className="App">
<HogeContainer />
</div>
);
}
}

export default App;



index.tsxの更新

src以下にあるindex.tsxが実際にReactのrootコンポーネントをマウントしているところです。

最後にここでstoreを使うように書き換えましょう。


index.tsx

import * as React from 'react';

import * as ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import Store from './store';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import './index.css';

ReactDOM.render(
<Provider store={Store}>
<App />
</Provider>,
document.getElementById('root') as HTMLElement
);

registerServiceWorker();



起動

あとは以下のコマンドで起動します。

$ npm start

(tslintでエラーが出る場合にはコメント欄にある@manomuさんのコメントを参考にしてみてください)


【発展編】非同期処理したい

以上であらかたのアプリケーションの骨組みは作れたものの、もっとちゃんとしたアプリケーション開発ではAPIを叩いたりしますよね。

typescript-fsaを使っているので、Actionは単なるステート更新の起点としての責務に徹し、APIを叩く処理はコンポーネントに対して振る舞いを定義するContainerの中3に置きます。


非同期Actionの定義

typescript-fsaに用意されているactionCreator.asyncというメソッドを使うと、内部でSTARTED, DONE, FAILEDというtypeサフィックスをつけたActionの定義を行ってくれます。

import actionCreatorFactory, { ActionCreator, Success, Failure } from 'typescript-fsa';

const actionCreator = actionCreatorFactory();

const submit =
actionCreator.async<{}, {}, {}>('ACTIONS_SUBMIT')

export interface HogeAsyncActions {
startLogin: ActionCreator<{}>;
failedLogin: ActionCreator<Failure<{}, {}>>;
doneLogin: ActionCreator<Success<{}, {}>>;
}

export const hogeAsyncActions = {
startLogin: submit.started,
failedLogin: submit.failed,
doneLogin: submit.done
}


Reducerの定義

ReducerはActionで定義された3つのステートを処理するcaseを作るだけです。


hogeAsyncState.ts

import { reducerWithInitialState } from 'typescript-fsa-reducers';

import { hogeAsyncActions } from '../actions/hogeAsyncActions';

// ...

export const hogeAsyncReducers = reducerWithInitialState(initialState)
.case(hogeAsyncActions.startLogin, (state) => {
// ...
})
.case(hogeAsyncActions.failedLogin, (state) => {
// ...
})
.case(hogeAsyncActions.doneLogin, (state) => {
// ...
})



ContainerでのAPIアクセス処理の実装

上ですでに実装したContainer内のmapDispatchToPropsで以下のようにAPIアクセスを行う一連の処理を実装します。


hogeContainer.ts

// ...

import { hogeAsyncActions } from '../actions/hogeAsyncActions';

// ...

function mapDispatchToProps(dispatch: Dispatch<void>) {
return {
submit() {
dispatch(hogeAsyncActions.startLogin({}));

callYouOwnAPI()
.then(() => {
dispatch(hogeAsyncActions.doneLogin({
params: {}, result: {}
}));
})
.catch(() => {
dispatch(hogeAsyncActions.failedLogin({
params: {}, error: {}
}));
});
},

// ...
};
}


あとはこれをコンポーネントの中から呼び出すだけです。


tslintの設定

tslint.jsonに以下を追加

"rules": {

"interface-name": [
false
],
"object-literal-sort-keys": false,
"ordered-imports": false,
"no-empty-interface": false,
"interface-over-type-literal": false
}





  1. https://github.com/acdlite/flux-standard-action 



  2. https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0 



  3. いやいや、そこはredux-sagaっしょ! という人もいるかもしれませんが、ここでは省略します。