React NativeでFluxを使う
React NativeのMovieチュートリアルをFluxで実装します。 Flux実装が山ほどある中、Facebook Fluxのgithub上のexampleのtodo,chatで使われている本家のFluxモジュールです。Reflux, altが色々enhanceしたもののようですが、npmのダウンロード数をみると圧倒的にFluxがダウンロードされています。やはりFacebook押しだと安心感がありますね。
では、必要なnpmを入れていきます。
$ npm install flux $ npm install object-assign $ npm install keymirror
まず全てindex.ios.jsに書かれていたもの分解して配置していきます。 最終的には、下記のようなツリー構造になります。
./index.ios.js
./app
├── actions
│ └── MovieActions.js
├── app.js
├── components
│ └── MovieListView.js
├── constants
│ └── MovieConstants.js
├── dispacher
│ └── AppDispatcher.js
├── dispatcher
│ └── AppDispatcher.js
├── stores
│ └── MovieStore.js
└── utils
└── MovieWebAPIUtils.js
index.ios.jsはシンプルにapp/app.jsを呼ぶ作りです。
"use strict";
var React = require('react-native');
var {
AppRegistry
} = React;
var App = require('./app/app')
AppRegistry.registerComponent('App', () => App);
app/app.jsは、Viewを呼びます。今回はWebAPIを用いてMovieリストを取ってくるので、最初の一回目を取得するコードgetMovies()を入れておきます。これでActionを走らせることになります。
// app/app.js
var MovieListView = require('./components/MovieListView');
var MovieWebAPIUtils = require('./utils/MovieWebAPIUtils');
MovieWebAPIUtils.getMovies();
module.exports = MovieListView;
app/dispatcher/AppDispatcherは、DispatcherをFluxライブラリを用いて作ります。 dispatchはObserverパターンそのものですね。このパターンがわかってる人なら感覚的にわかると思います。
// app/dispatcher/AppDispatcher
var Dispatcher = require('flux').Dispatcher;
module.exports = new Dispatcher();
app/utils/MovieWebAPIUtils.jsはAPIを定義して、Actionを発火します。今回はActionの発火はこれだけになりますが、アプリケーションを組んでいくとユーザイベントから発火させることがあるのは容易に想像できますね。
// app/utils/MovieWebAPIUtils.js
var MovieActions = require('../actions/MovieActions');
var REQUEST_URL
= 'https://raw.githubusercontent.com/facebook/react-native/master/docs/MoviesExample.json';
module.exports = {
getMovies: function() {
fetch(REQUEST_URL)
.then((response) => response.json())
.then((responseData) => {
MovieActions.receiveMovies(responseData.movies);
})
.done();
}
};
app/actions/MovieActions.jsは、アクションの登録です。決められたconstantに基づいてDispatchします。
// /app/actions/MovieActions.js
'use strict';
var AppDispatcher = require('../dispatcher/AppDispatcher');
var MovieConstants = require('../constants/MovieConstants');
var MovieActions = {
receiveMovies: function(movies) {
AppDispatcher.dispatch({
actionType: MovieConstants.MOVIES_CREATE,
movies: movies
});
},
};
module.exports = MovieActions;
app/constants/MovieConstants.jsは、Contantsを定義してます。これらのConstantsを見てstoreは状態を管理していくことになります。
//app/constants/MovieConstants.js
var keyMirror = require('keymirror');
module.exports = keyMirror({
MOVIES_CREATE: null,
});
app/stores/MovieStore.jsは、MVCでいうModelに当たるビジネスロジックを含める肝になる部分です。 今回は難しいことはやっていなく、dipatch用のメソッドが生えてるのがわかります。
'use strict';
var AppDispatcher = require('../dispatcher/AppDispatcher');
var EventEmitter = require('events').EventEmitter;
var assign = require('object-assign');
var MovieConstants = require('../constants/MovieConstants');
var CHANGE_EVENT = 'change';
var _loaded = false;
var _movies = {};
var MovieStore = assign({}, EventEmitter.prototype, {
isLoaded: function() {
return _loaded;
},
getMovies: function(){
return _movies;
},
setMovie: function(movies){
_loaded = true;
_movies = movies;
},
emitChange: function() {
this.emit(CHANGE_EVENT);
},
/**
* @param {function} callback
*/
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
/**
* @param {function} callback
*/
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
}
});
// Register callback to handle all updates
AppDispatcher.register(function(action) {
switch(action.actionType) {
case MovieConstants.MOVIES_CREATE:
MovieStore.setMovie(action.movies);
MovieStore.emitChange();
break;
default:
// no op
}
});
module.exports = MovieStore;
最後にViewです。app/components/MovieListView.jsは、dispatchされるために登録します。 setStateで書き換えるのは変わりません。
"use strict";
var React = require('react-native');
var MovieStore = require('../stores/MovieStore');
var {
StyleSheet,
Image,
ListView,
Text,
View,
} = React;
var Chifan = React.createClass({
getInitialState: function() {
return {
dataSource: new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2,
}),
loaded: false
};
},
componentDidMount: function() {
MovieStore.addChangeListener(this._onChange);
},
_onChange: function() {
this.setState(this.getMovieState());
},
getMovieState: function() {
var movies = MovieStore.getMovies();
return {
dataSource: this.state.dataSource.cloneWithRows(movies),
loaded: MovieStore.isLoaded()
};
},
render: function() {
if (!this.state.loaded) {
return this.renderLoadingView();
}
return (
<ListView
dataSource={this.state.dataSource}
renderRow={this.renderMovie}
style={styles.listView}
/>
);
},
renderLoadingView: function() {
return (
<View style={styles.container}>
<Text>
Loading movies...
</Text>
</View>
);
},
renderMovie: function(movie){
return (
<View style={styles.container}>
<Image
source={{uri: movie.posters.thumbnail}}
style={styles.thumbnail}
/>
<View style={styles.rightContainer}>
<Text style={styles.title}>{movie.title}</Text>
<Text style={styles.year}>{movie.year}</Text>
</View>
</View>
);
},
});
var styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'blue',
},
rightContainer: {
flex: 1,
},
title: {
fontSize: 20,
marginBottom: 8,
textAlign: 'center',
},
year: {
textAlign: 'center',
},
thumbnail: {
width: 53,
height: 81,
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
listView: {
paddingTop: 20,
backgroundColor: '#F5FCFF',
},
});
module.exports = Chifan
以上ですが、見てもらうとわかると思いますが、Viewはほぼ変わりません。これはReactがView操作に特化してるからです。Fluxがやっていることは情報を1方向にすることが主です。あとはどこにビジネスロジックを入れるとか、コードの見通しがよくなることですね。