ReactのRedux非同期処理がサルでも分かる超解説

この記事はかつての私と同じように「Reduxを使った非同期処理がいまいち分かんねー」という方に向けて書いた。とりあえずはReactの公式サイト、[Reduxの公式サイト](http://redux.js.org/)、[Dan氏のReduxビデオ解説](https://egghead.io/courses/getting-started-with-redux)を観たが、なんかスッキリしない。特にReduxの非同期処理が分からない、という方向けの超シンプル解説。

Reactは公式サイトのチュートリアルなんかも充実していて丁寧だし分かりやすかった。しかしReduxは違う。特に公式サイトの[非同期処理の例](http://redux.js.org/docs/advanced/ExampleRedditAPI.html)が変にややこしい。
こういうことをブログで書くと「アタシは公式サイトの説明を読んでも分からないバカです」と言ってるみたいだから、恥ずかしいしあまり書かれない。ウザいぐらいに「Reduxは素晴らしい。シンプル。カンタン」という発言がネット上にあふれている。

しかし私の頭ではパっと分からなかった。私以外でも「これ難しいなー」と思ってる人が居るんじゃないだろうか。仮に今は分かっていてもそこに達するまでにまーまー苦労したとか。オープンイノベーションの世界では「オレは習得するのに苦労したから、後続の人も同じ苦労をしろ」を根絶するべき。
したがって恥を忍んででも「ReactのRedux非同期処理がサルでも分かる超解説」を書くことにした。

この解説方法を一言で言うとこうなる。
**まず先にReduxを使わないで非同期処理のコードをReactで書いて、その後でReduxを加える**
これをやることでやっと理解できた。

[本記事で最終的にできあがるコードの動くサンプル](https://simple-example-redux.herokuapp.com/)

これは単にサーバーに保存されているコメント群をとってきて表示するだけ。
（mockapi.ioが動いてない場合はerror表示を出します。）

[ソースコード](https://bitbucket.org/jabba14/simple_example_redux/src)

クローンしてそのまま npm install してnpm startとすれば動きます。まだまだ学習中の身でもあるので「こうした方がいい」とかあったらぜひコメントください。

## Reactだけの例

まずはReact だけを使った例
これはReactの基礎知識があれば把握できるレベルの単純なコード。

```javascript:index.js
import React, { Component } from 'react'
import ReactDOM from 'react-dom'

class CommentList extends Component {
  constructor() {
    super();
    this.state = {
      comments: [
        {
          id: 1,
          comment: 'comment 1'
        },
        {
          id: 2,
          comment: 'comment 2'
        },
        {
          id: 3,
          comment: 'comment 3'
        },
        {
          id: 4,
          comment: 'comment 4'
        }
      ],
      hasError: false,
      isLoading: false
    }
  }
  render() {
    if (this.state.hasError) {
      return <p>error</p>;
    }
    if (this.state.isLoading) {
      return <p>loading . . . </p>;
    }
    return (
      <ul>
        {this.state.comments.map((item) => (
          <li key={item.id}>
            {item.comment}
          </li>
        ))}
      </ul>
    )
  }
}

ReactDOM.render(
  <CommentList />,
  document.getElementById('app')
)
```

実行した結果

ソースコードをクローンした場合はコミットログのaacf3a3 "non-redux example"にして、npm startすれば以下の画面が出る。
<img width="200" alt="20170626055310.png" src="https://qiita-image-store.s3.amazonaws.com/0/108761/ab7e8d17-33e9-d216-c1e4-ccbe71bbced5.png">


constructorを見れば分かるようにstateには配列でcommentとboolean形式で２種類のステータスを入れている。

```javascript
constructor() {
    super();
    this.state = {
      comments: [
        {
          id: 1,
          comment: 'comment 1'
        },
        {
          id: 2,
     // :  省略

      ],
      hasError: false,
      isLoading: false
    }
```


isLoading かもしくは hasErrorをtrueにすると、それぞれの表示に切り替わる。

## APIからデータを取ってくる
ソースコードにcommentsを書き入れるのでは内容が変化しないので、そこをAPIからJSON形式で取ってくるように変更する。


```javascript:index.js
import React, { Component } from 'react'
import ReactDOM from 'react-dom'

class CommentList extends Component {
  constructor() {
    super();
    this.state = {
      comments: []
    }
  }
  fetchData(url) {
    this.setState({ isLoading: true });
    fetch(url)
      .then((response) => {
        if (!response.ok) {
          throw Error(response.statusText);
        }
        this.setState({ isLoading: false });
        return response;
      })
      .then((response) => response.json())
      .then((comments) => this.setState({ comments }))
      .catch(() => this.setState({ hasErrored: true }));
  }
  componentDidMount() {
    this.fetchData('https://594ecc215fbb1a00117871a4.mockapi.io/comments');
  }
  render() {
    if (this.state.hasError) {
      return <p>error</p>;
    }
    if (this.state.isLoading) {
      return <p>loading . . . </p>;
    }
    return (
      <ul>
        {this.state.comments.map((item) => (
          <li key={item.id}>
            {item.comment}
          </li>
        ))}
      </ul>
    )
  }
}

ReactDOM.render(
  <CommentList />,
  document.getElementById('app')
)
```

つまり以下のコードがmount時に実行されてコメントをAPIから取ってくる。

```javascript
  componentDidMount() {
    this.fetchData('https://594ecc215fbb1a00117871a4.mockapi.io/comments');
  }
```

[https://594ecc215fbb1a00117871a4.mockapi.io/comments](https://594ecc215fbb1a00117871a4.mockapi.io/comments)というのは無料で登録したモックで、アクセスすると５つのコメントをJSON形式で返してくる。本来ならここはRailsとかのサーバーにしてお好きなJSONを返すようにする。

動かした結果の画面はほぼ同じだが、コメントの中身はAPIから取ってきてますよ、と。


## Reduxを入れる

では上記のコードにReduxを入れていく。まずはredux react-redux redux-thunkが必要になるのでそれらをインストールする。

```
npm install redux react-redux redux-thunk --save
```

### 念のためReduxの３原則
[Three Principles](http://redux.js.org/docs/introduction/ThreePrinciples.html)

- Single source of truth(状態管理は１箇所だけ)
- State is read-only(状態は読み取り専用)
- Changes are made with pure functions(変更は純粋な関数で行う)

Reduxを入れてコードが完成した後のファイル構成

```
├── package.json
└── src
    ├── actions
    │   └── comments.js
    ├── components
    │   └── CommentList.js
    ├── index.html
    ├── index.js
    ├── reducers
    │   ├── comments.js
    │   └── index.js
    └── store
        └── configureStore.js
```


## Stateの内容

Redux無しのコードで明らかになったようにStateには３つのプロパティが必要。comments、hasError、isLoadingでありそれぞれにReduxアクションが必要になる。


```javascript:src/actions/comments.js
export const getCommentsError = status => ({
  type: 'GET_COMMENTS_ERROR',
  hasError: status
})

export const loadComments = status => ({
  type: 'LOAD_COMMENTS',
  isLoading: status
})

export const fetchCommentsSuccess = comments => ({
  type: 'FETCH_COMMENTS_SUCCESS',
  comments
})
```

getCommentsErrorとloadCommentsはstatusを引数としてtype とステータスを返す。
fetchCommentsSuccessはデータの取り出しに成功したらコメントを配列に入れてcommentsとしてtype と共に返す。

アクションクリエータはアクションを返す。返すと書いているのにReturnが無い！となった方はこれは以下のように書いてるのと同じ。以下のコードをアロー関数で書いてreturnを省略しただけ。

```javascript
export function getCommentsError(status){
  return {
    type: 'GET_COMMENTS_ERROR',
    hasError: status
  };
}
```

アクションとしては元のRedux無しにあったfetchDataに相当するアクションがもうひとつ必要になる。ここではそれをfetchCommentsとして作成する。


```javascript:src/actions/comments.js
export const fetchComments = url => {
  return (dispatch) => {
    dispatch(loadComments(true));

    fetch(url)
      .then((response) => {
        if(!response.ok) {
          throw Error(response.statusText);
        }
        dispatch(loadComments(false));

        return response;
      })
      .then((response) => response.json())
      .then((comments) => dispatch(fetchCommentsSuccess(comments)))
      .catch(() => dispatch(getCommentsError(true)));
  }
}
```


## reducers

reducersはstateとactionという２つの引数を持つ。reducersの中ではswitchを使ってaction.typeごとに処理を分けて、それぞれのactionを返す。


```javascript:reducers/comments.js
export const getCommentsError = (state = false, action) => {
  switch (action.type) {
    case 'GET_COMMENTS_ERROR':
      return action.hasError;
    default:
      return state;
  }
}

export const loadComments = (state = false, action) => {
  switch (action.type) {
    case 'LOAD_COMMENTS':
      return action.isLoading;
    default:
      return state;
  }
}

export const comments = (state = [], action) => {
  switch (action.type) {
    case 'FETCH_COMMENTS_SUCCESS':
      return action.comments;
    default:
      return state;
  }
}
```

それぞれのreducerをrootReducerでくっつける。
importでそれぞれのreducerをインポートする。後はcombineReducersで囲う。
reducer名をもっとシンプルにgetErrorとかloadとかでも良かったんじゃないの？と思うかもしれないが、ここはできるだけcommentsという主語を入れた名前の方がいい。
今回の例では全てのreducerはcommentsに関することだが、これ以降にusers、likes、とか色んなreducerを扱うようになった時に混乱しないため。


```javascript:reducers/index.js
import { combineReducers } from 'redux';
import { getCommentsError, loadComments, comments } from './comments';

export default combineReducers({
  getCommentsError,
  loadComments,
  comments,
});
```


## Store

ここはほぼ全てのReduxの解説にある内容と同じ。こうしてStore作りますよ、と。

```javascript:store/configureStore.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';

const configureStore = initialState => {
  return createStore(
    rootReducer,
    initialState,
    applyMiddleware(thunk)
  );
}

export default configureStore
```



```javascript:index.js
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import configureStore from './store/configureStore';
import CommentList from './components/CommentList';

const store = configureStore();

render(
  <Provider store={store}>
    <CommentList />
  </Provider>,
  document.getElementById('app')
);
```

## Components


```javascript:components/CommentList.js
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { fetchComments } from '../actions/comments';

class CommentList extends Component {
  componentDidMount() {
    this.props.fetchData('https://594ecc215fbb1a00117871a4.mockapi.io/comments');
  }

  render() {
    if (this.props.hasError) {
      return <p> error </p>;
    }
    if (this.props.isLoading) {
      return <p> Loading...</p>;
    }

    return (
      <ul>
        {this.props.comments.map((item) => (
          <li key={item.id}>
            {item.comment}
          </li>
        ))}
      </ul>
    );
  }
}

CommentList.propTypes = {
  fetchData: PropTypes.func.isRequired,
  comments: PropTypes.array.isRequired,
  hasError: PropTypes.bool.isRequired,
  isLoading: PropTypes.bool.isRequired
};

const mapStateToProps = state => ({
  comments: state.comments,
  hasError: state.getCommentsError,
  isLoading: state.loadComments
});

const mapDispatchToProps= dispatch => ({
  fetchData: (url) => dispatch(fetchComments(url))
});

export default connect(mapStateToProps, mapDispatchToProps)(CommentList);
```


まず importしているconnectがcomponentをstore につなげる役割をする。
actionsからはfetchCommentsのみをimport する。ここで必要なのはこのアクションだけで他のはdispachして呼び出す。

後はもう細かい説明よりコード見た方がいい。


## ウェブ系エンジニアの皆様へ
「ほとんどのエンジニアには解けるが、下位10％のダメなエンジニアにだけ解けないパズル?」なるものをシリーズ化してパズル１から８まで作成した。もしご興味あれば解いてみてください。
http://tango-ruby.hatenablog.com/entry/2015/11/30/122814