Debuginfo

思考とアウトプット

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