【React】react × redux × material-ui × typescript いまどきのWebアプリの作り方

# 背景
Reactを使ってWebアプリを開発していく中で、ある程度作り方が自分の中で固まってきたので、この記事を書くことにしました。

# 概要
~~ReactのComponentは、classを使用せず**関数化SFC(Stateless Function Component)化させることで、パフォーマンス向上、ロジックとビューの分離、可読性の向上を図ることができます~~。

~~またComponentを関数化SFC(Stateless Function Component)化してしまうと、ライフサイクルメソッドを使えなくなります。
こちらはHoC(React Higher-order Component)という、Componentを引数に、機能を追加したComponentを返す関数を利用することでライフサイクルメソッドの問題は解決できます~~。

→**ReactのComponentは、classを使用せずHooksを利用してFC(Function Component)化させることでパフォーマンス向上、ロジックとビューの分離、可読性の向上を図ることができます**。(2019/3/10 追記)

※HoCを利用する必要はなくなりました。

ActionとReducerはFSA(Flux Standard Action)のコーディング規則に沿って実装をします。ただし、**FSAライブラリを使用することでコーディング量をできる限り削減し、可読性向上を図った書き方ができる**のでそちらをご紹介します。

※_加えて、**タスクという概念を加えることで、Actionをビューで使用するActionとReducerで使用するActionに切り分けてメンテ性を向上させる手法**が好きなのだが、ここでは割愛します。_

# 対象者
reactとreduxを使ってReactアプリを作れる方
typescriptがわかる方
material-uiの使い方をある程度知っている方


# 環境構築
まずは`create-react-app`で、**typescript用のReactアプリ**を作成します。

```bash
$ npm install create-react-app -g
$ create-react-app my-app --scripts-version=react-scripts-ts
```

my-appというフォルダが作成されているはずです。
my-appに移動して`npm start`で、**Reactアプリ**が起動するか確認します。

次に、必要なライブラリをインストールします。

| Library                 | version       |
|:------------------------|:--------------|
| redux                   | v4.0.1        |
| react-redux             | v6.0.0        |
| recompose               | v0.30.0       |
| typescript-fsa          | v3.0.0-beta-2 |
| typescript-fsa-reducers | v1.1.0        |
| @material-ui/core       | v3.6.1        |
| @material-ui/icons      | v3.0.1        |


※_本当はredux-sagaも導入したいが、好みが分かれると思うのでそちらは別の記事で説明します。_

```bash
npm install redux --save
npm install react-redux --save
npm install recompose --save
npm install typescript-fsa --save
npm install typescript-fsa-reducers --save
npm install @material-ui/core --save
npm install @material-ui/icons --save
```

ライブラリが**typescript**で動くように、typescript用のライブラリもインストールします。

| Library            | version |
|:-------------------|:--------|
| @types/redux       | v3.6.0  |
| @types/react-redux | v6.0.10 |
| @types/recompose   | v0.30.0 |

```bash
npm install @types/redux --save-dev
npm install @types/react-redux --save-dev
```

最後にtslint.jsonの設定を変えます。
my-app直下にtslint.jsonがあるので、以下のようにルールを設定してください。ルールに関してはわからない方はgoogleで検索するなりして、理解しておいてください。

```json:tslint.json
{
  "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
  "linterOptions": {
    "exclude": [
      "config/**/*.js",
      "node_modules/**/*.ts",
      "coverage/lcov-report/*.js"
    ]
  },
  "rules": {
    "variable-name": [
      true,
      "ban-keywords",
      "check-format",
      "allow-pascal-case",
      "allow-leading-underscore"
    ],
    "no-console": [false],
    "import-name": false,
    "ordered-imports": false,
    "interface-name": false,
    "no-empty-interface": false,
    "object-literal-sort-keys": false,
    "object-literal-shorthand": false,
    "jsx-no-lambda": false,
    "linebreak-style": false
  }
}
```

# 手順

### フォルダの作成

まず「components」、「containers」、「actions」、「reducers」、「helpers」フォルダを作成します。

```
my-app\src
├── actions
├── App.css
├── App.test.tsx
├── App.tsx
├── components
├── containers
├── helpers
├── index.css
├── index.tsx
├── logo.svg
├── reducers
└── registerServiceWorker.ts

```

### Componentの作成

まずは、components内にhome.tsxというComponentを作成します。

```tsx:home.tsx(components)
import * as React from 'react'
import { WithStyles, withStyles, createStyles } from '@material-ui/core'
import { Theme } from '@material-ui/core/styles/createMuiTheme'
import AppBar from '@material-ui/core/AppBar'
import Toolbar from '@material-ui/core/Toolbar'
import Typography from '@material-ui/core/Typography'
import Button from '@material-ui/core/Button'

interface OwnProps {}

const styles = (theme: Theme) => {
    return createStyles({
        layout: {
            marginTop: 100,
            padding: theme.spacing.unit * 2,
            maxWidth: 1200,
            [theme.breakpoints.up(1200)]: {
                marginLeft: 'auto',
                marginRight: 'auto',
            },
        },
  })
}

type Props = OwnProps & WithStyles<typeof styles>

const HomeComponent: React.SFC<Props> = (props: Props) => {
  const { classes } = props

  return (
    <div>
        <AppBar>
            <Toolbar>
                <Typography color='inherit'>Hello World</Typography>
            </Toolbar>
        </AppBar>
      <div className={classes.layout}>
        <Typography>this is home component</Typography>
        <Button>Open</Button>
      </div>
    </div>
  )
}

export default withStyles(styles)(HomeComponent) 
```

いつも何も考えず使っているであろう`props`はtypescriptでは定義してあげないと使えません。なので`Props`という型を作成します。

またmaterial-uiのレイアウト変えるstylesは`createStyles`を使用して作成し、`WithStyles`を使用して型を`Props`に渡します。

意外とtypescriptでmaterial-uiを使用する際、`styles`の指定の仕方が書いてある記事がないです。詳しい記事があればリンクを張りたいので教えて下さい。

※_`withStyles`ではなく、ここで使用するのは`WithStyles`です。_

```tsx

interface OwnProps {}

const styles = (theme: Theme) => {
    return createStyles({
        layout: {
            marginTop: 100,
            padding: theme.spacing.unit * 2,
            maxWidth: 1200,
            [theme.breakpoints.up(1200)]: {
                marginLeft: 'auto',
                marginRight: 'auto',
            },
        },
  })
}

type Props = OwnProps & WithStyles<typeof styles>
```

今回は**SFC(Stateless Function Component)のコンポーネントを作成するので、`class`は使用しません**。

Reactは**SFC(Stateless Function Component)を作成するAPIを用意してくれています**。

先ほど定義した`Props`を型に利用して、`props`を引数にとる`HomeComponent`を書きます。

```tsx
const HomeComponent: React.SFC<Props> = (props: Props) => {
    const { classes } = props

    return (
        // Componentの中身
    )
}
```

最後はいつも通り、material-uiお決まりの`withStyles`を使用して`default export`しておきます。

```tsx
export default withStyles(styles)(HomeComponent)
```

### HoC(React Higher-order Component)の作成

Componentのライフサイクルメソッドを利用できるようHoC(React Higher-order Component)を作成します。

**HoC(React Higher-order Component)はComponentを引数にして、機能を追加したComponentを返す関数**のことです。

helpers内にhocHello.tsというHoCを作成します。

**recomposeの`lifecycle`を利用することで、classを使用せず、Componentのライフサイクルメソッドを使用できるようになります**。

```ts:hocHello.ts(helpers)
import * as React from 'react'
import { lifecycle } from 'recompose'

export const hocFactory = (component: React.SFC) => {
    const wrappedComponent = lifecycle({
        componentDidMount() {
            console.log('Hello')
        },
    })(component);

    return wrappedComponent
}

```

components内のhome.tsxを`hocFactory`でHoCします。

```tsx:home.tsx(components)
import * as React from 'react'
import { WithStyles, withStyles, createStyles } from '@material-ui/core'
import { Theme } from '@material-ui/core/styles/createMuiTheme'
import AppBar from '@material-ui/core/AppBar'
import Toolbar from '@material-ui/core/Toolbar'
import Typography from '@material-ui/core/Typography'
import Button from '@material-ui/core/Button'

import { hocFactory } from '../helpers/hocHello' // 追加

interface OwnProps {}

const styles = (theme: Theme) => {
    return createStyles({
        layout: {
            marginTop: 100,
            padding: theme.spacing.unit * 2,
            maxWidth: 1200,
            [theme.breakpoints.up(1200)]: {
                marginLeft: 'auto',
                marginRight: 'auto',
            },
        },
  })
}

type Props = OwnProps & WithStyles<typeof styles>

const HomeComponent: React.SFC<Props> = (props: Props) => {
  const { classes } = props

  return (
    <div>
        <AppBar>
            <Toolbar>
                <Typography color='inherit'>Hello World</Typography>
            </Toolbar>
        </AppBar>
      <div className={classes.layout}>
        <Typography>this is home component</Typography>
        <Button>Open</Button>
      </div>
    </div>
  )
}

export default withStyles(styles)(hocFactory(HomeComponent)) // 修正

```

### ActionとReducerの作成

typescript-fsaとtypescript-fsa-reducersを利用して、actionとreducerを作成していきます。

#### Actionについて

`actions`内に`CardAction.ts`というActionを作成します。

**typescript-fsaの`actionCreatorFactory`を利用することで、簡潔にFSA(Flux Standard Action)に準じたActionを作成できます**。

※_FSA(Flux Standard Action)について[詳しくはこちら](https://redux-actions.js.org/)_

```ts:CardAction.ts(actions)
import actionCreatorFactory from 'typescript-fsa'

const actionCreator = actionCreatorFactory()

export const CardActions = {
  open: actionCreator<void>('USER_OPEN_CLICK'),
  close: actionCreator<void>('USER_CLOSE_CLICK'),
}

```

#### Reducerについて

reducers内にcardReducer.tsというReducerを作成します。

**typescript-fsa-reducersの`reducerWithInitialState`を利用することで、簡単にtypescript-fsa**で作成したActionに対応するReducerを作成できます**。

```ts:cardReducer.ts(reducers)
import { reducerWithInitialState } from 'typescript-fsa-reducers'
import { CardActions } from '../actions/CardAction'

export interface CardState {
  open: boolean
}

const initialState: CardState = {
  open: false,
}

export const CardReducer = reducerWithInitialState(initialState)
.case(CardActions.open, (state, payload) =>
  Object.assign({}, state, { open: true })
)
.case(CardActions.close, (state, payload) =>
  Object.assign({}, state, { open: false })
)

```

### Storeの作成と結合

ActionとReducerができたのでStoreを作成します。

src直下にstore.tsというStoreを作成します。

```ts:store.ts
import { createStore, combineReducers } from 'redux'
import { CardReducer, CardState } from './reducers/cardReducer'

export interface AppState {
  card: CardState
}

const configureStore = () => {
  const _store = createStore(
    combineReducers<AppState>({
      card: CardReducer,
    }),
    {},
  )
  
  return _store
}

const store = configureStore()

export default store
```

cardReducerで型を定義した`CardState`、を読み込み、AppStateの型定義に利用します。

※_actionとreducerが増えれば、`AppState`に型を追加していけばよいです。_

```ts
export interface AppState {
  card: CardState
}
```

`configureStore`という関数を作成し、reduxの`createStore`と`combineReducers`を使用して、`_store`を返すように書きます。

※_actionとreducerが増えれば、`combineReducers`にreducerを追加していけばよいです。_

```ts
const configureStore = () => {
  const _store = createStore(
    combineReducers<AppState>({
      card: CardReducer,
    }),
    {},
  )

  return _store
}
```

最後に、configureStoreを実行させ、`store`を`export default`しておきます。

```ts
const store = configureStore()

export default store
```

次に、作成した`store`を結合します。
すでにあるindex.tsxを編集します。

storeを読み込み、react-reduxの`Provider`を使用して結合します。

```tsx:index.tsx
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { Provider } from 'react-redux' // 追加
import App from './App'
import './index.css'
import registerServiceWorker from './registerServiceWorker'

import store from './store' // 追加


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

```

### Containerの作成

containers内にhome.tsというContainerを作成します。

```ts:home.ts(containers)
import { Action } from 'typescript-fsa'
import { Dispatch } from 'redux'
import { connect } from 'react-redux'
import { AppState } from '../store'
import { CardActions } from '../actions/CardAction'
import HomeComponent from '../components/home'

export interface HomeActions {
  handleOpen: () => Action<void>
  handleClose: () => Action<void>
}

const mapDispatchToProps = (dispatch: Dispatch) => {
  return {
    handleOpen: () => dispatch(CardActions.open()),
    handleClose: () => dispatch(CardActions.close()),
  }
}

const mapStateToProps = (appState: AppState) => {
  return Object.assign({}, appState)
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(HomeComponent)

```

`HomeComponent`で`handleOpen`、`handleClose`という関数を扱えるようにするための型定義と、対応する`dispatch`を`mapDispatchToProps`を書きます。

```ts
export interface HomeActions {
  handleOpen: () => Action<void>
  handleClose: () => Action<void>
}

const mapDispatchToProps = (dispatch: Dispatch) => {
  return {
    handleOpen: () => dispatch(CardActions.open()),
    handleClose: () => dispatch(CardActions.close()),
  }
}
```

`AppState`の値をHomeComponentで読み込めるように`mapStateToProps`を書きます。

```ts

const mapStateToProps = (appState: AppState) => {
  return Object.assign({}, appState)
}
```

あとはお決まりのreact-reduxの`connect`で`mapDispatchToProps`と`mapStateToProps`を`HomeComponent`とつなげます。

```ts
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(HomeComponent)
```

### Componentの修正

`AppState`とContainerの関数を呼び出せるように、home.tsxを修正します。そして、`AppState`によって表示が切り替わるCardを追加します。

```tsx:home.tsx
import * as React from 'react'
import { WithStyles, withStyles, createStyles } from '@material-ui/core'
import { Theme } from '@material-ui/core/styles/createMuiTheme'
import AppBar from '@material-ui/core/AppBar'
import Toolbar from '@material-ui/core/Toolbar'
import Typography from '@material-ui/core/Typography'
import Button from '@material-ui/core/Button'
import Grow from '@material-ui/core/Grow' // 追加
import Card from '@material-ui/core/Card' // 追加
import CardContent from '@material-ui/core/CardContent' // 追加
import CardActions from '@material-ui/core/CardActions' // 追加

import { HomeActions } from '../containers/home' // 追加
import { AppState } from '../store' // 追加

import { hocFactory } from '../helpers/hocHello'

interface OwnProps {}

const styles = (theme: Theme) => {
    return createStyles({
        layout: {
            marginTop: 100,
            padding: theme.spacing.unit * 2,
            maxWidth: 1200,
            [theme.breakpoints.up(1200)]: {
                marginLeft: 'auto',
                marginRight: 'auto',
            },
        },
        // 追加
        card: {
            width: 275,
            height: 300,
        }
  })
}

// 修正
type Props = OwnProps & AppState & HomeActions & WithStyles<typeof styles>

const HomeComponent: React.SFC<Props> = (props: Props) => {
  const { classes } = props

  return (
    <div>
        <AppBar>
            <Toolbar>
                <Typography color='inherit'>Hello World</Typography>
            </Toolbar>
        </AppBar>
      <div className={classes.layout}>
        <Typography>this is home component</Typography>
        <Button onClick={props.handleOpen}>Open</Button> {/* 修正 */}

        {/* Card 追加 */}
        <Grow in={props.card.open}>
            <Card className={classes.card}>
                <CardContent>
                    <Typography>this is card.</Typography>
                </CardContent>
                <CardActions>
                    <Button onClick={props.handleClose}>Close</Button>
                </CardActions>
            </Card>
        </Grow>
      </div>
    </div>
  )
}

export default withStyles(styles)(hocFactory(HomeComponent))

```

### Homeの表示

すでにある`App.tsx`を修正します。

```ts:App.tsx
import * as React from 'react'
import Home from './containers/home'

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

export default App

```

`npm start`でReactアプリを立ち上げてみましょう。下のような画面が出るはずです。

※_typescriptのバリーデーションのエラーで弾かれると思いますが、いわれた通り修正しちゃいましょう。_

<img width="447" alt="キャプチャ.JPG" src="https://qiita-image-store.s3.amazonaws.com/0/189146/b9e2ab30-6906-efb4-5399-e2f368d6babf.jpeg">

# 最後に
typescriptは正直わかりませんが、**ComponentのSFC(Stateless Function Component)化、FSA(Flux Standard Action)に準拠したActionは今後当たり前になってくると思います**ので(_もう当たり前かもしれない_)、ぜひ使えるようにしておきましょう。

全体のソースコード公開しておきます。
[https://github.com/kousaku-maron/react-sample-for-qiita](https://github.com/kousaku-maron/react-sample-for-qiita)