Reactをv16に上げるために行ったこと

去年の9月、Reactのv16がリリースされた。

自分も、15.xからバージョンアップする機会があった。

破壊的変更もいくつかあるが以前からアナウンスされていたし、あまり難しくはないはず。
何より、公式が丁寧なドキュメントやツールを用意しているから、それを使えばハマりどころはないと思う。

とはいえせっかく作業したので、記録しておく。

自分が行ったのは、廃止になったReact.PropTypesの対応、MatとSetのPolyfillの導入、Enzymeのバージョンアップ、など。

React.PropTypes

v16からReact.PropTypesが別パッケージになり本体から削除されたので、使っている場合は別パッケージであるprop-typesに置き換える必要がある。

jscodeshiftreact-codemodを使えば、簡単に置き換えることが出来る。

使用方法は以下の通り。

  1. $ npm install -g jscodeshift
  2. https://github.com/reactjs/react-codemod.gitをクローンしてくる
  3. 上記リポジトリ$ npm i
  4. $ jscodeshift -t react-codemod/transforms/React-PropTypes-to-prop-types.js <対象となるディレクトリやファイルのパス>

対象となるコードのなかでFlowを使っていると、4のコマンドを実行した際にTransformation errorになってしまうことがある。
そのときはコマンドの末尾に--parser=flowとつけて実行すればよい。

どうやらPropTypesの置き換えだけを行うわけではなく、よかれと思ってLintのようにインデントのずれを直したりするので、一応、意図しない差分が含まれていないかチェックしたほうがいい。

もちろん、prop-typesがまだ入っていなければ、npmインストールしておく必要がある。

MapとSet

v16から、Reactを動かすためにはMapSet、そしてrequestAnimationFrameが必要になった。
JavaScript Environment Requirements - React

これらに対応していないブラウザでもv16以上のReactを動かそうと思ったら、対応が必要になる。

例えば、Androidの標準ブラウザは、MapSetに対応していない。

職場の実機で試したところ大丈夫だったのだが、エミュレータでは予想通りのエラーが出た。

Uncaught ReferenceError: Set is not defined 

自分が担当しているウェブアプリはAndroidの標準ブラウザもサポートしないといけないため、Polyfillを入れることにした。

core-jsをnpmインストールして、エントリポイントであるJSファイルに以下のように記述すれば、解決する。

import 'core-js/es6/map';
import 'core-js/es6/set';

Polyfillをどのように使うのかはプロジェクト毎にやり方があるだろうから、それに沿う感じで。

ちなみに、エミュレータでの動作確認では、この記事が役に立った。
chrome://inspect/でDevToolsを開けるのはすごく便利だ。
フロントエンドエンジニアがAndroid標準ブラウザをデバッグする – T – Medium

Enzyme

テストツールとしてEnzymeを使っている場合、Reactのv16へのバージョンアップに合わせて、Enzymeのバージョンを3以上に上げる必要がある。

v3からは、アダプターの導入と設定が必要になる。

enzyme/migration-from-2-to-3

$ npm i -D enzyme-adapter-react-16

enzyme-adapter-react-16を使うには、依存関係にあるreactreact-domreact-test-rendererが必要。

そして、configure()を行う必要がある。

import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({ adapter: new Adapter() });

テストの設定ファイルなどに書いておくのが妥当だろう。

なお、v3からはshallowの挙動が変わるので注意が必要。

参考資料

フロントエンドでバイナリファイルを扱うためのBlobオブジェクトとFileオブジェクト

このエントリで紹介するBlobFileFileReaderHTML5で利用可能になったAPIで、ECMAScriptで定義されているわけではない。
そのため、Node.jsには存在せず、ブラウザ環境でのみ利用できる。

Blob

Blobは、バイナリデータを表すimmutableなオブジェクト。

const blob = new Blob(['<xml>foo</xml>'], {type: 'text/xml'});
console.log(blob); // Blob(14) {size: 14, type: "text/xml"}

第二引数で設定しているtypeで、MIMEを設定できる。
何も設定しなかった場合は空の文字列になる。

File

Fileはその名の通りファイルを表すオブジェクトで、Blobを継承している。

const file = new File(['<xml>foo</xml>'], 'example.xml', {type: 'text/xml'});
console.log(file); // name: "example.xml", type: "text/xml", size: 14, などのプロパティを持つ
console.log(file instanceof File); // true
console.log(file instanceof Blob); // true

new File()の第二引数で、ファイル名を指定する(必須)。それ以外のプロパティは第三引数のオブジェクトで指定するが、これはオプションであり必須ではない。

<input type="file">DnDイベントで、ローカルファイルをFileとして取得できる。

// ドロップで取得するケース
// e はdropイベントのイベントオブジェクト
e.dataTransfer.files
// <input type="file"> で取得するケース
// e はchangeイベントのイベントオブジェクト
e.target.files

filesはその名の通り、Fileの配列。

2018/1/25 追記

はてなブックマークで以下のコメントを頂きました。

id:daichirata filesはFileの配列ではなくFileList、配列として扱いたいならArray.fromとかしないと駄目なのでは

はい、仰る通りです。
配列のように添え字を使って各要素にアクセスできる、というだけで、配列ではありません。
{0: File, 1: File, ...}という形式のオブジェクトですね。
大変失礼しました。

const {files} = e.dataTransfer;
console.log(files); // FileList {0: File(6740), 1: File(14292), length: 2}
console.log(files[0]); // File(6740)

console.log(files[0] instanceof File); // true
console.log(files instanceof FileList); // true
console.log(Array.isArray(files)); // false

const array = Array.from(files);
console.log(array); // [File(6740), File(14292)]
console.log(Array.isArray(array)); // true
console.log(files[0] === array[0]); // true

追記終わり

FileReader

Blobオブジェクトの中身に直接アクセスすることは出来ない。当然、それを継承しているFileオブジェクトも同様である。
アクセスしたい場合はFileReaderを使う。

まず、FileReaderインスタンスを作成する。
次に、onloadを設定する。これは、読み込みが終わったときに呼び出されるコールバック関数。
そして、readAsXXXのメソッドを使ってBlobを読み込む。

const blob = new Blob(['<xml>foo</xml>'], {type: 'text/xml'});
console.log(blob); // Blob(14) {size: 14, type: "text/xml"}

const reader = new FileReader();
reader.onload = () => {
  console.log(reader.result);
};
reader.readAsText(blob); // <xml>foo</xml>
reader.readAsArrayBuffer(blob); // ArrayBuffer(14) {}
reader.readAsDataURL(blob); // data:text/xml;base64,PHhtbD5mb288L3htbD4=
reader.readAsBinaryString(blob); // <xml>foo</xml>

最後に使っているreadAsBinaryString()は、現在では非推奨になっている。

バイナリファイルをURLで表現する

DataURL

上記のサンプルでも使っているreadAsDataURL()は、BlobをDataURL形式で読み出すメソッド。

では、DataURLとは何か。

これは、データをURL(data:で始まる文字列)で表現するための仕組み。
バイナリファイルの場合はBase64という形式でエンコードする。

サンプルで出力されたdata:text/xml;base64,PHhtbD5mb288L3htbD4=をブラウザのアドレスバーに入れると、<xml>foo</xml>と表示されることを確認できる。

URLの代わりとして使えるため、例えば、img要素のsrc属性にDataURLを使うことも出来る。

データそのものをURLで表現しているため、Cookieなどに保存したり、サーバーに渡したりすることが出来る。

BlobURL

DataURLと似たような仕組みとして、BlobURLがある。
こちらは、blob:で始まる文字列。

URL.createObjectURL()Blobを渡すと作成される。

const blob = new Blob(['<xml>foo</xml>'], {type: 'text/xml'});
const url = URL.createObjectURL(blob);

基本的な使い方はDataURLと同じで、これもアドレスバーに入れると確認できる。

ただ、以下の違いがある。

BlobURLは必ずユニークな文字列になる。同一のBlobを渡しても、その都度、異なるBlobURLが生成される。

そして、これが最大の違いだが、BlobURLの場合は、それ自体がデータを表現しているわけではない。
データはあくまでもブラウザに保存されており、BlobURLはそれにアクセスするためのキーに過ぎない。
データの有効期間は、ブラウザを閉じるまで。

そのため、速度やメモリが、DataURLよりも効率的になる。
その反面、それ自体にデータが入っているわけではないので、BlobURLをサーバーなどに渡してもデータにはアクセスできない。

Chromeの場合、以下のURLで有効なBlobURLを確認できる。
chrome://blob-internals/

使われなくなったBlobURLは自動的に消去されるが、URL.revokeObjectURL()で明示的に消去するのが望ましいとされる。

URL.revokeObjectURL(url);

参考資料