Help us understand the problem. What is going on with this article?

関東最速でReact+Redux+TypeScriptなアプリの開発環境を作る

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

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

注2: React+Reduxはデフォルトの最小構成でもファイル数が多く複雑、かつTypeScriptで型を付けても完全に実行時エラーをゼロにするのは不可能です。よりアプリケーションの質を高めるためには、Reduxの起源となったElmの利用を検討してみてください→関東最速でElm+JSなアプリの開発環境を作る

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っしょ! という人もいるかもしれませんが、ここでは省略します。 

IzumiSy
Elmエンジニャー / https://dev.to/izumisy
https://izumisy.work
fringe81
Fringeは、最新のテクノロジーとプロフェッショナルによるサービスにより、社会課題に仮説を立てて市場に広げていくことで、数十年という長期的なスパンで価値を生み出し続け、より良い世界を創る集団です。 既存の領域に限らず、時流を読み、仮説を生み出し、テクノロジーの力で優れたサービスを生み出し続けます。
https://www.fringe81.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした