<input type="file">
要素を使うと、ローカルのファイルが選べます。そして、取得した情報にもとづいて、ファイルが操作できるのです。本稿では選択するのは画像ファイルとし、そのイメージをページに差し込んでみます(サンプル001)。
サンプル001■React + TypeScript: Displaying image selected with <input type="file">
この記事は全3回のチュートリアルシリーズの第1回です。3回とおしてつぎのサンプル002ような画像ファイル操作のインタフェース例をつくります。<input type="file">
要素を共通に扱うものの、それぞれテーマは別です。興味に応じて、解説を個別に読んでいただいても構いません。
サンプル002■React + TypeScript: Change style of with MUI
チュートリアルシリーズ
- 「React + TypeScript: <input type="file">要素で選択した画像ファイルのイメージをページに表示する」(本稿)
- 「React + TypeScript: 動的に読み込んだ画像ファイルのイメージを縦横比は変えずに一定サイズに収める」
- 「React + TypeScript: <input type="file">要素の見映えのカスタマイズとファイルのドロップによる選択 ー MUIを用いて」
React + TypeScriptのひな型アプリケーションに<input type="file">
要素を加える
React + TypeScriptのひな型アプリケーションは、Create React Appでつくるか(「Create React AppでTypeScriptが加わったひな形アプリケーションをつくる」参照])、前掲サンプル001と同じCodeSandboxを用いるとよいでしょう。
<input type="file">
要素でファイルを選択するモジュールFileInput.tsx
は、新たに定めてルートモジュールsrc/App.tsx
の子コンポーネント(FileInput
)として差し込みます。accept
属性で選べるのは画像ファイルに制限しました。もっとも、ファイルを選択する操作ができるだけで、そのあとの処理はまだ書いていません。
import { FC } from 'react';
export const FileInput: FC = () => {
return (
<div>
<input type="file" accept="image/*" />
</div>
);
};
import { FileInput } from './FileInput';
import './styles.css';
export default function App() {
return (
<div className="App">
<FileInput />
</div>
);
}
選択したファイルの情報を得る
処理を進めるには、選んだファイルの情報を取り出さなければなりません。選択が変わったときのイベントはonChange
です。ロジックはカスタムフック(useHooks
)に切り分けることにします。
import { useHooks } from './hooks';
export const FileInput: FC = () => {
const { handleFiles } = useHooks();
return (
<div>
{/* <input type="file" accept="image/*" /> */}
<input type="file" accept="image/*" onChange={handleFiles} />
</div>
);
};
選んだファイルのリストをFileList
オブジェクトとして収めるのが<input type="file">
要素のfiles
プロパティです。今回は、要素にmultiple
属性を与えていません。したがって、リストの長さ(length
)は0か1です。0でなかったとき、最初の要素をコンソールで確かめてみることにしましょう。
import { ChangeEventHandler } from 'react';
export const useHooks = () => {
const handleFiles: ChangeEventHandler<HTMLInputElement> = (event) => {
const files = event.currentTarget.files;
if (!files || files?.length === 0) return;
const file = files[0];
console.log('file:', file);
};
return { handleFiles };
};
選択した画像ファイルのイメージをページに差し込む
選んだファイルの情報が得られましたので、画像イメージをページに表示します。モジュールsrc/FileInput.tsx
に加えたのは、イメージの差し込み先となる<div>
要素です。処理はフック(useHooks
)で動的に行うため、このあとフックに加えるrefオブジェクト(imageContainerRef
)を渡してref
プロパティに与えました。
export const FileInput: FC = () => {
// const { handleFiles } = useHooks();
const { handleFiles, imageContainerRef } = useHooks();
return (
<div>
<input type="file" accept="image/*" onChange={handleFiles} />
<div ref={imageContainerRef} />
</div>
);
};
モジュールsrc/FileInput.tsx
はこれででき上がりです。記述全体をつぎのコード001にまとめました。
コード001■ファイル選択と画像表示のモジュール
import { FC } from 'react';
import { useHooks } from './hooks';
export const FileInput: FC = () => {
const { handleFiles, imageContainerRef } = useHooks();
return (
<div>
<input type="file" accept="image/*" onChange={handleFiles} />
<div ref={imageContainerRef} />
</div>
);
};
画像イメージにはいくつかの扱い方があります。たとえば、「ウェブアプリケーションからのファイルの使用」が参考になるでしょう。今回用いるのは、HTMLImageElement
インスタンスをつくるImage()
コンストラクタです。Document.createElement('img')
と機能は変わりません。
imageContainerRef
は、ファイル選択と画像表示のコンポーネントに渡して、差し込み先要素(<div>
)のref
プロパティに定めるオブジェクトです。HTMLImageElement.src
プロパティに画像のURLを与え、差し込み先要素にインスタンスをNode.appendChild()
メソッドで加えればイメージが表れます。File
オブジェクトからURLを生成するのが、URL.createObjectURL()
メソッドです。
// import { ChangeEventHandler } from 'react';
import { ChangeEventHandler, useRef } from 'react';
const fileImage = new Image();
export const useHooks = () => {
const imageContainerRef = useRef<HTMLDivElement>(null);
const handleFiles: ChangeEventHandler<HTMLInputElement> = (event) => {
const imageContainer = imageContainerRef.current;
if (!imageContainer) return;
fileImage.src = window.URL.createObjectURL(file);
imageContainer.appendChild(fileImage);
};
// return { handleFiles };
return { handleFiles, imageContainerRef };
};
イメージの表示をリセットする
画像ファイルのイメージはページに表示されるようになりました。けれど、ファイル選択のダイアログで[キャンセル]ボタンをクリックしたとき、<input type="file">
要素には「選択されていません」と示されるのに、画像イメージは残ったままです。イメージの表示をリセットしなければなりません。
そこでフックのモジュールsrc/hooks.ts
に加えるのが、イメージ表示をリセットする関数resetSelection
です。おもにつぎの3つの処理を行います。
-
HTMLImageElement.src
プロパティのURLを空文字(''
)にする。 -
HTMLImageElement
インスタンスをコンテナ要素(imageContainer
)から除く。 - オブジェクトURLを
URL.revokeObjectURL()
メソッドで解放する。
// import { ChangeEventHandler, useRef } from 'react';
import { ChangeEventHandler, useRef, useState } from "react";
export const useHooks = () => {
const [objectURL, setObjectURL] = useState('');
const resetSelection = () => {
fileImage.src = '';
const imageContainer = imageContainerRef.current;
if (imageContainer && fileImage.parentNode === imageContainer) {
imageContainer.removeChild(fileImage);
}
if (objectURL) {
window.URL.revokeObjectURL(objectURL);
setObjectURL('');
}
};
const handleFiles: ChangeEventHandler<HTMLInputElement> = (event) => {
resetSelection();
if (!files || files?.length === 0) return;
const objectURL = window.URL.createObjectURL(file)
// fileImage.src = window.URL.createObjectURL(file);
fileImage.src = objectURL;
setObjectURL(objectURL);
};
};
ここで、オブジェクトURLについて、補っておきます。オブジェクトURLはFile
オブジェクトを識別する一意の文字列です。同じファイルについてURL.createObjectURL()
を呼び出しても、URLは同じにはなりません。そして、ドキュメントがアンロードされるまで、これらのオブジェクトURLは解放はされないのです(「オブジェクト URL を利用する」参照)。
試しに、前掲フックに加えたURL.revokeObjectURL()
メソッドの呼び出しをコメントアウトしましょう。そのうえで、画像ファイルをひとつ表示して、そのイメージのURLを別ウィンドウで開きます。つぎに、ダイアログで[キャンセル]ボタンを押し、ファイルが選択されていない状態にしてください。別ウィンドウで開いてあったURLをリロードしても画像は残ったままのはずです。
そのため、URL.revokeObjectURL()
を呼び出して、オブジェクトURLは明示的に解放しなければなりません。改めてコメントアウトは外して、呼び出しをもとに戻せば、前の画像はリロードすると表示されないでしょう。
ドラッグ&ドロップしたファイルのファイル型の制限
不具合はまだもうひとつ残っています。ファイルを<input type="file">
要素にドラッグ&ドロップして選んだときの、ファイル型(種類)の制限です。要素のaccept
属性の指定が、この操作には効きません。画像ファイルでなくても、ドロップできてしまうのです。
ファイル型はFile.type
プロパティで調べられます。そこで、フックのモジュールsrc/hooks.ts
の関数handleFiles
に加えたのがつぎの判定です。<input type="file">
要素のvalue
の値を空文字(''
)にすれば、「選択されていません」という状態になります。
export const useHooks = () => {
const handleFiles: ChangeEventHandler<HTMLInputElement> = (event) => {
const file = files[0];
if (!file.type.includes('image/')) {
event.currentTarget.value = '';
return;
}
}
};
これで、<input type="file">
要素で選択した画像ファイルのイメージを、ページに表示することができました。カスタムフックのモジュールsrc/hooks.ts
の記述全体は、つぎのコード002のとおりです。アプリケーションの動きは、冒頭に掲げたサンプル001でお確かめください。
コード002■ファイルと画像を扱うカスタムフック
import { ChangeEventHandler, useRef, useState } from "react";
const fileImage = new Image();
export const useHooks = () => {
const imageContainerRef = useRef<HTMLDivElement>(null);
const [objectURL, setObjectURL] = useState('');
const resetSelection = () => {
fileImage.src = '';
const imageContainer = imageContainerRef.current;
if (imageContainer && fileImage.parentNode === imageContainer) {
imageContainer.removeChild(fileImage);
}
if (objectURL) {
window.URL.revokeObjectURL(objectURL);
setObjectURL('');
}
};
const handleFiles: ChangeEventHandler<HTMLInputElement> = (event) => {
const files = event.currentTarget.files;
resetSelection();
if (!files || files?.length === 0) return;
const file = files[0];
if (!file.type.includes('image/')) {
event.currentTarget.value = '';
return;
}
const imageContainer = imageContainerRef.current;
if (!imageContainer) return;
const objectURL = window.URL.createObjectURL(file)
fileImage.src = objectURL;
imageContainer.appendChild(fileImage);
setObjectURL(objectURL);
};
return { handleFiles, imageContainerRef };
};