React + Reduxを使ったWebアプリケーション開発速習会＠Wantedly


[React + Reduxを使ったWebアプリケーション開発速習会＠Wantedly](http://wantedly.connpass.com/event/33168/) のための資料です。

WantedlyではSingle Page Applicationなどより複雑な構成にも耐えられるよう、React + Reduxを中心としたWebフロントエンドの技術スタックを導入しました。

* ES2015
* React
* Redux
* Immutable.js
* CSS Modules
* webpack

導入の経緯などは以前発表した :point_down: の資料を参照ください!

[WantedlyにReact + Reduxを導入した話](https://speakerdeck.com/shimpeiws/wantedlynireact-plus-reduxwodao-ru-sitahua)

今回はこれらのスタックを使って実際にSingle Page Application構成のアプリケーションをハンズオンで作成します。

## :package: 開発の準備 :package: 

### 開発環境

npmライブラリを使用するため、開発マシンにnode.jsがインストール済みであることを前提としています。

サンプルはv5.1.1で動作確認しています。

```
% node -v                                                                                                                                                                                        
v5.1.1
```

### 今回使うソース

[shimpeiws/react-redux-sokushu-practice](https://github.com/shimpeiws/react-redux-sokushu-practice)

GitHub上の :point_up: のソースをgit cloneした後に、ディレクトリ直下で以下を実行してください。

```
npm install
npm run webpack:serve
```

もうひとつターミナルのペインを開き、ディレクトリ直下に移動し、以下を実行してください。

```
npm run serve
```


`localhost:8000`にアクセスして画面が表示されれば準備は完了です。

![スクリーンショット 2016-06-09 14.31.23.png](https://qiita-image-store.s3.amazonaws.com/0/29637/095942c1-0ab9-41d9-4af0-145af9df488f.png "スクリーンショット 2016-06-09 14.31.23.png")

### 完成したサンプル

演習の内容を実装済みのリポジトリです :point_down: 

演習でつまったときや、動作の確認に使ってください。

[shimpeiws/react-redux-sokushu](https://github.com/shimpeiws/react-redux-sokushu)

起動方法は同じですが、こちらは`localhost:9000`で起動するようになっています。

### 今回の課題内容

GitHubのIssue機能を模したアプリケーションを作成します。

"一覧画面" "詳細画面" "Issue新規作成画面" の3画面で構成されています。

#### 一覧画面

Issueのデータを一覧で表示しています。行をクリックすることで詳細画面に遷移します。

Issueのstatusなど各種フィルタが実装されます。

![スクリーンショット 2016-06-09 18.38.44.png](https://qiita-image-store.s3.amazonaws.com/0/29637/6cc39fc7-b2b2-2a1d-c87d-20de317e573d.png "スクリーンショット 2016-06-09 18.38.44.png")


#### 詳細画面

Issueに対してアサインやラベル、コメントを追加することができます。
![スクリーンショット 2016-06-09 18.38.25.png](https://qiita-image-store.s3.amazonaws.com/0/29637/b2cc9b54-ac15-3523-7aad-c9e243b9659f.png "スクリーンショット 2016-06-09 18.38.25.png")


#### 新規Issue作成画面

Issueを新規追加することができます、titleとcontentは必須欄です。

![スクリーンショット 2016-06-09 18.36.52.png](https://qiita-image-store.s3.amazonaws.com/0/29637/eb14ae0f-5e29-469a-7e1f-7c6bef5e061d.png "スクリーンショット 2016-06-09 18.36.52.png")

### 使用するAPI

[shimpeiws/react-redux-sokushu-api](https://github.com/shimpeiws/react-redux-sokushu-api)

[APIドキュメント](http://shimpeiws.github.io/react-redux-sokushu-api/)

![スクリーンショット 2016-06-13 10.05.41.png](https://qiita-image-store.s3.amazonaws.com/0/29637/74d60193-53ec-2ace-4d86-7650a2edfbe1.png "スクリーンショット 2016-06-13 10.05.41.png")


演習中はherokuにデプロイされたものを使います。

### ディレクトリ構成

#### ディレクトリ直下

```
% tree -L 1                                                                                                                                                                                      [14:34:45]
.
├── .babelrc
├── finished
├── node_modules
├── package.json
├── public
├── server-finished.babel.js
├── server.babel.js
├── src
├── webpack-dev-server.babel.js
└── webpack.config.babel.js

4 directories, 5 files
```

* .babelrc
	* babelの設定ファイルが記述されています。基本的なものに加え、state-0の機能が利用可能になっています。
* package.json
	* 使用しているライブラリや、サーバの起動などのスクリプトが記述されています
* public
	* このフォルダ配下が`localhost:8000`としてマウントされます
* server.babel.js
	* public配下を`localhost:8000`で起動します。node(Express)を使っています
* src
	* 実際に開発するソースです
* webpack.config.babel.js
	* webpack dev serverからコンパイルされたアセットを配信するための設定が書かれています

#### /src 配下

```
% tree -L 3                                                                                [19:12:48]
.
├── actions
│   ├── issue.js
│   ├── issueDetail.js
│   └── issueNew.js
├── components
│   ├── IssueCommentForm.js
│   ├── IssueCommentForm.scss
│   ├── IssueCommentList.js
│   ├── IssueCommentList.scss
│   ├── IssueCommentListItem.js
│   ├── IssueCommentListItem.scss
│   ├── IssueDescription.js
│   ├── IssueDescription.scss
│   ├── IssueDetailHeader.js
│   ├── IssueDetailHeader.scss
│   ├── IssueList.js
│   ├── IssueList.scss
│   ├── IssueListHeader.js
│   ├── IssueListHeader.scss
│   ├── IssueListItem.js
│   ├── IssueListItem.scss
│   ├── IssueNewHeader.js
│   └── IssueNewHeader.scss
├── containers
│   ├── IssueContainer.js
│   ├── IssueDetailContainer.js
│   ├── IssueDetailContainer.scss
│   ├── IssueListContainer.js
│   ├── IssueListContainer.scss
│   ├── IssueNewContainer.js
│   └── IssueNewContainer.scss
├── entries
│   └── issue.js
├── lib
│   ├── constants
│   │   └── EndPoints.js
│   ├── records
│   │   ├── Comment.js
│   │   ├── Issue.js
│   │   ├── IssueDetailManager.js
│   │   ├── IssueListManager.js
│   │   ├── IssueManager.js
│   │   ├── IssueNewManager.js
│   │   ├── Label.js
│   │   └── User.js
│   └── utils
│       └── nl2br.js
├── reducers
│   ├── issue.js
│   └── issueApp.js
└── stores
    └── configureIssueStore.js

10 directories, 42 files
```

Fluxフレームワークとして、Reduxを使うので、Reduxの構成に沿ったディレクトリ構成になっています。

以下の図がReduxの構成を理解しやすいです。(出典: [UNIDIRECTIONAL USER INTERFACE ARCHITECTURES](http://staltz.com/unidirectional-user-interface-architectures.html))

![Redux](http://staltz.com/img/redux-unidir-ui-arch.jpg)

* actions
	* ReduxのActionsにあたる部分を担当します。今回はこの層にAPI通信やビジネスロジックもおいているため、主要なロジックはここに集まります。
* components
	* Reactコンポーネントと、CSS Modulesで使用するsassのファイルを配置しています。
	* [Presentational and Container Components](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.j48axzeyq)などの設計議論をふまえて、このフォルダ配下はActionを発行せず、"受け取ったpropsからの描画" "ユーザ入力を受け付ける" の2点にフォーカスします。
* containers
	* Reactコンポーネントですが、`components`配下とは逆に極力画面要素の描画はさけ、"components配下で発生したイベントのハンドリング" -> "Actionのコール" に専念します。
* entries
	* webpackはこのファイルを起点にビルドをかけます。ここにルーティングに関する記述があります。
* lib/records
	* [immutable.js](https://facebook.github.io/immutable-js/)のRecord型を使い、独自にモデルクラスを作成しています。
* reducers
	* ReduxのReducersにあたる部分です。実際にStateを更新します。
* stores
	* stateをまとめて保持する層です。初期設定項目のみで、開発時に触ることはないはずです。

## :notebook: 簡単にソースを追いながら概要説明 :notebook: 

サンプルソースを追えるようになるために、**詳細画面のタイトル編集**の部分を例にソースをおってみます。

![スクリーンショット 2016-06-09 18.40.59.png](https://qiita-image-store.s3.amazonaws.com/0/29637/64d675bc-4281-83a3-08da-95c0a3f585d1.png "スクリーンショット 2016-06-09 18.40.59.png")

### src/entries/issue.js

初期設定が多いですが、開発時にはルーティングに関する以下記述が重要です。

[reactjs/react-router-redux](https://github.com/reactjs/react-router-redux) と [reactjs/react-router](https://github.com/reactjs/react-router)を使っています。

`/`をルートとして、`/new`と`/:id`のパスがあることが分かります。それぞれのパスにアクセスされた時に対応する`component`がマウントされます。

```src/entries/issue.js
ReactDOM.render(
  <Provider store={store}>
    <Router history={history}>
      <Route path="/" component={IssueContainer}>
        <IndexRoute component={IssueListContainer} />
        <Route path="/new" component={IssueNewContainer} />
        <Route path="/:id" component={IssueDetailContainer} />
      </Route>
    </Router>
  </Provider>,
  document.getElementById('content')
)
```

### src/containers/IssueDetailContainer.js

詳細画面の画面を作成しています。

renderメソッドで必要なコンポーネントを描画しています。

```src/containers/IssueDetailContainer.js
render() {
  const { issueDetail, issueDetailManager } = this.props
  return (
    <div className={styles.base}>
      <Link to="/">List Page</Link>
      <Loader loaded={!issueDetailManager.loading}>
        <IssueDetailHeader
          issue={issueDetail}
          isTitleEditing={issueDetailManager.isTitleEditing}
          onClickTitleEdit={this.onClickTitleEdit.bind(this)}
          onClickTitleSave={this.onClickTitleSave.bind(this)}
        />
        <div className={styles.main}>
          <IssueDescription
            issue={issueDetail}
          />
          <IssueCommentList
            comments={issueDetail.comments}
            onClickSave={this.onClickCommentSave.bind(this)}
            onClickDelete={this.onClickCommentDelete.bind(this)}
          />
          <IssueCommentForm
            issue={issueDetail}
            onClickComment={this.onClickCommentSave.bind(this)}
            onClickChangeStatus={this.onClickChangeStatus.bind(this)}
          />
        </div>
      </Loader>
    </div>
  )
}
```

例えばタイトルの編集後、Saveボタンを押したタイミングでは、このコンポーネント内の`onClickTitleSave`メソッドがコールバックされます。

```src/containers/IssueDetailContainer.js
onClickTitleSave(issue) {
  this.props.changeTitleEditing(false)
  this.props.updateIssue(issue)
}
```

あわせて、`IssueCommentForm `の方も見てみす。

`Save`の文言を囲む`div`タグに、`onClick`のコールバックが設定され、`onClickTitleSave`メソッドをコールしています。

`onClickTitleSave` は編集中の状態から新たに `Issue` モデルを作成して、上位の階層に`this.props.onClickTitleSave`コールバックで引き渡しています。


```src/components/IssueDetailHeader.js
onClickTitleSave() {
  const newIssue = this.props.issue.set('title', this.state.title)
  this.props.onClickTitleSave(newIssue)
}

render() {
  const { issue } = this.props
  return (
    <div styleName="base">
      <div styleName="title-wrapper">
        { this.props.isTitleEditing ? (<div>
            <div styleName="title">
              <input
                type="text"
                value={this.state.title}
                onChange={this.onChangeTitle.bind(this)}
              />
              <div
                styleName="edit-button"
                onClick={this.onClickTitleSave.bind(this)}
              >
                Save
              </div>
            </div>
          </div>) : (
          <span>
            <div styleName="title">
              {issue.title}
            </div>
            <div styleName="edit-button" onClick={this.props.onClickTitleEdit}>
              Edit
            </div>
          </span>
          )
        }
      </div>
      // 以下略
}
```

ここで`this.props.XXX`は`src/actions/issueDetail.js#XXX`をコールされます。以下の部分でActionとこのコンポーネントが接続されているためです。

```src/containers/IssueDetailContainer.js
const mapDispatchToProps = (dispatch) => {
  return bindActionCreators({
    findIssueDetail,
    addComment,
    updateComment,
    deleteComment,
    changeTitleEditing,
    updateIssue,
  }, dispatch)
}
```

### src/actions/issueDetail.js

Actionの `updateIssue` メソッドを見ていきます。Issue更新のAPIリクエストを投げ、画面を更新する部分です。

* `dispatch(setIssueDetail(issueDetail))` -> `setIssueDetail`のアクションをdispatchしている、ここからReducerに処理が続きます
* `updateIssueRequest(issueDetail)` -> Issue更新のAPIリクエストです

```src/actions/issueDetail.js
async function updateIssueRequest(issue) {
  const data = {
    issue: {
      id: issue.id,
      title: issue.title,
      status: issue.status,
    }
  }

  const response = await $.ajax({
    url: `${END_POINTS.ISSUES}/${issue.id}`,
    method: 'PATCH',
    data,
    timeout: 100000,
  })

  return initIssueDetail(response)
}

function setIssueDetail(issueDetail) {
  return {
    type: Actions.SET_ISSUE_DETAIL,
    issueDetail,
  }
}

export function updateIssue(issueDetail) {
  return async(dispatch) => {
    dispatch(setIssueDetail(issueDetail))
    try {
      await updateIssueRequest(issueDetail)
    } catch (error) {
      console.log("error", error)
    }
  }
}
```

#### redux-thunkによる非同期処理

`return async(dispatch) => {}`で囲われているのは、Reduxで非同期処理を扱う[redux-thunk](https://github.com/gaearon/redux-thunk) middle wareの作法です。Timerや通信処理を含む場合に必要です。

#### async/await

ES2015のstage-3の機能です。Promiseよりもよりシンプルに非同期処理を書くことができます。エラー処理は`try ~ catch`で行い、Ajaxリクエストの場合、ステータス200以外の場合にcatchされます。


### src/reducers/issue.js

dispatchされたActionは全てのReducerに対して送信されます。dispatchした時のキー(今回だと`type`)で分岐しながら、処理するActionを補足します。

ここでは`IssueDetailActions.SET_ISSUE_DETAIL` の周辺を見ます。

typeと共に渡された、`issueDetail ` を返しているだけですね。これでstateが更新され、更新をトリガーに画面の必要な部分だけが再描画されます。

```
function issueDetail(state = new Issue(), action) {
  switch (action.type) {
    case IssueDetailActions.SET_ISSUE_DETAIL:
      return action.issueDetail
    case IssueDetailActions.SET_COMMENTS:
      return state.set('comments', action.comments)
    default:
      break // do nothing
  }
  return state
}
```

### src/lib/records/Issue.js


ここまでの一連の流れで利用されている `issueDetail` の実体は、`src/lib/records/Issue.js`で独自に定義されたモデルクラスで、今のWantedlyのスタックで非常に重要な部分を占めています。

Component間のやりとりや、Action -> Reducerの間の受け渡しをこのモデルに統一することで、見通しをよくするだけではなく、キー名のtypoなどにも気付きやすくなります。

以下のブログ記事はWantedlyの利用方法に近い例を紹介しています。
[How to use Immutable.js Records with React and Redux](https://medium.com/azendoo-team/immutable-record-react-redux-99f389ed676#.kfj7klw7z)

`fromJS`は`Issue.fromJS(someObject)`で実行可能で、APIレスポンスなどをこのモデルに変換するために利用します。

また、モデルクラスのため、validationのロジックもこの中に寄せています。

```src/lib/records/Issue.js
const _Issue = Record({
  id: null,
  title: '',
  status: STATE.CLOSE,
  comment_count: null,
  created: '',
  updated: '',
  comments: new List(),
  content: '',
  assignee: new User(),
})

export default class Issue extends _Issue {
  static fromJS(issue = {}) {
    let comments = new List()

    if (issue.comments) {
      comments = new List(issue.comments.map((comment) => {
        return Comment.fromJS(comment)
      }))
    }

    return (new this).merge({
      id: parseInt(issue.id),
      title: issue.title,
      status: issue.status,
      comment_count: issue.comment_count,
      created: issue.created,
      updated: issue.updated,
      content: issue.content,
      comments,
      assignee: issue.assignee ? User.fromJS(issue.assignee) : new User(),
    })
  }

  isValidTitle() {
    return this.title.length > 0
  }

  isValidContent() {
    return this.content.length > 0
  }
}
```


## :muscle: 演習 :muscle: 

### 課題1: 一覧画面にコメント数を表示するカラムを追加しましょう

* 表示要素は`src/lib/records/Issue.js`に追加してください
* APIからは`comment_count ` というキー名でデータが返ってきています
* 併せて`src/components/IssueListItem.scss`を触って少しスタイルを調整してみましょう

![comment-count.png](https://qiita-image-store.s3.amazonaws.com/0/29637/f84460c8-9c1e-8c75-ac49-1b6ad5b5a832.png "comment-count.png")


### 課題2: 一覧画面にアサインとラベルによる絞込機能を追加しましょう

* 既にOpen/Closeのステータスに絞込の実装はされているので、最終的に`src/containers/IssueListContainer.js`の`search`メソッドにつなぎ込むことを目指します。

![assign-label.gif](https://qiita-image-store.s3.amazonaws.com/0/29637/c027eb7e-f0fa-9608-9ad1-a7813e30a10d.gif "assign-label.gif")


### 課題3: 詳細画面に投稿済みコメントの編集機能と削除機能を追加しましょう

* コメントの編集中を判定するための状態をもつ必要があります
	* そのフラグを見て、Viewの中でinput/表示を切り替えます
	* コメントの投稿は`src/components/IssueCommentForm.js` を真似てみてください
	* `src/actions/issueDetail.js`の`updateComment`メソッドをコールすることを目指します。
* 削除機能はAPIアクセス部分が未実装なので、`${END_POINTS.ISSUES}/${issue.id}/comments/${comment.id}`へ`DELETE`メソッドを送るアクセス部分を作成します

![comment-edit.gif](https://qiita-image-store.s3.amazonaws.com/0/29637/b9d8aeb2-4ab8-9883-75f7-7e633b3fd0ad.gif "comment-edit.gif")


### 課題4: 詳細画面の新規コメント投稿時に"ユーザ名"と"コメント内容"のブランクチェックを実装しましょう

* `src/actions/issueDetail.js`の`postCommentRequest`メソッドの中で、サーバサイドからのエラーを補足しましょう。
	* エラー文言などはIssueDetailManagerに置くのが良いと思います
* クライアントサイドのバリデーション処理も追加しましょう。
	* バリデーションのロジックは`src/lib/records/Comment.js`に書きましょう

### 課題5: 詳細画面に"ラベル追加機能"と"アサイン追加機能"を追加しましょう

* `react-modal`を使ってモーダル画面を追加します。
	* 一覧画面のアサインとラベルの絞込が参考になります。
	* 最終的には

![add-assign-label.gif](https://qiita-image-store.s3.amazonaws.com/0/29637/1952146b-09df-ebc1-e3a4-6d4a9db4e363.gif "add-assign-label.gif")



## :coffee: 物足りない方のための追加演習 :coffee:

### 一覧での検索状態を保持できるようにしましょう

* 一覧 -> 詳細 -> 一覧のような遷移をすると、一覧の絞込条件がクリアされてしまいます。
	* paramsを保持することで、復帰できるようにしましょう。

### 一覧の並び換えを実装してみましょう

* APIは実装済みです

### ラベルの作成画面を実装してみましょう

* ここまでの内容をふまえて、ラベルの作成・編集画面を実装してみましょう

## :thumbsup: 以上です :thumbsup:

いかがでしたでしょうか、React + Reduxなスタックでの実装の参考になれば幸いです :bow:

## :smile:  Contributors :smile: 

![スクリーンショット 2016-06-09 18.42.16.png](https://qiita-image-store.s3.amazonaws.com/0/29637/5b097e3d-dd36-0b3f-9139-b68c86246f35.png "スクリーンショット 2016-06-09 18.42.16.png")

* API & CSS -> [@mamoru0217](https://github.com/mamoru0217)
* Client -> [@kawasin73](https://github.com/kawasin73)
* supervisor -> [@KentoMoriwaki](https://github.com/KentoMoriwaki)