41
41

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

[PR] React で Immutable な設計をうまく扱うためのライブラリを公開しました

Last updated at Posted at 2015-04-14

react-helixというライブラリを公開しました。
これは、私が作成したライブラリの PR 記事です。

react-helix は React で Immutable な設計をうまく扱うための小さなライブラリで、いわゆる Flux フレームワークの亜種になります。
亜種ですので Flux そのものではありませんが、その基本的な思想は継承しています。
その上で、ボイラープレートが不要なインターフェイスになるよう再設計してあります。

このライブラリの目標は、以下の3つです。

  • Action (ただの関数) を中心に、Model を Immutable にして、自然に View からビジネスロジックを引き剥がす。
  • Flux にあるボイラープレート (定数とかDispatcherへの処理登録とか) をゼロにする。
  • サーバーサイド レンダリングを妨害しない。

このライブラリを利用して作成したTODOアプリのサンプル

以下、①設計思想、②ライブラリの使い方、の順で書いていきます。

Overview Helix

                               This has the Application Model
                              +-------------------------------
                             /
                       +---------------------+
                       | The Root Component  |
                   +---| (Application class) |<<-+
                   |   +---------------------+   |
                   |                             |
Distribute changes |                             | Send actions
  via Virtual DOMs |                             | via Event Bubblings
                   |                             |
                   |   +---------------------+   |
                   |   |  Detail Components  |   |
                   +->>|                     |---+
                       +---------------------+
                                       /
     Those accept user's interactions /
    ---------------------------------+

まず最初に、このライブラリが実現する設計指針を、Helix と呼ぶことにします。

Helix は、Flux の最も基本的なコンセプト (単方向のデータフロー) を踏襲しつつ、より Immutable なデータ構造を扱いやすくします。

構成要素

Helix には3種類の構成要素があります。

  • Model は Immutable なデータ構造です。
  • Action は現在の Model を処理後の Model に変換する関数です。ビジネスロジックはここに実装されます。
  • View は Model を表示し、ユーザー操作等によって Action を発生させるコンポーネント群です。

Action はただの関数であるという点が、Helix の肝です。

データフロー

Model は Immutable なので、ルート コンポーネントがアプリケーション全体の Model の所有権を持ちます。そして、その Model を変更するのは、末端のコンポーネントです。
このギャップを埋めるために、Helix は対称性のある2種類のデータフローを持ちます。

  • Downward Flow は、Virtual DOMs (diff & patch) を利用して変更を配布する流れです。
  • Upward Flow は、Event Bubbling を利用してドメイン モデルの所有者へアクションを送る流れです。

前者はモデルを表示するための、後者は (ユーザー操作等によって) モデルを更新するための流れです。
Downward Flow は React が受け持ち、Upward Flow は react-helix ライブラリが受け持つことになります。

全体の流れを見てみましょう。
ユーザー操作等によって Model を更新する必要が生まれると、末端のコンポーネントが Send Action イベントを発行します。このイベントは Bubbling によって上へ上へと伝搬して、ルート コンポーネントへ到達します (Upward Flow)。
ルート コンポーネントはアプリケーション全体の Model を所有しています。受け取った Send Action イベントから Action を取り出して Model に適用・更新し、Virtual DOM の仕組みによって末端のコンポーネントへ変更結果を届けます (Downward Flow)。

これによって、モデルの所有権をルート コンポーネントに持たせたまま、末端のコンポーネントが Model 更新を主導することができます。そして、Flux にある定数群や Dispatcher/Store への登録作業のようなボイラープレートが不要になり、Action の増加に対してスケーラブルになるであろうという目論見です。

Overview react-helix

react-helix は、Helix を実現するための小さなライブラリです。どのくらい小さいかというと、Minifyすればこの文章よりも小さくなる程度です。
react-helix は2つのクラス(とMixin)を提供します。

  • StageComponent (StageMixin) は、Model を持ち、自身の子コンポーネントのいずれかで発生した Send Action イベントを拾って、自身の Model を更新する機構を提供するクラスです。
    前項の説明におけるルート コンポーネントのためのクラスになります。
  • AgentComponent (AgentMixin) は、指定した Action と引数で Send Action イベントを発生させる機構を提供するクラスです。
    前項の説明における末端のコンポーネントのためのクラスになります。

基本的な使い方は、単純な2ステップです。

  • Action を定義する:

    export function removeTodoItem(model, id) {
      return model.withItems(items =>
        items.filter(item => item.id !== id)
      );
    }
    
  • Action を発生させる:

    onRemoveButtonClick(/*event*/) {
      const id = this.props.value.id;
      this.request(removeTodoItem, id);
    }
    

Action を受け取って Model を更新する処理は、StageComponent (StageMixin) が自動的に行うため、ライブラリ利用者が意識する必要はありません。

インストール

npm install react react-helix

使い方

StageComponent / StageMixin

declare class StageComponent extends React.Component {
  constructor(props: any, stageValuePath: string = "");

  stageValue: any;
  setStageValue(value: any, callback?: () => void): void;

  filterAction(event: SendActionEvent): boolean;

  // 次のライフサイクル メソッドをオーバーライドする場合は、`super` を呼ぶ必要があります。
  componentDidMount(): void;
  componentWillUnmount(): void;
  componentWillUpdate(): void;
  componentDidUpdate(): void;
}

const StageMixin = {
  stageValue: any;
  setStageValue(value: any, callback?: () => void): void;

  // stageValuePath: string = "";
  //   stageValuePath プロパティを定義すると、componentWillMount の中で 
  //   stageValue と setStageValue を定義するために利用されます。

  // filterAction(event: SendActionEvent): boolean
  //   filterAction メソッドを定義すると、Send Action イベントを受信した時に、
  //   そのイベントを無視するかどうかを判定する事ができます。
};

stageValuePath

StageComponent のコンストラクタは、追加の引数 stageValuePath を持っています。
この引数は、どこに stageValue (= Model) を保持するかを決める文字列です。

この引数に "model" を渡すと、stageValuethis.state.model に保持されます。
この引数に "myapp.model" を渡すと、this.state.myapp.model に保持されます。
デフォルトは空文字列 (this.state に保持) です。

stageValue

stageValuePath の値を取得する Getter プロパティです。

setStageValue

stageValuePath の値を設定するメソッドです。
内部で this.setState を実行します。

filterAction

filterAction は、受信した Send Action イベントを無視するかどうかを決定するメソッドです。

  • event.action は Action です。
  • event.arguments は Action に渡す引数の配列です。
  • false を返すと、そのイベントを無視します。

デフォルトでは、常に true を返します。

AgentComponent / AgentMixin

declare class AgentComponent extends React.Component {
  constructor(props: any);
  request(action: (stageValue: any, ...) => any, ...args: any[]): void;
}

const AgentMixin = {
  request(action: (stageValue: any, ...) => any, ...args: any[]): void;
};

request

request は、指定した Action と引数で Send Action イベントを発生させるメソッドです。

コンポーネントの単体テストのために、requestメソッドをSpyで上書きすることができます。
ユーザー操作は最終的にrequestメソッドを呼ぶ事になります。


落ち葉拾い

Actions について、もっと深く

react-helix では、Action が Promise や Generator を返すことができます。
この場合、StageComponent は戻り値を特別に扱います。

Promise

Promise が返されると、それが Fulfilled になるまで待機して、その結果を stageValue に設定します。
このとき Promise の結果が関数だった場合は、第1引数にその時点の stageValue を渡して呼び出し、関数の結果値を stageValue に設定します。

function promiseAction(model) {
  // ↑ この model は Action が呼び出された時点の値です。
  return hogeAsync()
    .then(function() {
      // ↓ この model2 は Promise が Fulfilled になった時点の値です。
      return function(model2) { ... };
    });
}

Generator

Generator が返されると、それを最後まで進めながら、yield された値を stageValue に設定します (つまり Model を複数回更新できます)。
また、yield された値が Promise だった場合だけは特別に扱い、単に結果を yield に戻します (Model を更新しません)。

function* generatorAction(model) {
  // ↑ この model は Action が呼び出された時点の値です。

  // 単独の yield は、現在の Model を返します。
  const model2 = yield;

  // yield にオブジェクトを与えると、その値で Model を更新し、新しい Model 値を返します。
  const model3 = yield model.withStatus(1);

  // yield に Promise を与えると、その結果を返します (Model を更新しません)。
  const threeSevens = yield Promise.resolve(777);

  // yield に Rejected になる Promise を与えると、例外がスローされます。
  try {
    yield Promise.reject(new Error());
  }
  catch (err) {
    //...
  }
}

Generator を返す Action は、複雑なビジネスロジックを実装するために便利です。

StageComponent をネストさせる

StageComponent::filterAction() メソッドを活用すると、StageComponentをネストさせることもできます。

        +-------+
        | Stage |<---------+
        +-------+          |
         /     \           |
       +--+   +--+         |
       |  |   |  |         |
       +--+   +--+         |
      /   \      \         |
  +--+   +--+   +-------+  |
  |  |   |  |   | Stage |<-+ If filterAction() determines ignore, more bubbles.
  +--+   +--+   +-------+  |
  /      /      /     \    |
+--+   +--+   +--+   +--+  |
|  |   |  |   |  |   |//|--+
+--+   +--+   +--+   +--+

サーバーサイド レンダリング

react-helix は DOM Event を利用しているため、サーバーサイドでは動作しません。
しかし、ユーザー操作の結果を扱う仕組みであるため、動作する必要はないと考えています。
動作はしませんが、レンダリングを妨げることはありません (たぶん。まだテストしてない)。

ブラウザー互換性

react-helix の実装では、以下の機能を使っています:

  • Object.defineProperties
  • Function.prototype.bind
  • EventTarget.prototype.addEventListener
  • EventTarget.prototype.removeEventListener

従って、ES5 をサポートしていない IE8 以前では動作しません。


更新履歴

  • 2015/04/19
    解説順序が変だったので、全体的に修正。
41
41
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
41
41

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?