Reduxのサンプル async (非同期通信)を調べてみた。


Reduxのexampleにある async　というサンプルについて調べてみた。

### 留意点
- 初心者の覚書です。
- 自分の環境で動くように参考にしたコードを適当に修正している。
- Windows10 64bit , PowerShellなどで動かしている。
- 見栄えを若干よくする為にbootstrap4を利用している。
- 解説はアバウトな言い回しで厳密ではない。誤解を含む可能性あり。
- stateの表示などでconsole.logを多用している。

###実行画面

変則的だが、最初に実行画面で、selectボックスの選択やボタンのクリックなどに応じて、
stateの状態がどのように変化するか確認。


<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Reduxのサンプル asyncの実行画面 <a href="https://t.co/7Ud37CSE3f">https://t.co/7Ud37CSE3f</a> <a href="https://twitter.com/YouTube?ref_src=twsrc%5Etfw">@YouTube</a>さんから</p>&mdash; tkarasuma (@tkarasuma) <a href="https://twitter.com/tkarasuma/status/1009300478956453888?ref_src=twsrc%5Etfw">2018年6月20日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


tokyo(デフォルト), osaka, kyoto の3つから選択できるが、最初はstatesByWordにtokyoのデータだけが入っている。osakaやkyotoなどを選択すると、そのたびに、statesByWordにそれらのデータが追加されていくのがわかる。
3つとも選択されたあとは、selectボックスを変更してもstatesByWordのそれらのデータは(アプリ内部では)更新されない。

ここで、osakaを選択したまま　**`再読込み`** ボタンを押すと, 
INVALIDATA_Wordアクションのところで、osakaのdidInvalidateがtrueにかわり、
REQUEST_POSTSアクションのあと、osakaのisfetchingがtrue, didInvalidateがfalseになっている。 
RECEIVE_POSTSのあとは、isfetching, didInvalidateはどちらもfalseになり、itemsに新しいデータが格納されている。


### リデューサー/ アクション

リデューサは selectedWord(初期値tokyo) postsByWord(初期値{})からなる。あとで、combineReducersで一括りにする。
postsByWordオブジェクトは tokyoやosakaなどのプロパティに posts関数で

```test:
{isFetching:xxx,
 items:xxx,
 lastUpdated:xxx,
 didInvalidate:xxx
}
```
のようなオブジェクトを格納。 didInvalidateは後で追加する。 次がリデューサ。

```javascript:reducers\index.js
import { combineReducers } from 'redux'
import { SELECT_WORD, REQUEST_POSTS, RECEIVE_POSTS } from '../actions'


const selectedWord = (state = 'tokyo', action) => {
    switch (action.type) {
        case SELECT_WORD:
            return action.word
        default:
            return state
    }
}
// posts の初期値
const init_posts = {
    isFetching: false,
    items: []
}


const posts = (state = init_posts, action) => {
    switch (action.type) {
        case REQUEST_POSTS:
            // didInvalidateは強制的に更新したいときだけ使う。
            // いまは必要ない。
            return {
                ...state,
                isFetching: true
            }
        // itemsにはjsonデータから引っ張ってき配列を入れる。
        case RECEIVE_POSTS:
            return {
                ...state,
                isFetching: false,
                items: action.posts,
                lastUpdated: action.receivedAt
            }
         default:
            return state
    }
}

const postsByWord = (state = {}, action) => {
    switch (action.type) {
        case RECEIVE_POSTS:
        case REQUEST_POSTS:
            return {
                ...state,
                [action.word]: posts(state[action.word], action)
            }

        default:
            return state;
    }
}

const rootReducer = combineReducers({
    postsByWord,
    selectedWord
})

export default rootReducer
``` 
アクションは次。middlewareを使っているので、dispatchの引数のplain objectタイプではなく、asyncタイプのアクションを利用しているところがあるのに注意。

```javascript:actions\index.js
export const REQUEST_POSTS = 'REQUEST_POSTS'
export const RECEIVE_POSTS = 'RECEIVE_POSTS'
export const SELECT_WORD = 'SELECT_WORD'

export const selectWord = word => ({
    type: SELECT_WORD,
    word
})
// isFetching: trueにする
export const requestPosts = word => ({
    type: REQUEST_POSTS,
    word
})
//stateに取得したデータを格納し、isFetchingをfalseにする。
export const receivePosts = (word, json) => ({
  type: RECEIVE_POSTS,
  word,
  posts: json.data.children.map(child => child.data),
  receivedAt: Date.now()
})


// dはdispatch
const fetchPosts = (word) => d => {
    d(requestPosts(word))
    fetch(`https://www.reddit.com/r/${word}.json`)
        .then(res => res.json())
        .then(json => d(receivePosts(word, json)))
}


const shouldFetchPosts = (state, word) => {
    const posts = state.postsByWord[word]
// 対応するwordに対するデータ取得前はpostsは空っぽ undefined
    if (!posts) {
        return true
    }
    // ロード中はいじらない
    if (posts.isFetching) {
        return false
    }
    return false
}
// dはdispatch g はgetState
export const fetchPostsIfNeeded = word => (d, g) => {
    if (shouldFetchPosts(g(),word)) {
        //d すなわち dispatchの引数は　plain object アクションではなく
        // middleware依存の Async アクションということらしい。
        d(fetchPosts(word));
    }
}
```


###コンポーネント

```jsx:components\Posts.js
import React from 'react'
import PropTypes from 'prop-types'

// postsはオブジェクトの配列で、
// 例えば、tokyo関係のスレッド30件弱程度
const Posts = ({posts})=>(
    <ul>
    {posts.map((post,i)=>
    <li key={i}>{post.title}</li>)}
    </ul>
)


Posts.propTypes = {
    posts: PropTypes.array.isRequired
  }
  
  export default Posts
```

```jsx:components\Picker.js
import React from 'react'
import PropTypes from 'prop-types'

// value は　stateの selectedWord
const Picker = ({ value, onChange, options }) => (
  <div>
    <h1>{value}</h1>
    <select className="form-control" onChange={e => onChange(e.target.value)}
            value={value}>
      {options.map(option =>
        <option value={option} key={option}>
          {option}
        </option>)
      }
    </select>
  </div>
)

Picker.propTypes = {
  options: PropTypes.arrayOf(
    PropTypes.string.isRequired
  ).isRequired,
  value: PropTypes.string.isRequired,
  onChange: PropTypes.func.isRequired
}

export default Picker

```

```jsx:containers\App.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { selectWord, fetchPostsIfNeeded } from '../actions'
import Picker from '../components/Picker'
import Posts from '../components/Posts'

class App extends Component {
    static propTypes = {
        selectedWord: PropTypes.string.isRequired,
        posts: PropTypes.array.isRequired,
        isFetching: PropTypes.bool.isRequired,
        lastUpdated: PropTypes.number,
        dispatch: PropTypes.func.isRequired
    }
    //最初のページ読み込み時に一回だけ実行される
    componentDidMount() {
        const { dispatch, selectedWord } = this.props
        dispatch(fetchPostsIfNeeded(selectedWord))
    }
    //
    // この関数の内部で this.props とすると古いプロップスになる
    //引数で渡したパラメータnに実行時、新しいプロップスが渡される。 
    // componentWillReceiveProps(n) {
    //   console.log("⚡componentWillReceiveProps");
    //   if (n.selectedWord !== this.props.selectedWord) {
    //     const { dispatch, selectedWord } = n
    //     dispatch(fetchPostsIfNeeded(selectedWord))
    //   }
    // }


    // 上記のcomponentWillReceivePropsは
    // 非推奨になっているようで、代わりにこっちを使う
    //引数で渡したパラメータpに実行時、古いプロップスが渡される。
    // dispatchの引数は　plain object アクションではなく
    // middleware依存の Async アクションということらしい。
    componentDidUpdate(p) {
        if (p.selectedWord !== this.props.selectedWord) {
            const { dispatch, selectedWord } = this.props
            dispatch(fetchPostsIfNeeded(selectedWord))
        }
    }

    handleChange = nextWord => {
        this.props.dispatch(selectWord(nextWord))
    }

    render() {
        const { selectedWord, posts, isFetching, lastUpdated } = this.props
        const isEmpty = posts.length === 0
        return (
            <div>
                <Picker value={selectedWord}
                    onChange={this.handleChange}
                    options={['tokyo', 'osaka', 'kyoto']} />
                <p>
                    {lastUpdated &&
                        <span>
                            最終更新日{new Date(lastUpdated).toLocaleTimeString()}.
              {' '}
                        </span>
                    }
                </p>
                {isEmpty
                    ? (isFetching ? <h2>ロード中...</h2> : <h2>空.</h2>)
                    : <div style={{ opacity: isFetching ? 0.5 : 1 }}>
                        <Posts posts={posts} />
                    </div>
                }
            </div>
        )
    }
}

const mapStateToProps = state => {

    // まずは大きな塊から取り出し
    const { selectedWord, postsByWord } = state
    // ES6の新しい文法で混乱した
    // postsByWord[selectedWord]は
    // {
    //   didInvalidate: false​​​,
    //   isFetching: false​​​,
    //   items: Array(26) [ {…}, {…}, {…}, … ]​​​,
    //   lastUpdated: 1529304245998
    // }
    // のようなオブジェクト
    // 下記のように、items: posts と記述すると
    //なんと、postsがプロパティ名になり、Array(26) [ {…}, {…}, {…}, … ]​​​が対応する値になる。

    const {
        isFetching,
        lastUpdated,
        items: posts
    } =
        postsByWord[selectedWord] ||
        {
            isFetching: true,
            items: []
        }
    //上記のようにstateのpostsByWordからデータをとりだし、下記のようにプロップスをつくる。
    return {
        selectedWord,
        posts,
        isFetching,
        lastUpdated
    }
}

export default connect(mapStateToProps)(App)

```

###エントリーポイント

```jsx:index.js
import React from 'react'
import { render } from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import thunk from 'redux-thunk'
import reducer from './reducers'
import App from './containers/App'

const store = createStore(
  reducer,
  applyMiddleware(thunk)
)
store.subscribe(()=>{
  console.log(store.getState());
})

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

```

###DOMのコンテナのhtml

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Redux async サンプル</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
    <style type="text/css">
html{
  font-size: 12px;
}
    </style>
  <body >
      <!-- classでBootsrapに関する設定 -->
    <div class="mx-auto w-75 mt-3"  id="root"></div>

    <script src="dist/index.js"></script>
  </body>
</html>

```

[ソースコード 01](https://github.com/tkarasuma/JavaScript/tree/master/Redux_async_01)

###再読込みの機能を追加

ここまでは、selectボックスで一回選択したあとは、そのwordについては更新されない。そこで強制的に
更新する機能を追加する。
そこで、postsByWordの中で各wordに結び付けられているオブジェクトにdidInvalidateフラグを追加。
ここをfalseにしておいて、selectボックスでの選択変更だけでは更新されないようにする。
更新ボタンを押した場合だけ、didInvalidateフラグをtrueにして、最読み込みを可能にする。


リデューサを修正。　INVALIDATE_WORDへの対応。　didInvalidateフラグ導入。

```javascript:reducers\index.js
import { combineReducers } from 'redux'
import { SELECT_WORD, REQUEST_POSTS, RECEIVE_POSTS, INVALIDATE_WORD } from '../actions'


const selectedWord = (state = 'tokyo', action) => {
    switch (action.type) {
        case SELECT_WORD:
            return action.word
        default:
            return state
    }
}
// posts の初期値
const init_posts = {
    isFetching: false,
    didInvalidate: false,
    items: []
}


const posts = (state = init_posts, action) => {
    switch (action.type) {
        case REQUEST_POSTS:
            // didInvalidateは強制的に更新したいときだけ使う。
            // いまは必要ない。
            return {
                ...state,
                isFetching: true,
                didInvalidate: false
            }
        // itemsにはjsonデータから引っ張ってき配列を入れる。
        case RECEIVE_POSTS:
            return {
                ...state,
                isFetching: false,
                items: action.posts,
                lastUpdated: action.receivedAt,
                didInvalidate: false
            }
        // didInvalidate をfalse → true　にするだけ。
        case INVALIDATE_WORD:
            return {
                ...state,
                didInvalidate: true
            }
        default:
            return state
    }
}

const postsByWord = (state = {}, action) => {
    switch (action.type) {
        case RECEIVE_POSTS:
        case REQUEST_POSTS:
        case INVALIDATE_WORD:
            return {
                ...state,
                [action.word]: posts(state[action.word], action)
            }

        default:
            return state;
    }
}

const rootReducer = combineReducers({
    postsByWord,
    selectedWord
})

export default rootReducer
```

 アクションの修正。invalidateWord関数追加。shouldFetchPosts 関数の挙動にinvalidateWordフラグの真偽を反映。

```javascript:actions\index.js
export const REQUEST_POSTS = 'REQUEST_POSTS'
export const RECEIVE_POSTS = 'RECEIVE_POSTS'
export const SELECT_WORD = 'SELECT_WORD'
export const INVALIDATE_WORD = 'INVALIDATE_WORD'

export const selectWord = word => ({
    type: SELECT_WORD,
    word
})
// isFetching: trueにする
export const requestPosts = word => ({
    type: REQUEST_POSTS,
    word
})
//stateに取得したデータを格納し、isFetchingをfalseにする。
export const receivePosts = (word, json) => ({
  type: RECEIVE_POSTS,
  word,
  posts: json.data.children.map(child => child.data),
  receivedAt: Date.now()
})

export const invalidateWord = word => ({
    type: INVALIDATE_WORD,
    word
  })
// dはdispatch
const fetchPosts = (word) => d => {
    d(requestPosts(word))
    fetch(`https://www.reddit.com/r/${word}.json`)
        .then(res => res.json())
        .then(json => d(receivePosts(word, json)))
}


const shouldFetchPosts = (state, word) => {
    const posts = state.postsByWord[word]
// 対応するwordに対するデータ取得前はpostsは空っぽ undefined
    if (!posts) {
        return true
    }
    // ロード中はいじらない
    if (posts.isFetching) {
        return false
    }
    return posts.didInvalidate
}
// dはdispatch g はgetState
export const fetchPostsIfNeeded = word => (d, g) => {
    if (shouldFetchPosts(g(),word)) {
        //d すなわち dispatchの引数は　plain object アクションではなく
        // middleware依存の Async アクションということらしい。
        d(fetchPosts(word));
    }
}
```

Appコンポーネント修正。再読込みボタンを追加。それに紐付いたクリックイベントハンドラのhandleRefreshClick関数を追加。mapStateToPropsも修正。

注意点をひとつ。オリジナルのサンプルではcomponentWillReceiveProps関数が利用されていたが、どこかで非推奨になっているとの記事を見つけたので、componentDidUpdate関数に変更。元の関数とは微妙に仕様がちがうのでコメントのところ参照。

componentDidMount関数は初回の読み込み時のみ。


```jsx:containers\App.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { selectWord, fetchPostsIfNeeded, invalidateWord } from '../actions'
import Picker from '../components/Picker'
import Posts from '../components/Posts'

class App extends Component {
    static propTypes = {
        selectedWord: PropTypes.string.isRequired,
        posts: PropTypes.array.isRequired,
        isFetching: PropTypes.bool.isRequired,
        lastUpdated: PropTypes.number,
        dispatch: PropTypes.func.isRequired
    }
    //最初のページ読み込み時に一回だけ実行される
    componentDidMount() {
        const { dispatch, selectedWord } = this.props
        dispatch(fetchPostsIfNeeded(selectedWord))
    }
    //
    // この関数の内部で this.props とすると古いプロップスになる
    //引数で渡したパラメータnに実行時、新しいプロップスが渡される。 
    // componentWillReceiveProps(n) {
    //   console.log("⚡componentWillReceiveProps");
    //   if (n.selectedWord !== this.props.selectedWord) {
    //     const { dispatch, selectedWord } = n
    //     dispatch(fetchPostsIfNeeded(selectedWord))
    //   }
    // }


    // 上記のcomponentWillReceivePropsは
    // 非推奨になっているようで、代わりにこっちを使う
    //引数で渡したパラメータpに実行時、古いプロップスが渡される。
    // dispatchの引数は　plain object アクションではなく
    // middleware依存の Async アクションということらしい。
    componentDidUpdate(p) {
        if (p.selectedWord !== this.props.selectedWord) {
            const { dispatch, selectedWord } = this.props
            dispatch(fetchPostsIfNeeded(selectedWord))
        }
    }

    handleChange = nextWord => {
        this.props.dispatch(selectWord(nextWord))
    }

    handleRefreshClick = e => {
        e.preventDefault()

        const { dispatch, selectedWord } = this.props

        dispatch(invalidateWord(selectedWord))
        dispatch(fetchPostsIfNeeded(selectedWord))
    }
    render() {
        const { selectedWord, posts, isFetching, lastUpdated } = this.props
        const isEmpty = posts.length === 0
        return (
            <div>
                <Picker value={selectedWord}
                    onChange={this.handleChange}
                    options={['tokyo', 'osaka', 'kyoto']} />
                <p>
                    {lastUpdated &&
                        <span>
                            最終更新日{new Date(lastUpdated).toLocaleTimeString()}.
              {' '}
                        </span>
                    }
                </p>
                {isEmpty
                    ? (isFetching ? <h2>ロード中...</h2> : <h2>空.</h2>)
                    : <div style={{ opacity: isFetching ? 0.5 : 1 }}>
                        <Posts posts={posts} />
                    </div>
                }
                {!isFetching &&
                    <button className="btn btn-warning mt-2" onClick={this.handleRefreshClick}>
                        再読込み
            </button>
                }
            </div>
        )
    }
}

const mapStateToProps = state => {

    // まずは大きな塊から取り出し
    const { selectedWord, postsByWord } = state
    // ES6の新しい文法で混乱した
    // postsByWord[selectedWord]は
    // {
    //   didInvalidate: false​​​,
    //   isFetching: false​​​,
    //   items: Array(26) [ {…}, {…}, {…}, … ]​​​,
    //   lastUpdated: 1529304245998
    // }
    // のようなオブジェクト
    // 下記のように、items: posts と記述すると
    //なんと、postsがプロパティ名になり、Array(26) [ {…}, {…}, {…}, … ]​​​が対応する値になる。

    const {
        isFetching,
        lastUpdated,
        items: posts
    } =
        postsByWord[selectedWord] ||
        {
            isFetching: true,
            items: []
        }
    //上記のようにstateのpostsByWordからデータをとりだし、下記のようにプロップスをつくる。
    return {
        selectedWord,
        posts,
        isFetching,
        lastUpdated
    }
}

export default connect(mapStateToProps)(App)

```

[ソースコード 02](https://github.com/tkarasuma/JavaScript/tree/master/Redux_async_02)

ついでに、冒頭の動画にあったコンソール出力しまくるソースもアップしておいた。

[ソースコード　コンソール出力多用](https://github.com/tkarasuma/JavaScript/tree/master/Redux_async_03)