React+MobX+Firebase-Authenticationでログイン/ログアウト機能を実装する

Qiitaにも書いてます React+MobX+Firebase-Authenticationでログイン/ログアウト機能を実装する

はじめに

以前似たようなことをReact+Reduxでやってみています。 React+Redux+Firebase Authenticationでログイン/ログアウト機能を実装する (1) React+Redux+Firebase Authenticationでログイン/ログアウト機能を実装する (2)

最近、趣味の小規模プロジェクトだとReduxめんどくさいな、、と思い始めました。そこで、シンプルでいいよと聞くMobXを試してみることに。 まず手始めに、Reduxをつかってやってみたのと同じことをやってみようと思い、Firebase-Authenticationを使ったログイン/ログアウト機能をもつアプリケーションを作ってみました。

作成したものはk5trismegistus/react-mobx-firebase-loginにあります。

MobXとは

MobXとは、Simple, scalable state managementをうたうステート管理ライブラリです。

Observable属性を持つなステートがあり、Observableな属性が変化するとそれをObserveしているObserverに通知されるというシンプルなものです。Reactと組み合わせる場合はObserverがReactコンポーネントとなり、Observableが変化するとコンポーネントの見た目も変化する、ということになるでしょう。

Reduxのようなフレームワークよりも薄いものであり、ステートの変更方法などについては利用者に任されています。今回はステートを保持するストアにステートを変更するメソッドをもたせておいて、コンポーネントからそのメソッドを呼び出すというシンプルなものになっています。

公式ドキュメントではこういった図で概要を表現しています。 flow

もっとよく知りたい方はこちらをどうぞ。

はじめかた

公式ボイラープレートがあります。react-mobx-boilerplate

実装

ストアの実装

MobXとはどんなものか、実際のストアをみてみましょう。

import { observable, computed } from 'mobx';
import firebase from 'firebase';

export class AuthStore {
  @observable uid = '';
  @observable displayName = '';

  @computed get isLoggedin() {
    return !!this.uid;
  }

  doLogin() {
    let provider = new firebase.auth.GoogleAuthProvider();
    firebase.auth().signInWithPopup(provider);
  }

  doLogout() {
    firebase.auth().signOut()
  }

  refLogin() {
    firebase.auth().onAuthStateChanged(user => {
      if (!user) {
        this.clearUserInfo();
        return;
      }
      this.setUserInfo(user);
    });
  }

  setUserInfo(user) {
    this.uid = user.uid;
    this.displayName = user.displayName;
  }

  clearUserInfo() {
    this.uid = '';
    this.displayName = '';
  }
}

見ての通り、@observableがついているプロパティがObservableなものです。Reactコンポーネントはこの値を参照することができます。

そしてもう一つ特徴的なのが@computed。これはRailsでいうDraperみたいなもので、他のプロパティから導出可能な値をあたかもobservableなプロパティのように扱えるようにするものです。 今回は、ログインしているかどうかをuidに値が入っているかどうかで判別し、それを@computedにしています。こうすることで、「uidの有無でログイン状態を判別する」というロジックをコンポーネントに持たせずに済ますことが可能です。

使い方

Reactコンポーネントに関しては主題ではないので紹介にとどめます。

まず、エントリーポイント。

import React from 'react';
import { render } from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import { AuthStore } from './stores/AuthStore';
import { App } from './components/App';
import { firebaseApp } from './util/firebase/initializer'

const authStore = new AuthStore();

render(
  <AppContainer>
    <App authStore={authStore} />
  </AppContainer>,
  document.getElementById('root')
);

if (module.hot) {
  module.hot.accept('./components/App/App', () => {
    const NextApp = require('./components/App/App').default;

    render(
      <AppContainer>
        <NextApp authStore={authStore} />
      </AppContainer>,
      document.getElementById('root')
    );
  });
}

アプリケーションのルートとなるApp。

import React, { Component } from 'react';
import { observer } from 'mobx-react';
import DevTools from 'mobx-react-devtools';

import { Auth } from '../Auth';
import { Root } from '../Root'

@observer
class App extends Component {
  componentDidMount() {
    this.props.authStore.refLogin();
  }

  render() {
    return (
      <div>
        { this.props.authStore.isLoggedin ? <Root authStore={this.props.authStore} /> : <Auth authStore={this.props.authStore} /> }
      </div>
    )
  }
};

export default App;

ログインしていないときに表示されるAuth。

import React, { Component } from 'react';
import { observer } from 'mobx-react';
import DevTools from 'mobx-react-devtools';

@observer
class Auth extends Component {

  render() {
    return (
      <div>
        <div>
          ログインしてね
        </div>
        <div>
          <button onClick={this.props.authStore.doLogin}>Login</button>
        </div>
      </div>
    )
  }
};

export default Auth;

ログインしているときに表示されるRoot。(Rootという名前だけどRootじゃない笑)

import React, { Component } from 'react';
import { observer } from 'mobx-react';
import DevTools from 'mobx-react-devtools';

@observer
class Root extends Component {
  render() {
    return (
      <div>
        <div>
          こんにちはー {this.props.authStore.displayName} さん
        </div>
        <div>
          <button onClick={this.props.authStore.doLogout}>Logout</button>
        </div>
      </div>
    )
  }
};

export default Root;

ログインしたら「こんにちはー<Googleの登録名>さん」と言ってくれるだけのシンプルなアプリです。

エントリーポイントにおいてAuthStoreを作り、それをAppのpropsに渡します。そして必要であればAppが子コンポーネントのpropsにAuthStoreを渡し…というふうに、Reduxと比較するとReactのデータ管理の仕組みに依存している部分が強いです。React+Reduxでアプリ開発しようとすると、手を付ける前にReactを学んだ後Reduxをも学ぶ必要があります。 一方MobXはReactさえ知っていればある程度手を動かすところまでいけそうだと思います。

コンポーネントは親コンポーネントが知ってる以上のこと(=props)を知らない、というReactの仕組みにのっとっている以上、Reactとの相性はReduxより良いのではないかと思います。

Single source of truthを旨とするReduxと違い、MobXはストアをいくつでも持つことができます。SPAを作ったことがないので又聞きなのですが、SPAでReduxを使うとストアのメモリ使用量がどんどん膨らんでいくそうですが、MobXの場合はそのとき必要なストアだけを存在させるようにできるのがよい、という意見を聞きました。

Reduxと比較してみて

Reduxと比較すると、「とりあえず動くもの」を作れるまでの手間が圧倒的に少なくて済みます。ストアの構造を考え、Actionの構造を決め、Reducerを作って…ではなく、ストアをつくるだけで済むので。

状態はイミュータブルでなければならず、Reducerは純粋関数で…といった理念的な話もないので、全然経験ないけど、フロントエンド開発チャレンジしてみたいな〜!と思っている方はReduxよりMobXを選んだほうが楽しく始められると思います!