React Native - Redux (part2 - 非同期アプリRedditAPIApp)


本日も昨日に引き続き、React Native + Reduxの説明をします。

Reduxの公式ドキュメントの[Advancedのセクション](https://redux.js.org/docs/advanced/AsyncActions.html)にRedditAPIを用いた非同期のサンプルがあります。これをReact Nativeで実装していみたいと思います。

![スクリーンショット 2015-12-09 20.59.14.png](https://qiita-image-store.s3.amazonaws.com/0/13663/8280d944-cfe2-cffd-45f4-fa47f3c74a3a.png)

ソースコードはgithubに載せてあります。

https://github.com/shohey1226/ReactNativeReduxRedditAPIApp


## Directory structure

昨日のtodoAppのと違って、configureStore.jsを作る必要があります。

```
index.ios.js
app/
├── actions.js
├── components
│   └── Posts.js
├── configureStore.js
├── containers
│   ├── AsyncApp.js
│   └── Root.js
└── reducers.js
```

## コード解説

### index.ios.js

`index.ios.js`は登録処理のみ。

```
import React from 'react-native';
import Root from './app/containers/Root';

let { AppRegistry } = React;

let ReactNativeReduxRedditAPIApp = React.createClass({
  render: function() {
    return(
      <Root />
    );
  }
});

AppRegistry.registerComponent('ReactNativeReduxRedditAPIApp', () => ReactNativeReduxRedditAPIApp);
```

### app/containers/Root.js

トップレイヤーのRoot.jsではStoreは設定して、AsyncAppコンポーネントを呼び出します。


```
import React from 'react-native'
import { Provider } from 'react-redux/native'
import configureStore from '../configureStore'
import AsyncApp from './AsyncApp'

const store = configureStore()
var Root = React.createClass({
  render() {
    return (
      <Provider store={store}>
        {() => <AsyncApp />}
      </Provider>
    )
  }
});

export default Root;
```

### app/configureStore.js

configureStoreではMiddlewareを設定します。作者のDan氏もあるpodcastで話してましたが、マーケティングのためにReduxは最小のコード（確か70行ぐらいだったような）のみで、その他の必要なものはmiddlewareとして提供されます。そのため非同期を実現するためには`redux-thunk`というmiddlewareが必要になります。redux-loggerはstateの変化をSTDOUTに表示してくれます。これも便利なmiddlewareです。

```
import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import createLogger from 'redux-logger'
import rootReducer from './reducers'

const loggerMiddleware = createLogger()

const createStoreWithMiddleware = applyMiddleware(
  thunkMiddleware,
  loggerMiddleware
)(createStore)

export default function configureStore(initialState) {
  return createStoreWithMiddleware(rootReducer, initialState)
}
```

### app/containers/AsyncApp.js

AsyncAppはメインなコンポーネントでActionをdispatchで実行します。connectを用いてstoreをpropで読めるようにします。

```
import React, {ScrollView, Text, PropTypes, PickerIOS} from 'react-native';
import { connect } from 'react-redux/native'
import { selectReddit, fetchPostsIfNeeded, invalidateReddit } from '../actions'
import Posts from '../components/Posts'
var Button = require('react-native-button');

var {
  AppRegistry,
  StyleSheet,
  View,
} = React;

var AsyncApp = React.createClass({

  componentDidMount() {
    const { dispatch, selectedReddit } = this.props
    dispatch(fetchPostsIfNeeded(selectedReddit))
  },

  componentWillReceiveProps(nextProps) {
    if (nextProps.selectedReddit !== this.props.selectedReddit) {
      const { dispatch, selectedReddit } = nextProps
      dispatch(fetchPostsIfNeeded(selectedReddit))
    }
  },
...
function mapStateToProps(state) {
  const { selectedReddit, postsByReddit } = state
  const {
    isFetching,
    lastUpdated,
    items: posts
  } = postsByReddit[selectedReddit] || {
    isFetching: true,
    items: []
  }

  return {
    selectedReddit,
    posts,
    isFetching,
    lastUpdated
  }
}

export default connect(mapStateToProps)(AsyncApp)
```

### app/actions.js

actionでは非同期を行うためにdispatchを返す形で書きます(redux-thunkを利用)。fetchPostsが非同期の箇所になります。つまり、actionが非同期を書く場所になります。ReducersはtodoAppと変わらなくシンプルなコードになります。

```
function receivePosts(reddit, json) {
  return {
    type: RECEIVE_POSTS,
    reddit,
    posts: json.data.children.map(child => child.data),
    receivedAt: Date.now()
  }
}

function fetchPosts(reddit) {
  return dispatch => {
    dispatch(requestPosts(reddit))
    return fetch(`http://www.reddit.com/r/${reddit}.json`)
      .then(req => req.json())
      .then(json => dispatch(receivePosts(reddit, json)))
  }
}

function shouldFetchPosts(state, reddit) {
  const posts = state.postsByReddit[reddit]
  if (!posts) {
    return true
  } else if (posts.isFetching) {
    return false
  } else {
    return posts.didInvalidate
  }
}

export function fetchPostsIfNeeded(reddit) {
  return (dispatch, getState) => {
    if (shouldFetchPosts(getState(), reddit)) {
      return dispatch(fetchPosts(reddit))
    }
  }
}
```

## Summary

以上でReduxにあるExampleを実装してみました。React Nativeでも非同期が問題なく書けることがわかります。気をつけるところは、asyncなどのBabelでtransformする箇所がactionで動かない場合があるように思いました。（深くは調査していませんが。