FileReader を Promise で扱いやすくするライブラリを作った
はじめに
こんにちは、株式会社TERASSの myrear です。
あるサービスの開発で FileReader を使うことがあったのですが、いくつかの点で扱いづらさを感じました。
そこで扱いやすくするための小さいライブラリを作ったのでご紹介します。
結論だけ知りたい!という方はリポジトリをご覧ください。
スターを付けてくれると喜びます⭐
FileReader とは
FileReader とはざっくり言うと File や Blob を読み取るための Web API です。
以下 MDN より抜粋です。
FileReader オブジェクトを使用すると、ウェブアプリケーションは、ユーザーのコンピューターに保存されているファイル(または生のデータバッファー)の内容を非同期に読み取ることができます。File または Blob オブジェクトを使用して、読み込むファイルまたはデータを指定します。
FileReader の扱いづらさ
まずは FileReader のどういったところが扱いにくいと感じたのかをサンプルコードとともに見ていきます。
以下は input[type="file"] な要素から選ばれた画像を img 要素に表示する React コンポーネントの例です。
export const ImageInput = () => {
const [dataUrl, setDataUrl] = React.useState<string>()
const onChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
const file = e.target.files?.item(0)
if (!file) return
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => setDataUrl(reader.result as string)
}
return (
<>
<input type="file" onChange={onChange} />
<img src={dataUrl} />
</>
)
}
簡単に処理の流れを追うと、
- 選択されたファイルを取得
-
FileReaderインスタンスの作成 -
readAsDataURLメソッドでファイルの読み込みを開始する -
loadイベントを待ち受けてresultをステートに保持する
となっています。
最低限の処理だけですが雰囲気だけ掴んでもらえればOKです。
Promise ではない
1つ目のイマイチポイントとして Promise ではないことが挙げられます。
今回の例では readAsDataURL を使っていますが、その他にも FileReader には readAsArrayBuffer readAsBinaryString readAsText とデータを読み込むためのメソッドが全4種類あります。
これらすべて戻り値は void であり、データの読み込みが完了したことを知るためには load イベントを別途監視しなければなりません。
もし戻り値が load イベント発火により解決する Promise であれば、処理を一つにまとめられそうです。
- reader.readAsDataURL(file)
- reader.onload = () => setDataUrl(reader.result as string)
+ reader.readAsDataURL(file).then(() => setDataUrl(reader.result as string))
result の型が曖昧
2つ目のイマイチポイントは result の型の曖昧さです。
FileReader.result の型は string | ArrayBuffer | null と定義されています。
というのも呼び出される readAs... メソッドによって result が変化するからですね。
読み込みが完了するまでは null なのでこの型なのも納得はできます。
ただ、今回の例のように readAsDataURL で読み込んだ場合は string として扱いたいのに、型が曖昧だと型アサーションなどで対応しなければならず少し面倒です。
load イベント発火により対応する型の値で解決する Promise にできれば取り回しやすそうです。
- reader.readAsDataURL(file).then(() => setDataUrl(reader.result as string))
+ reader.readAsDataURL(file).then(setDataUrl)
扱いやすくするためには
以上を踏まえると下記2点を満たすことができれば扱いやすくなりそうです。
-
readAs...メソッドはloadイベントの発火に合わせて解決するPromiseとする - 呼び出された
readAs...メソッドに対応した型の値でPromiseを解決する
というわけで、冒頭でも紹介しましたがこれらを満たすライブラリを作りました。
このライブラリは readAs 関数と FileReader クラスの2種類のモジュールを提供します。
readAs
本ライブラリのコア部分とも言える関数です。
オーバーロードによって上述の要件を表現しています。
load イベントでの解決の他に error や abort イベントでのリジェクトにも対応しています。
つまり FileReader.abort() が呼び出されたり読み込みに失敗すると Promise はリジェクトします。
FileReader
本ライブラリの本体です。
とはいっても各種 readAs... メソッドをオーバーライドして上記の readAs 関数を呼び出すようにしただけの薄いラッパークラスです。
globalThis.FileReader つまり Web API の FileReader を継承したクラスなので既存のコードベースを大きく変更することなく使えると思います。
適用してみる
上の方に挙げたコード例を本ライブラリを使って書き換えてみます。
+ import { FileReader } from '@myrear/file_reader_promise'
export const ImageInput = () => {
const [dataUrl, setDataUrl] = React.useState<string>()
const onChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
const file = e.target.files?.item(0)
if (!file) return
const reader = new FileReader()
- reader.readAsDataURL(file)
- reader.onload = () => setDataUrl(reader.result as string)
+ reader.readAsDataURL(file).then(setDataUrl)
}
return (
<>
<input type="file" onChange={onChange} />
<img src={dataUrl} />
</>
)
}
readAsDataURL と onload を別々に書いていたのが一つになり、 as による型アサーションも消えてコードの見通しが良くなりました。
類似ライブラリとの比較
実は似たようなことをやっているライブラリは(筆者が把握している限りでは)3種類あります。
簡単にですがそれぞれとの比較をしてみます。
promise-file-reader
readAsDataURL readAsText readAsArrayBuffer の3種類の関数をエクスポートしています。
-
readAsBinaryStringに非対応 -
readAsTextの文字コードが指定できない - 引数に渡せるのは
Blobのみ -
FileReaderインスタンスを内部で作成・保持しておりアクセスできない(abortなどができない)
@tanker/file-reader
Web API の FileReader に酷似した FileReader クラスをエクスポートしています。
- コンストラクタ引数で
Fileを受け取ることが可能 - Web API の
FileReaderを継承したクラスではない(abortメソッドは実装している模様) -
ArrayBufferのチャンク単位での読み込みに対応
file-reader-promise
FileReader ライクなオブジェクトをエクスポートする CommonJS ライブラリです。
- 型定義ファイルがない
-
Promiseをイベントオブジェクトとresultのどちらで解決するかを設定できる -
FileReaderインスタンスを内部で作成・保持しておりアクセスできない(abortなどができない)
おわりに
以上、 FileReader を扱いやすくするためのライブラリを作成・紹介しました。
実は Node ではなく Deno で作成したのですが、 Deno での開発体験は結構快適でした。
Deno で開発をする上で得た知見はまた別の機会に共有できればと思います。
最後までお読みいただきありがとうございました。
雑談
本題とは全く関係のない話になりますが、弊社には労働環境構築のために最大10万円の購入費が支給される Best Productivity という制度があります。(その他詳細はこちら)
筆者は最近この制度を利用しまして、 Kinesis Advantage360 Keyboard を購入いたしました。
パームレスト込でだいたい85000円くらい(!)でした。

初めての分割キーボードどころかこれまでずっとラップトップで生きてきた人間なので慣れるまでが大変でした(届いて2週間近く経った今でもタイポが多いです)が、肩が縮こまらず使えるので肩への負担を抑えられてかなり満足しています。
以上、人生初の高級キーボードを自慢したかっただけの雑談でした。
Discussion