ReactでOverlay Spinner Componentを作る

#ReactでSpinnerを使いたい

reactでspinnerのような定番のコンポーネントが必要になったときは探せば誰かが作ったものが見つかることがあるので、それをそのまま利用したり参考にして自分で作ったりすると良いです。

react spinnerというようなワードで検索すればいろいろと見つかります。

今回はspinnerの基本部分は[spin.js](http://fgnass.github.io/spin.js/)をReact Component化した[react-loader](https://github.com/quickleft/react-loader)を使うことにしました。

react-loaderにはOverlayの機能がなかったので機能を追加したComponentを作ってみました。

##Live demoとcode
code: https://github.com/enu-kuro/React-Loader-Overlay-Sample
Live demo: http://enu-kuro.github.io/React-Loader-Overlay-Sample/index.html
テキトーにボタンを押してみてください。

##Spinner.react.js
Loader Component(react-loader)をOverlay viewでwrapしています。

さらにReactCSSTransitionGroupで包むことでfade in out機能を実装しています。
パラメータはcssで設定します。


```js
import React, { PropTypes } from 'react/addons';
var Loader = require('react-loader');
var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;

//fade in, outさせるにはCSSに以下の項目を追加する
/*
.spinner-enter {
  opacity: 0.01;
  transition: opacity .5s ease-in;
}
.spinner-enter.spinner-enter-active {
  opacity: 1;
}
.spinner-leave {
  opacity: 1;
  transition: opacity .5s ease-in;
}
.spinner-leave.spinner-leave-active {
  opacity: 0.01;
}
*/

class Spinner extends React.Component {

  static get propTypes() {
    return { isLoading: PropTypes.bool };
  }

  constructor(props){
    super(props);
  }

  get options() {
    return (
      {
        lines: 13,
        length: 20,
        width: 10,
        radius: 30,
        corners: 1,
        rotate: 0,
        direction: 1,
        color: '#fff',
        speed: 1,
        trail: 60
      }
    );
  }

  get styleForOverlay() {
    return (
      {
        background: 'rgba(0, 0, 0, 0.2)',
        position: 'fixed',
        top: 0,
        left: 0,
        width: '100%',
        height: '100%',
        zIndex: 10,
        display: 'block'
      }
    );
  }

  render() {
    return (
      <ReactCSSTransitionGroup transitionName="spinner" transitionLeave={false}>
      {this.props.isLoading ?
          <div style={this.styleForOverlay}>
            <Loader key='Loader' options={this.options}></Loader>
          </div>
      : null}
      </ReactCSSTransitionGroup>
    );
  }

}

export default Spinner;
```

##App.react.js
上記で作成したSpinnerをRoot Componentに配置します。

Fluxの流れに沿って、Child ComponentでActionを発行, StoreからRoot ComponentにLoadingの開始・停止を通知しています。

```js
import React from 'react';
import Router from 'react-router';
var { RouteHandler } = Router;
import Spinner from './Spinner.react';
import LoadingStore from '../stores/LoadingStore.js';

export default class App extends React.Component {

  constructor(props){
    super(props);
    this.state = {isLoading: false};
    this.onChange = this.onChange.bind(this);
  }

  componentDidMount(){
    LoadingStore.addChangeListener(this.onChange);
  }

  componentWillUnmount(){
    LoadingStore.removeChangeListener(this.onChange);
  }

  onChange(){
    this.setState({isLoading: LoadingStore.isLoading()});
  }

  render() {
    return (
      <div className="container">
        <Spinner isLoading={this.state.isLoading} />
        <header className="page-header">
          <h1>React Loader Overlay Sample</h1>
          <a href="https://github.com/quickleft/react-loader">react-loader Github page</a>
        </header>
        <RouteHandler/>
      </div>
    );
  }

}
```

##Login.react.js
ボタンを押すとActionCreators.startLoading();でActionが発行されます。
処理が成功したらActionCreators.stopLoading();でSpinnerを止めて画面遷移をしています。

```js
import React from 'react';
import RouterContainer from '../services/RouterContainer';
import ActionCreators from '../actions/ActionCreators';

export default class Login extends React.Component {

  login() {
    ActionCreators.startLoading();

    ActionCreators.login()
    .then(function(){
      ActionCreators.stopLoading();
      RouterContainer.get().transitionTo('/main', {});
    });
  }

  render() {
    return (
      <div className="login jumbotron center-block">
        <h1>Login</h1>
        <form role="form">
        <div className="form-group">
          <label htmlFor="username">UserName</label>
          <input type="text" className="form-control" id="username" placeholder="UserName" />
        </div>
        <div className="form-group">
          <label htmlFor="password">Password</label>
          <input type="password" className="form-control" id="password" ref="password" placeholder="Password" />
        </div>
        <button type="button" className="btn btn-default" onClick={this.login.bind(this)}>Submit</button>
      </form>
    </div>
    );
  }
}
```

##LoaingStore.js
Actionを受け取ったらisLoadingの値を更新してLoadingStore.emitChange()でComponentに更新通知をしています。

```js
var AppDispatcher = require('../dispatcher/AppDispatcher');
var AppConstants = require('../constants/AppConstants');
var EventEmitter = require('events').EventEmitter;
var objectAssign = require('react/lib/Object.assign');
var ActionTypes = AppConstants.ActionTypes;
var CHANGE_EVENT = 'change';

var isLoading = false;

var LoadingStore = objectAssign({}, EventEmitter.prototype, {

    emitChange: function() {
      this.emit(CHANGE_EVENT);
    },

    addChangeListener: function(callback) {
      this.on(CHANGE_EVENT, callback);
    },

    removeChangeListener: function(callback) {
      this.removeListener(CHANGE_EVENT, callback);
    },

    isLoading: function(){
      return isLoading;
    }

});

LoadingStore.dispatchToken = AppDispatcher.register(function(action) {

  switch(action.type) {

    case ActionTypes.START_LOADING:
      isLoading = true;
      LoadingStore.emitChange();
    break;

    case ActionTypes.STOP_LOADING:
      isLoading = false;
      LoadingStore.emitChange();
    break;
    default:

  }
});

module.exports = LoadingStore;
```