本日も昨日に引き続き、React Native + Reduxの説明をします。
Reduxの公式ドキュメントのAdvancedのセクションにRedditAPIを用いた非同期のサンプルがあります。これをReact Nativeで実装していみたいと思います。
ソースコードはgithubに載せてあります。
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で動かない場合があるように思いました。(深くは調査していませんが。