React上でモデルを扱う

木内智史之介(シャッチョー)
ミンカさんけっこんしてくださいおねがいします(ズザー
SEGAさん、DIVAの筐体ください(ズザー

Reactをこよなく愛している皆さん。開発を楽しんでますか! 今日はモデルに関するお話をしたいと思います。

React + Reduxにおけるモデルの立ち位置

いわゆる一般的な React+Redux 構成のアプリケーションでは、「モデル」というものが登場しません。 特に何も意識しないで開発を進めると、おそらく plain object ベースでデータを扱う事になるのではないでしょうか?

せいぜい、storeに保存されるデータをimmutableにする程度かと思います。
(type scriptを使用する場合、interfaceとしてのモデルは登場するかとは思いますが)

正直なところ、もっと強くモデルを活用していけばいいのに、と思ってしまいます。

というわけで、活用してみる

Reactにおける、Modelの活用例(Wantedlyさんの場合)

Reactと言えばWantedlyさん!と言えるくらい、Reactへの熱い気持ちが伝わってくるWantedlyさんから、このモデルの件に関して言及している記事がすでに書かれています。 今回、この記事には本当に助けられました…!

React使い必見! Immutable.jsでReactはもっと良くなる

この記事を確認すると、どうもImmutable.jsには、immutableなMapやListを生成するだけでなく、モデルを作成するための機能があるようです。
Recordとな…ふむふむ。

Immutable.Recordを用いて、モデルを定義してみる

Immutable.jsでモデルを作成するには、Immutable.Record を活用するそうです。

1
2
3
4
5
6
7
import { Record } from 'immutable';

Shop = Record({
  name: '',
  tel: '',
  introduction: '',
});

Record関数は、このモデルのattributesを引数で受け取り、クラスのコンストラクターを返却します。
つまり、このままだと、モデルの属性の定義しかできず、ビジネスロジックの受け皿としては役割を果たせません。

そこで、Wantedly様よろしく、継承を用いてロジックの受け皿を作成したいと思います。

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Record } from 'immutable';

ShopRecord = Record({
  name: '',
  tel: '',
  introduction: '',
});

class Shop extends ShopRecord {
  function hoge() {
    alert('hage !');
  }
}

こうすることで、モデルとしての属性の定義、およびビジネスロジック実装場所の受け皿として、遺憾なく力を発揮する事ができるようになりました!

ネストしたjsonを取り扱うための工夫

さて、ここからが今回の記事の本丸です。

単一的なモデルであれば、上記までの対応でほぼほぼ問題なく対応できるかと思いますが、実際にはモデルは入れ子になる可能性が限りなく高いです。 今回のShopモデルに関しても、仮にCategoryモデルを参照したい、としましょう。

ShopモデルにCategoryモデルを持たせてみる

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Record } from 'immutable';

class Category extends Record({
  name: '',
} {
}

class Shop extends Record({
  name: '',
  tel: '',
  introduction: '',
  category: new Category(),
}) {
}

const shop = new Shop({name: 'some shop', category: {name: 'food'}});

console.log(shop.name, shop.category.name, shop.category);
// "some shop", "food", Object {name: "food"}

サクッとそれっぽく実装して、ログに流して確認してみます。

  • shop.name: 意図通りっぽい
  • shop.category.name: 意図通りっぽい
  • shop.category: plain objectになっちゃってる (Categoryインスタンスであってほしい)

はい、そうなんですね。Recordクラスに渡す値は、キーさえ一致していればよくて、その型までは検証も、補正も行われないのです。

うーん、それは微妙!

fromJSを拡張しよう!

じゃあどうすればいいの?と、色々対応方法を探してみると、github上で「まさに」のissueがありまして、そこで、一つの実装コードが引用されていました。

https://github.com/facebook/immutable-js/issues/385#issuecomment-179347662

issue上で引用されていた実装コード

1
2
3
4
5
6
7
8
9
10
11
12
Record.constructor.prototype.fromJS = function(values) {
  var nested = fromJS(values, function(key, value){
    //See https://facebook.github.io/immutable-js/docs/#/fromJS for docs on custom reviver functions
    if(this.prototype[key] && this.prototype[key].constructor.prototype instanceof Record){
      return this.prototype[key].constructor.fromJS(value.toJS()); //use toJS() here if nest more than once
    }
    else {
      return value;
    }
  }.bind(this));
  return this(nested);
};

ところが、上記のコードは、Record({}) で生成するクラスをそのまま活用する場合のコードで、今回のように、さらに1回extendsを挟む形だとうまく動作しませんでした。

なので、少しだけいじったコードを掲載しておきます。

上記のコードを extends Record({}) したクラスでも動作するように修正

1
2
3
4
5
6
7
8
9
10
11
12
13
Record.constructor.prototype.fromJS = function(values) {
  var nested = Immutable.fromJS(values, function(key, value){
    //See https://facebook.github.io/immutable-js/docs/#/fromJS for docs on custom reviver functions
    if(this.prototype && this.prototype[key] && this.prototype[key].constructor.prototype instanceof Record){
      return this.prototype[key].constructor.fromJS(value.toJS()); //use toJS() here if nest more than once
    }
    else {
      return value;
    }
  }.bind(this));
  // 関数の実行ではなく、new に修正
  return new this(nested);
};

extendsすることで、関数的な性質が飛んでしまったようですね。いやー、jsは奥が深い…。

実際に使うとこんな感じ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Record } from 'immutable';

class Category extends Record({
  name: '',
} {
}

class Shop extends Record({
  name: '',
  tel: '',
  introduction: '',
  category: new Category(),
}) {
}

const shop = Shop.fromJS({name: 'some shop', category: {name: 'food'}});

console.log(shop.name, shop.category.name, shop.category);
// "some shop", "food", Category {_map: Map}
  • shop.name: 意図通りっぽい
  • shop.category.name: 意図通りっぽい
  • shop.category: Categoryインスタンスとして取り込まれてr

素晴らしい!