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方向にすることが主です。あとはどこにビジネスロジックを入れるとか、コードの見通しがよくなることですね。