JavaScript
flux
reactjs
React

Nanox - react-micro-containerをベースにしたお手頃規模のFlux系フレームワーク

はじめに

素のReactだと面倒。でもFluxやReduxを使う程でもない規模のプロジェクトで何を使うか、というテーマに対する最適解を紹介します(胡散臭げな顔で)。

小規模なReact拡張ライブラリとして、例えばreact-micro-containerflumptなどがあります。

これらはフレームワークというよりはReactアプリケーションの実装を手助けする補助的役割がメインで、素のReactを徒歩とすると、react-micro-containerやflumptは自転車的な位置付けになるかと思います。

それに対し、FluxやReduxなどの本格的なフレームワークは2tトラックとかそんなところでしょうか。

徒歩は論外、自転車でもちょっとしんどい、かといって2tトラックは大袈裟なんだよなぁ・・・といった時の選択肢として今回紹介するのが拙作のNanoxです。

規模としてはバイクとか軽自動車とか、そのくらいを想像していただければ(分かりやすいようで分かりにくい謎の例え)。

図にするとこんな感じです。

flow.png

使用方法

Nanoxはreact-micro-containerをベースにしたフレームワークなので、こちらの記事をベースにどんなものか説明していきます。

基本部分は大体同じです。

子コンポーネント

react-micro-containerの記事から抜粋

まず普通のステートレスなReactコンポーネントを作る。dispatchというpropsを受け取ってそれを通してイベントを発火するというのが唯一の規約。

components/counter.js
import React from 'react';

// Stateless component
export default class Counter extends React.Component {
  render() {
    return (
      <div>
        <div>{this.props.count}</div>
        <button onClick={() => this.props.dispatch('increment', 1)}>+1</button>
        <button onClick={() => this.props.dispatch('decrement', 1)}>-1</button>
        <button onClick={() => this.props.dispatch('increment', 100)}>+100</button>
      </div>
    );
  }
}

ここはNanoxでも全く同じです。

コンテナコンポーネント

react-micro-containerの記事から抜粋

それをwrapするコンテナコンポーネントを作る。こいつはstateを持ち、子のコンポーネントにdispatchを渡してイベントをsubscribeし、適時stateを更新する。

container/counter.js
import MicroContainer from 'react-micro-container';
import Counter from '../components/counter';

export default class CounterContainer extends MicroContainer {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  componentDidMount() {
    this.subscribe({
      increment: this.handleIncrement,
      decrement: this.handleDecrement,
    });
  }

  handleIncrement(count) {
    this.setState({ count: this.state.count + count });
  }

  handleDecrement(count) {
    this.setState({ count: this.state.count - count });
  }

  render() {
    return <Counter dispatch={this.dispatch} {...this.state} />;
  }
}

ここからNanox独自の実装が入ってきますので、拡張ポイントを説明していきます。

上記のreact-micro-containerでの実装部分をNanoxでは以下のように書きます。

import Nanox from 'nanox'; // 変更点①
import Counter from '../components/counter';
import actions from './actions'; // 変更点②

export default class CounterContainer extends Nanox { // 変更点③
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  componentDidMount() {
    this.registerActions(actions); // 変更点④
  }

  // 変更点⑤

  render() {
    return <Counter dispatch={this.dispatch} {...this.state} />;
  }
}

変更点①

react-micro-containerをインポートするかわりにnanoxをインポートします。

変更点②

actions.jsという外部ファイルをインポートしています。react-micro-container版ではコンテナコンポーネント内に直接記述していたhandleIncrementhandleDecrementをこちらのactions.jsに切り出しています。

変更点③

変更点①に合わせて、MicroContainerを継承していた部分をNanoxを継承するように変更しています。

NanoxMicroContainerを継承していくつかの機能拡張を付加したクラスです。

変更点④

react-micro-container版ではthis.subscribe()でイベント登録を行っているところを、Nanox版ではthis.registerActions()というメソッドでイベント登録を行っています。

見た感じはthis.subscribe()と同じように見えますが、this.registerActions()では内部で色々といい感じにやってくれた後にthis.subscribe()でイベント登録をしています。

具体的に何がいい感じなのかについては後述します。

変更点⑤

変更点②に合わせて、react-micro-container版で記述していたhandleIncrementhandleDecrementがなくなっています。

マウント

react-micro-containerの記事から抜粋

このコンポーネントも単なるReact Componentなのでこのコンテナを普通にマウントするだけ。

app.js
import ReactDOM from 'react-dom';
import CounterContainer from './container/counter';

ReactDOM.render(<CounterContainer />, document.getElementById('app'));

こちらもreact-micro-container版とNanox版で違いはありません。

まとめ

以上のように、Nanoxでreact-micro-containerから拡張したのはコンテナコンポーネントの部分だけという事になります。

それ以外はreact-micro-containerと同じ使用感で書くことができます。

Nanoxにおけるアクション登録方法

上記使用方法で紹介したコンテナコンポーネントの説明でさらっと流していた部分について掘り下げていきます。

actions.jsの内容

上記変更点②でactions.jsという外部ファイルにイベント処理を切り出しました。ここではそのactions.jsの内容について説明します。

以下はreact-micro-container版でのhandleIncrementhandleDecrementを同期、非同期で実装したイベントのサンプルです。

なお、以降はアクションという名称で呼んでいきます。

actions.js
const actions = {
  increment(count) {  // ポイント①
    const currentState = this.getState();  // ポイント②
    return {  // ポイント③
      count: currentState.count + count
    };
  },

  decrement(count) {
    return new Promise((resolve, reject) => {  // ポイント④
      setTimeout(() => {
        const currentState = this.getState();
        resolve({  // ポイント⑤
          count: currentState.count - count
        });
      }, 1000);
    });
  }
};

// ポイント⑥
export default actions;

ポイント①

incrementというのがアクション名です。子コンポーネントから実行されるthis.dispatch()で指定されるやつです。

                                            コレ
                                             
<button onClick={() => this.props.dispatch('increment', 1)}>+1</button>

アクション名の後に指定されている引数のcountthis.dispatch()でアクション名の後に指定されたものです。複数指定も可能です。

                                                        複数指定した場合
                                                           
<button onClick={() => this.props.dispatch('increment', 1, 2, 3)}>+1</button>

複数指定した場合のアクションでの受け取り方は以下のようになります。

increment(x, y, z) {
  // x = 1, y = 2, z = 3

ポイント②

アクション内ではthis.getState()というメソッドによって現在のコンテナコンポーネントの最新のstateを取得できます。

なお、ここで取得したstateはコンテナコンポーネントのstateのコピーなので、この値を直接変更してもコンテナコンポーネントのstateは更新されません。

ポイント③

このアクション内で更新したい部分だけをオブジェクトで返すことにより、コンテナコンポーネントのstateが更新されます。

ポイント④

Promiseを返すことにより非同期アクションの実装も可能です。

ポイント⑤

Promise内の処理で、ポイント③と同様に更新したい部分のオブジェクトをresolveすることによりコンテナコンポーネントのstateを更新することができます。

ポイント⑥

アクションをエクスポートしてコンテナコンポーネントからインポートできるようにします。

まとめ

ぶっちゃけ、別ファイルに切り出さずにコンテナコンポーネント内のthis.registerActions()に直接アクションをぶっこんでしまってもよいのですが、コンポーネント部分は極力描画だけに専念してもらおうという計らいです。

this.registerActions()のいい感じなところ

1. シンプルなアクションハンドラ

react-micro-containerのthis.subscribe()で登録するイベントはあくまでもただのイベントなので、各イベントハンドラがthis.setState()でコンテナコンポーネントのstateを更新する所まで面倒を見る必要があります。

Nanoxのthis.registerActions()で登録したアクションは更新する部分のみを返すだけで良いというのと、非同期処理もPromiseで包むだけで良いので、毎回同じようなお作法的処理を書く必要がなく、処理がスッキリします。

2. アクション名のTypoを検知

react-micro-containerのthis.subscribe()で登録するイベントはあくまでもただのイベントなので(2回目)、this.dispatch()に指定するイベント名を間違えたとしても何も起きないだけで、イベント名を間違えたかどうかというところまでは分かりません。

Nanoxではthis.registerActions()で登録したアクション以外のアクション名がthis.dispatch()で指定された時にエラーで教えてくれます。

3. 自動エラーハンドリング

Nanoxのthis.registerActions()時にこっそりとエラーハンドリング用の隠しアクション(__error)が登録されます。

ユーザーが登録したアクション内で例外が発生したり、Promiserejectされた時には自動的にこの__errorアクションがdispatchされます。

各アクション内で個々にtrycatchしなくても、まとめて__errorがエラーの面倒を見てくれます。逆に面倒を見てほしくない場合は自前でtrycatchして例外を捕捉すればOKです。

4. エラーハンドラのカスタマイズ

Nanoxデフォルトの__errorアクションはエラー内容をconsole.errorで表示するだけですが、ユーザー側で登録するアクションに__errorという名称のアクションを実装することによってこの挙動を上書きすることができます。

まだ質問なんてないけどFAQ

Q. アクション内の処理開始時と終了時にsetState()したいんだけど
A. 例えばAjaxでサーバーからデータを取得する処理の開始時に「通信中」的な表示をするためにコンテナコンポーネントのstateに通信中フラグをたてて、通信が終了した時にそのフラグを解除するみたいなやつのことです。

確かに、更新部分を返すことによってコンテナコンポーネントのstateを更新する形だとこういった2段階にわたっての更新はできないように思えます。

が、アクション内ではthis.dispatch()によって別のアクションを呼び出すことができるので、上記のようなケースはこのような感じで実装することができます。

const actions = {
    .
    .
    .

  getData(url) {
    // getDataアクション内でwaitingアクションを呼び出し
    this.dispatch('waiting', true);

    return new Promise((resolve, reject) => {
      fetch(url)
      .then((response) => response.json())
      .then((json) => {
        resolve({
          data: json,
          waiting: false // 通信終了時にコンテナコンポーネントのwaitingを解除
        });
      });
    });
  },

  waiting(flag) {
    // waitingアクションでコンテナコンポーネントのstateを更新
    return {
      waiting: flag
    };
  }

    .
    .
    .

なお、アクション内のthisは、このdispatch()と先ほど紹介したgetState()のみを実装しています。各アクションのあちこちでsetState()subscribe()を呼び出したり、stateを直接更新するような世紀末カオス状態に陥るのを防止するための制限となります。

デモ

Nanoxのソースリポジトリに同梱されているサンプルのデモになります。

さいごに

大勢のスタッフが関わる大規模アプリケーションならFlux、Reduxのようなガッチリとしたフレームワークがベストチョイスになると思いますが、個人や数人規模で管理していくものであればこれくらいこじんまりしてた方が見通しが良いのではないでしょうか。

まあ、同じようなコンセプト(バイクや軽自動車規模)のFlux系フレームワークはいくらでもあるとは思いますが、その中の選択肢の1つとして、こじんまりとはしつつも無茶はさせない制約と多少の気の利いたエラー検知機能を備えたNanoxを検討してみてはいかがでしょうか、なんて。

おっと忘れてたインストールは、

$ npm install nanox

で、ソースはここにありますのでPRなどありましたらよろしくお願いします。