カミナシ エンジニアブログ

株式会社カミナシのエンジニアが色々書くブログです

input[type="file"] 同じファイルを選んでもchangeイベントが発火しない問題をちょっとだけ深掘りするよ🎶

「よし、これで完璧!」と思って実装したファイルアップロード機能。テストで同じファイルを2回選んだら...あれ?2回目は何も起きない。

// さっきまで動いてたはずなのに...
<input
  type="file"
  onChange={(e) => {
    console.log('ファイル選択した!', e.target.files[0]);
    uploadFile(e.target.files[0]);
  }}
/>

「え、なんでonChange動かんの...?」

Chromeのコンソールとにらめっこすること数分。これ、実は私の実装ミスじゃなくて、ブラウザの仕様に起因する面白い問題だったんです。今日は、この挙動を掘り下げて、Web標準とブラウザによる挙動の違いについて話してみたいと思います。

そもそもchangeイベントってさぁ

教科書的な説明を確認してみる

MDNのchangeイベントのドキュメントを読むと、changeイベントは「要素の値が変更されたとき」に発火するとあります。で、<input type="file">の場合は、内部的なvalueプロパティ(ファイルパスが入るやつ)が変わったかどうかで判断してるんですね。

つまりChrome的にはこう:

  • 同じファイル選択 = valueは変わってない = changeイベント出さんでええやろ

まあ、理屈は通ってる...けど、ちょ、待てよ。(木村)

ユーザー(僕)の声を代弁すると

実際にこんなケースありません?

  1. エラーったから再アップロード: 「お、なんかエラー出た。もう一回同じファイル送ってみる」
  2. ファイル編集して即確認: 「i-want-many-money.txtの中身を書き換えたから、同じ名前のまま再アップロードするぞ」
  3. とりあえずもう一回: 「なんかアップロードできたか不安だからもう一回選び直す」(ユーザーあるある)

これらって全部「同じファイルをもう一回選ぶ」という、ごく自然な操作なんですよね。ファイルピッカーで選んで「開く」押したら、それは「よし、これでいく!」っていう明確な意思表示のはず。

Web標準(WHATWG)の本音を探る

仕様書の奥深くに潜む一文

この謎を解くために、WHATWG HTML Living Standard)を読み漁りました。そしたら、ファイルピッカーの処理についてこんな記述が:

まず基本的な処理フロー:

  • ユーザーがキャンセルした(変更なし)→ cancelイベント発火
  • それ以外(変更あり)→ inputchangeイベント発火

ここまでは普通。でも問題はその後の注釈(Note)です:

"As with all user interface specifications, user agents have a good deal of freedom in how they interpret these requirements. [...] Similarly, it's up to the user agent whether re-selecting the same files as were previously selected counts as a dismissal, or as a change of selection."

ざっくり訳すと:

「UIの仕様については、ブラウザには解釈の自由があるよ。[...] 同じファイルを再選択したときに、それを『変更なし』とするか『変更あり』とするかも、ブラウザ次第だよ」

え、丸投げかい!

ブラウザごとの実装の違い

実際に確認してみると、ブラウザによって挙動が全然違うんです。

Chrome/Safari派:セマンティクス重視

// Chrome/Safariの世界観
// 1回目: file.txt選択 → onChange発火! ✅
// 2回目: file.txt選択 → シーン... ❌

ChromeとSafariは「値が変わらないなら変更じゃないでしょ」という、理屈を重視した実装。まあ、言われてみればそうかも...と思いつつ、ちょっと融通が利かない感じ。

おそらく「changeイベントのセマンティクス(意味論)を厳密に守ろう」という設計思想があるんじゃないかとお察しします。(古畑)

Firefox派:ユーザー意図重視

// Firefoxの世界観
// 1回目: file.txt選択 → onChange発火! ✅
// 2回目: file.txt選択 → onChange発火! ✅(毎回ちゃんと反応)

Firefoxは「ユーザーがわざわざファイル選んでOK押したんだから、それは意味のある操作でしょ」という、ユーザーの気持ちを重視した実装。個人的にはこっちの方が自然に感じます。

それぞれの言い分(想像)

ブラウザ 挙動 たぶんこう考えてる
Chrome/Safari 発火しない 「値が同じなら変更じゃないっしょ」(理屈派)
Firefox 発火する 「選び直したってことは何か意図があるはず」(気持ち派)

Chrome/Safariの実装、これでええん?

ファイル名しか見てないという事実

色々触っていると、Chrome/Safariの実装はファイル名の文字列だけを比較してることが観測されます。

こんなケースで困る...

  1. hello.txtをinput[type='file']で選択
  2. hello.txtの中身をちょっと書き換える
  3. hello.txt選択 → onChange発火しない!(中身変わってるのに...)
  4. ファイルの最終更新日時
  5. ファイルサイズ
  6. ハッシュ値

この辺りも見て欲しいな...

実はバグかもしれない?

Chromium Issue Tracker(OSSのGithubのIssueと同じもの、Chromiumのバグ報告とか機能リクエストが飛び交う社交場、段差で踊るダンサダンサー)

にはUpload the same file doesn't trigger onchange event という本ブログと全く同じissueがあり、その中でChromiumの開発者側から面白いコメントがあります。

I don't know the rationale for the current behavior, or even whether it was intentional or accidental.

「この動作(二回同じファイルを選んでもonchangeが発火しない)ってのは、意図的なのか偶発的(バグ)なのかよーわかんねぇっすわ…」

…真相は闇の中…

とりあえず動くようにする方法

定番の回避策:valueをリセット

愚痴ってても仕方ないので、実践的な解決策を。王道は「処理の最後でvalueを空にする」です。

function handleFileSelect(event) {
  const files = event.target.files;

  if (files && files.length > 0) {
    console.log('選んだファイル:', files[0]);
    uploadFile(files[0]);
  }

  // 最後にvalueリセット
  // これで次回同じファイルでもonChange発火する!
  event.target.value = '';
}

まとめ

今回わかったのは、この挙動の違いは「バグ」じゃなくて「Web標準があえて曖昧にしてる」が故の実装の違いでした。

個人的には、Firefoxの「ユーザーが選んだんだから反応しようぜ」という姿勢の方が好きです。

でも現実的には、ChromeとSafariのシェアを考えるとevent.target.value = ''は必須。この一行、忘れずに書いときましょう。未来の自分が白目むかないためにも。

参考にした資料