完全に理解したオレオレFileListControlComponentを解説
JavaScriptのFileListの操作ってちょっと厄介ですよね.
Reactが絡むともっと厄介ですよね(本当は絡んでいないんですが)
今回は<input type="file" multiple />においてアップロードされたファイルを管理するコンポーネントを作ったので理解を深めながら解決していきます.
React, TypeScriptでやります.
今回はevent.targetとevent.currentTargetの違いに関してあまり考慮しておりません.その性質を考慮すると,もしかしたらもう少しスマートな実装ができるかもしませんので,ご意見などありましたら歓迎です.
作ったもの
以下の条件を満たす複数選択可能なファイル入力のUIです.
選択したファイル一覧が可視化されている
その一つ一つに削除ボタンがある
再度ファイルを選択すると,ファイルが追加される(再選択ではなく追加)
なにが厄介なのか
Reactでは基本的にはuseStateによって定義した変数で入力状態を管理します.
基本となる<input type="text" />の場合を見てみましょう
ここまでは何も問題ありません.
何が問題ないのかというと,インターフェースは一つであり,それから入力されたテキストは必ず,e.target.valueを介してinputTextに格納されるという点です.
<input type="file" />の場合
さて,ここからは実装方法が人によって異なってくるかと思います.
私はこのようにしました.
valueをどうするか
実は,<input type="file" />に対してvalueを指定しようとすると
InvalidStateError: Failed to set the 'value' property on 'HTMLInputElement': This input element accepts a filename, which may only be programmatically set to the empty string.
というエラーになります.この形式はvalueを受け付けることができませんので空の文字列を指定してくださいとのことです.
よってひとまずinputに渡すvalueは不要です.(これは無視できることにはなりません.inputが入力状態を何で管理するかという問題が残ります.後述します.)
FileListかFile[]か
考えうるもう一つの実装としては,
よく見る形式ですが,余分なArray.from()による変換や条件分岐があるので複雑に感じます.
いずれにせよe.target.filesは複数であることに注意
e.target.filesの型を見ます.
前提知識としてe.target.filesによって取得できるfilesは命名の通り複数です.そしてこのFileListは配列なように見えて配列ではなく,配列のメソッドは使用できません.このことが厄介の根源であると考えています.(否定的な意味ではない)
<input type="file" multiple />の場合
いよいよファイルを複数アップロードできるパターンについて考えます.
こちらは先程のものにmultipleを追加しただけです.おそらくなにも問題なく動くでしょう.
この手のコンポーネントはUIライブラリでも多く見かけます.
しかし,このコンポーネントのUX的な欠点は,「追加し直す場合に,すべてを選択し直す必要がある」という点です.
再度ファイルを選択すると,以前までの入力状態(選択したファイルたち)は失われます.
そこで,以下のような条件を満たすコンポーネントを作ります.(前述した作ったもの)
選択したファイル一覧が可視化されている
その一つ一つに削除ボタンがある
再度ファイルを選択すると,ファイルが追加される(再選択ではなく追加)
inputの選択状態を把握し,変更する必要が出てくる
最後の厄介な理由です.
inputの選択状態というのはe.target.filesの中のそれです.
しかし,e.target.filesによって取得している以上はコントロールしていることにはなりません.
なので,refを使用してコントロールしていこうと思います.
作ってみる
できた
ポイントは
handleChangeで元のファイルに追加されたファイルを追加し,重複するものは削除する
handleDeleteではinputRef.current.filesに対しても削除する処理を書くこと
ともに,inputRef.current.filesに対する操作とinputFilesに対する操作を忘れずに行う
解説していきます.
FileListを操作する術を知る
useRefを使う
前述したようにe.target.filesだけではコントロール不可なので,useRefを使用してinputのrefをコントロールできるようにします.
DataTransferを使う
FileListは配列なようで配列ではないというのが厄介ということでした.
これの操作を実現するのが,このDataTransferです.
とは言っても,new DataTransfer()として新たにFileListを作ってそれをrefに再代入しているだけなので,あまりスマートではないのかもしれません.
dt.items.add()で追加してdt.filesとすることで,FileListの型でrefに挿入できます.
実装完了
一応これで目的のUIを実現可能です.
ReactのstateとHTMLInputElement.filesの2重管理となるのが複雑な点ですね…