LoginSignup
322
267

More than 3 years have passed since last update.

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

Last updated at Posted at 2017-12-24

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

322
267
9

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
322
267