JavaScript
React
reactnative
react-primitives

react-primitivesってなにやってるんですか?! 〜ソースコードから要点を解説〜

はじめに

最近よく聞くフロントエンド周りで真のユニバーサルプラットフォームを目指すぜ!っていう文脈でreact-native-web, reactxpなどが出てきています。
今回は、その中でもreact-primitivesについてこのライブラリがなにをやっているのか、どうやって動いているのか要点を解説したいと思います。

react-primitivesの概要

そもそもreact-primitivesとはなんなのか。
react-primitivesは簡単に言うと、ReactNativeで書いたコードを、web, native, vr, sketch, windowsのプラットフォームを抽象化する層を挟むことで各プラットフォームで動作可能にし、reactのアプリケーションを開発する上で必要最低限のコンポーネントを集めたライブラリです。
primitivesという名前はこの共通の必要最低限のものと言った意味で命名されているようです。

react-native-webとの違いを最初は勘違いされる方が多いのですが、react-native-webはReactNativeの書き方でwebのdomをベースとしたreactに変換するものです。
つまり、webでは動きますが、nativeでは動きません。そのため、同じコードでどちらも動かそうとするためにはそれぞれのビルドを分ける必要があるのですが、それをreact-primitivesは、解消しています。
また、そのwebの部分はreact-native-webから一部のコンポーネントを取ってきて使用するように設計されているため、webにおいておおよその部分はreact-native-webが使用されています。

スクリーンショット 2018-09-13 16.29.37.png

使用できるコンポーネント・関数

このライブラリで公開されているコンポーネントは以下のようになっています。
- Animted
- View
- Text
- Image
- Touchable
- Easing
- Dimensions
- PixelRatio
- Platform

どうやってweb, native, vr, sketch等を分けているのか。

どうやって振り分けているのか、やっていることはかなり簡単で、プラットフォームごとに拡張子を変えて読み込むファイルを変えているだけです。先ほど、react-native-webでは、ビルドを分ける必要があったとしましたが、その部分をここでやってくれている。そんなところでしょうか。
実際に見てみますと、entryのファイルは以下のindex.jsで同じものの、

index.js
module.exports = require('./lib');

ここから読み込まれるlib以下のものが異なります。

$ ls lib/index*
lib/index.android.js lib/index.ios.js     lib/index.js         lib/index.sketch.js
lib/index.vr.js      lib/index.web.js     lib/index.windows.js

それぞれの実装はどうなっているのか。

それぞれのプラットホームごとの実装はどうなっているのか、まずsrc以下のディレクトリは以下のようになっています。

src
├── ReactPrimitives.js
├── index.android.js
├── index.ios.js
├── index.js
├── index.sketch.js
├── index.vr.js
├── index.web.js
├── index.windows.js
├── injection
│   ├── react-native-web.js
│   ├── react-native.js
│   ├── react-sketchapp.js
│   └── react-vr.js
├── modules
│   ├── PixelRatio.js
│   ├── Platform.js
│   └── Touchable.js
└── vr
    └── Touchable.js

その中でまずは、index.ios.jsを見てみましょう。
ここで登場するファイルについて追って行くと実装が見えてきます。

index.ios.js
require('./injection/react-native');

module.exports = require('./ReactPrimitives');

ReactPrimitives.jsのReactPrimitivesが外部から呼び出されるオブジェクトです。
ここでは、それぞれのコンポーネント・関数がkeyでvalueがnullのものとinjectという関数が見受けられます。
このinject関数では引数にそれぞれのプラットフォームごとにそれぞれのコンポーネントを定義したオブジェクトを受け取り、そのオブジェクトにそれぞれのコンポーネントのvalueが存在する時にnullだったものから受け取ったコンポーネントに書き換えています。

ReactPrimitives.js
const ReactPrimitives = {
  StyleSheet: null,
  View: null,
  Text: null,
  Image: null,
  Touchable: null,
  Easing: null,
  Animated: null,
  Dimensions: null,
  PixelRatio: require('./modules/PixelRatio'),
  Platform: require('./modules/Platform'),
  inject: (api) => {
    if (api.StyleSheet) {
      ReactPrimitives.StyleSheet = api.StyleSheet;
    }
    if (api.View) {
      ReactPrimitives.View = api.View;
    }
    if (api.Text) {
      ReactPrimitives.Text = api.Text;
    }
    if (api.Image) {
      ReactPrimitives.Image = api.Image;
    }
    if (api.Touchable) {
      ReactPrimitives.Touchable = api.Touchable;
    }
    if (api.Easing) {
      ReactPrimitives.Easing = api.Easing;
    }
    if (api.Animated) {
      ReactPrimitives.Animated = api.Animated;
    }
    if (api.Dimensions) {
      ReactPrimitives.Dimensions = api.Dimensions;
    }
    if (api.Platform) {
      ReactPrimitives.Platform.inject(api.Platform);
    }
  },
};

module.exports = ReactPrimitives;

実際に上のinject関数がどう使われているのかを見てみましょう。
ここではmodules以下のreact-native.jsを見てみます。
react-nativeをそのままほとんどinjectしているのがわかります。
また、Touchableコンポーネントのような自前のハックを行ったりするそのままではないコンポーネントをinjectしている場合があります。
特に、react-native-web.jsなどではそういうパターンがよくみられます。
ここは割愛しますが、Touchableではwebだとカーソルをつけるだったり、Touchable.Mixinなるものを使ってTouchableを拡張したりして、それぞれのプラットフォームへの対応などを行っています。(ここで1秒間に2回しかボタンが押せないように調整されているのですごくパフォーマンスが悪く感じます、、)

injections/react-native.js
const ReactPrimitives = require('../ReactPrimitives');
const {
  Animated,
  View,
  Text,
  Image,
  StyleSheet,
  Platform,
  Easing,
  Dimensions,
  Touchable,
} = require('react-native');

ReactPrimitives.inject({
  StyleSheet,
  View,
  Text,
  Image,
  Easing,
  Animated,
  Platform: {
    OS: Platform.OS,
    Version: Platform.Version,
  },
  Dimensions,
  Touchable: require('../modules/Touchable')(Animated, StyleSheet, Platform, Touchable.Mixin),
});

こうしてReactPrimitivesオブジェクトがそれぞれのプラットフォームごとに書き換えられエクスポートされます。
以上が主な実装の概要です。理解いただけたでしょうか?
今回説明していない部分のソースコードもそれほど難しくありませんので、要点以外にも読みたいと思う方は読んでみてください。

まとめ

実装を見て、かなり薄いライブラリであることがわかっていただけたと思います。
実際に実務で使ってみた感想としては、導入も容易で、フラットで簡単なデザインを実装するのは容易だったりするのですが、TextInputがまだ実装していなかったり、web特有の擬似クラスを自分たちで実装する必要があったり(react-primitivesというかreact-native-webの問題ですが)と、なかなか苦しいところはあります。
しかし、開発でreact-primitivesでもう無理となった場合でもそれぞれのプラットフォームごとにビルドシステムを構築する等すれば容易に捨てることができますし、統一性、メンテナンス性などの面ではかなりいいものがあるように感じました。