<input type="file"> を使った画像アップローダーやファイル読み込み処理の実装中に、「一度選んだファイルを再度選択しても change イベントが発火しない」「同じファイルをプレビューし直せない」という現象に遭遇したことはないでしょうか。
これは不具合というより、<input type="file"> の選択状態とブラウザのイベント仕様によって起こり得る挙動です。
この記事では、同一ファイル選択時に change が発火しない理由から、event.target.value = '' を使った最小構成の解決策、リセットを入れるタイミングの使い分け、cancel イベントの仕様や通常のフォーム送信時の注意点までを解説します。
input type=”file”で同じファイルを選び直してもchangeが発火しない原因
この挙動を理解するには、まず change イベントの基本的な役割を確認する必要があります。
change イベントは、基本的にファイルの選択内容が変更され、その選択が確定したときに発火します。
ユーザーがダイアログを開いて前回と全く同じファイルを選択した場合、ブラウザから見ると「直前と選択されているファイル状態が変わっていない」と判断されることがあります。
その結果、「値の変更はない」とみなされて change イベントが発火しません。
※ なお、HTML仕様上、同一ファイルの再選択を「選択を変更しなかった」と扱うか、新たな選択として扱うかはユーザーエージェント(ブラウザ)側に裁量があります。実務上は「同じファイルを再度選んでも change は発火しない場合がある」という前提で設計するのが安全です。
valueをリセットして同じファイルを選び直せるようにする
最もシンプルで広く使われている解決策は、ファイル取得処理が終わった直後に input 要素の value を空文字('')でリセットする 方法です。
const fileInput = document.querySelector('#file');
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (!file) return;
console.log('選択されたファイル:', file.name);
// 処理完了後に選択状態をクリアして、次回同じファイルでも change を発火可能にする
event.target.value = '';
});
処理の最後で value を空にしておくことで、次回ユーザーが同じファイルを選んだ場合でも「空の状態からファイルが選ばれた」という変更差分が成立するため、change イベントを発火させやすくなります。
file inputのvalueへ任意のファイルパスは代入できない
<input type="file"> は、セキュリティ上の理由から JavaScript側から任意の文字列やファイルパスを value に代入することは禁止 されています(空文字以外の文字列を設定しようとすると仕様上 InvalidStateError が発生します)。
// InvalidStateErrorになる
fileInput.value = 'C:\\path\\to\\image.png';
// 選択状態をリセットするための空文字指定は許可されている
fileInput.value = '';
ただし、選択状態をクリアする目的での「空文字('')の代入」は標準仕様として認められています。
リセット前にFileオブジェクトを取得する
event.target.value = '' でリセットする前に、必要な File オブジェクトを変数へ取得しておきます。
単一ファイルなら変数へ直接代入し、複数ファイルの場合は Array.from(event.target.files) などで配列として退避しておけば、input の選択状態をクリアした後も安全に各 File の読み込みや送信処理を継続できます。
click時にvalueをリセットする方法とタイミングの違い
リセットを行うタイミングとして、click イベント時にあらかじめクリアしておく手法もあります。
const fileInput = document.querySelector('#file');
// ファイル選択ダイアログが開く前にリセットする
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (!file) return;
console.log(file.name);
});
用途やUIの要件に応じて、リセットするタイミングを選択します。
1. change処理の最後でリセットする
- 特徴:ユーザーがファイルを選択し、スクリプト側で読み込みやプレビュー処理を行った直後にクリアする方式。
- 向いているケース:選んだ画像をその場で即座にCanvas描画やWebAssembly処理、即時アップロードへ回すような「ツール系Webアプリ」に最適です。処理フロー(選択 → 処理 → 初期化)が直感的でコードの意図が分かりやすくなります。
通常のフォーム送信を行う場合は注意
event.target.value = '' を実行すると、input 要素が保持している選択ファイル自体もクリアされます。その後に通常の <form> 送信を行う場合、そのファイルは送信対象に残りません。選択直後にプレビュー・変換・アップロードなどの処理を完了するUIには向いていますが、送信ボタンを押すまで input にファイルを保持したいフォームでは、処理後のリセットを使わない方が適しています。
2. ファイル選択前(click時)にリセットする
- 特徴:ダイアログを開く直前に既存の選択をクリアする方式。
- 向いているケース:画面上の表示と同期させつつ、選び直すときだけ同一ファイルも許可したいケース。
- 注意点:ダイアログを開いた後にユーザーが「キャンセル」を押した場合、直前の選択状態がすでに消えてしまっているという副作用があります。
cancelイベントで選択内容が変更されなかった場合を扱う
近年の主要ブラウザでは、<input type="file"> に cancel イベント が実装されており、広く利用可能です。
fileInput.addEventListener('cancel', () => {
console.log('ファイル選択は変更されませんでした');
});
ファイル選択ダイアログを開いて「キャンセル」ボタンを押した際や、Escキーで閉じた際など、「選択が変更されなかった」状態を検知できます。
※ 同一ファイルを再選択した場合、現在の主要ブラウザでは cancel が発火するケースがあります。ただしHTML仕様上は、同じファイルの再選択を「選択を変更しなかった」と扱うか、新たな選択として扱うかはユーザーエージェント側に裁量があります。そのため、cancel を同一ファイル再選択専用のイベントとして設計するのではなく、「選択内容が変更されなかった場合を扱うイベント」として考えるのが安全です。
multiple属性で複数ファイルを選択する場合
複数ファイル選択(<input type="file" multiple>)を許可している場合、event.target.files は複数の File を含んだ FileList になります。
fileInput.addEventListener('change', (event) => {
// 配列に変換して退避
const files = Array.from(event.target.files);
if (files.length === 0) return;
files.forEach(file => {
console.log(file.name);
});
// 複数選択の場合も value を空にすれば全体がリセットされる
event.target.value = '';
});
単一ファイルと同様に、全く同じファイル群(同じ組み合わせ)を再選択した場合は「変更なし」と判定される可能性があります。複数選択であっても、再選択を可能にする場合は Array.from() 等で配列化して保持した後に event.target.value = '' で一括クリアします。
valueをリセットしてもchangeイベントは自動発火しない
JavaScriptのフォーム操作における重要な原則として、スクリプト経由で fileInput.value = '' と値を書き換えても、ブラウザは input や change イベントを自動発火させない という仕様があります。
fileInput.value = ''; // これを実行しても、登録した change リスナーは呼ばれない
これにより、「リセットしたせいで意図しないイベントループが発生する」という心配はありません。fileInput.value = ”で値をクリアしただけでは、登録したinputやchangeイベントが自動的に発火することはありません。
リセットしてもうまく動かないときの確認点
value をリセットするコードを入れたのにうまく動かない場合は、以下のポイントを切り分けてみてください。
1. 早期returnでリセット処理を通らずに終了していないか
ファイルのバリデーション(容量チェックや拡張子チェック)でエラーになり、return で関数を抜けてしまうと、末尾に書いた event.target.value = '' まで到達しないことがあります。早期リターンする場合でもリセットを通すか、try...finally 等で確実にクリアされる設計になっているか確認してください。
2. Fileを取得する前にリセットしていないか
event.target.value = '' を実行すると、その瞬間に event.target.files の中身もクリアされます。必ず files から各 File(または配列化したリスト)を取り出して変数に格納した 後 にリセットを行ってください。
// NGな例:先にクリアすると files が取得できない
event.target.value = '';
const file = event.target.files[0]; // undefined
// OKな例:取得してからクリアする
const file = event.target.files[0];
event.target.value = '';
3. DOMの再生成でイベントリスナーが外れていないか
フレームワーク(React, Vueなど)や独自スクリプトでDOMの再生成を行っている場合、イベントリスナーが外れていたり、別の要素を参照していないか確認します。
4. multipleで選択した複数ファイルに対応しているか
単一ファイルのみを想定したコード(files[0] だけを処理して終わるなど)になっていないか、複数選択時のハンドリングを確認します。
5. モバイルブラウザやWebViewで確認しているか
モバイル端末のSafari/ChromeやWebView環境では、ファイル選択ダイアログの挙動やイベント発火のタイミングがデスクトップと微妙に異なる場合があります。実機での動作確認を行ってください。
まとめ
<input type="file"> で同一ファイルを選んだときに change が発火しないのは、ブラウザが「選択内容に変更がない」と判定しているためです。
- 基本の解決策:ファイル取得処理の直後に
event.target.value = ''で選択状態をリセットする(選択したファイルをその場で処理するUI向け) - フォーム送信時の注意:
value = ''を行うと通常のフォーム送信時にファイルが送られなくなるため、フォームの性質に合わせて使い分ける - 安全なデータ保持:必ず
FileオブジェクトやArray.from(files)でデータを退避してからvalueをクリアする
同じファイルを繰り返し読み込ませるツールや即時プレビュー機能では、処理完了時のリセット処理をセットで組み込んでおくのが実務的な定石です。