React + TypeScript: <input type="file">要素で選択した画像ファイルのイメージをページに表示する


[`<input type="file">`](https://developer.mozilla.org/docs/Web/HTML/Element/input/file)要素を使うと、ローカルのファイルが選べます。そして、取得した情報にもとづいて、ファイルが操作できるのです。本稿では選択するのは画像ファイルとし、そのイメージをページに差し込んでみます(サンプル001)。

#### サンプル001■React + TypeScript: Displaying image selected with \<input type="file"\>
https://codesandbox.io/s/react-typescript-displaying-image-selected-with-input-type-file-v1j229

この記事は全3回のチュートリアルシリーズの第1回です。3回とおしてつぎのサンプル002ような画像ファイル操作のインタフェース例をつくります。`<input type="file">`要素を共通に扱うものの、それぞれテーマは別です。興味に応じて、解説を個別に読んでいただいても構いません。

#### サンプル002■React + TypeScript: Change style of <input type="file"> with MUI
https://codesandbox.io/s/react-typescript-change-style-of-input-type-file-with-mui-ufvjqt

**チュートリアルシリーズ**
1. 「React + TypeScript: \<input type="file"\>要素で選択した画像ファイルのイメージをページに表示する」(本稿)
2. 「[React + TypeScript: 動的に読み込んだ画像ファイルのイメージを縦横比は変えずに一定サイズに収める](https://qiita.com/FumioNonaka/items/524b65e5c7dcfc3cdb99)」
3. 「[React + TypeScript: \<input type="file"\>要素の見映えのカスタマイズとファイルのドロップによる選択 ー MUIを用いて](https://qiita.com/FumioNonaka/items/6b28f809648c5d47d32d)」


# React + TypeScriptのひな型アプリケーションに`<input type="file">`要素を加える
React + TypeScriptのひな型アプリケーションは、[Create React App](https://ja.reactjs.org/docs/create-a-new-react-app.html#create-react-app)でつくるか(「[Create React AppでTypeScriptが加わったひな形アプリケーションをつくる](https://www.fumiononaka.com/Business/html5/FN2006001.html#sec01)」参照])、前掲サンプル001と同じ[CodeSandbox](https://codesandbox.io)を用いるとよいでしょう。

`<input type="file">`要素でファイルを選択するモジュール`FileInput.tsx`は、新たに定めてルートモジュール`src/App.tsx`の子コンポーネント(`FileInput`)として差し込みます。[`accept`](https://developer.mozilla.org/docs/Web/HTML/Element/input/file#attr-accept)属性で選べるのは画像ファイルに制限しました。もっとも、ファイルを選択する操作ができるだけで、そのあとの処理はまだ書いていません。

```react:src/FileInput.tsx
import { FC } from 'react';

export const FileInput: FC = () => {
	return (
		<div>
			<input type="file" accept="image/*" />
		</div>
	);
};
```

```react:src/App.tsx
import { FileInput } from './FileInput';
import './styles.css';

export default function App() {
	return (
		<div className="App">
			<FileInput />
		</div>
	);
}
```


# 選択したファイルの情報を得る
処理を進めるには、選んだファイルの情報を取り出さなければなりません。選択が変わったときのイベントは`onChange`です。ロジックはカスタムフック(`useHooks`)に切り分けることにします。

```react:src/FileInput.tsx
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`](https://developer.mozilla.org/docs/Web/API/FileList)オブジェクトとして収めるのが`<input type="file">`要素の[`files`](https://developer.mozilla.org/docs/Web/HTML/Element/input/file#attr-files)プロパティです。今回は、要素に[`multiple`](https://developer.mozilla.org/docs/Web/HTML/Element/input/file#attr-multiple)属性を与えていません。したがって、リストの長さ(`length`)は0か1です。0でなかったとき、最初の要素をコンソールで確かめてみることにしましょう。

```react:src/hooks.ts
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`プロパティに与えました。

```react:src/FileInput.tsx
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■ファイル選択と画像表示のモジュール
```react:src/FileInput.tsx
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>
  );
};
```

画像イメージにはいくつかの扱い方があります。たとえば、「[ウェブアプリケーションからのファイルの使用](https://developer.mozilla.org/docs/Web/API/File/Using_files_from_web_applications)」が参考になるでしょう。今回用いるのは、`HTMLImageElement`インスタンスをつくる[`Image()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/Image)コンストラクタです。`Document.createElement('img')`と機能は変わりません。

`imageContainerRef`は、ファイル選択と画像表示のコンポーネントに渡して、差し込み先要素(`<div>`)の`ref`プロパティに定めるオブジェクトです。[`HTMLImageElement.src`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/src)プロパティに画像のURLを与え、差し込み先要素にインスタンスを[`Node.appendChild()`](https://developer.mozilla.org/docs/Web/API/Node/appendChild)メソッドで加えればイメージが表れます。`File`オブジェクトからURLを生成するのが、[`URL.createObjectURL()`](https://developer.mozilla.org/docs/Web/API/URL/createObjectURL)メソッドです。

```react:src/hooks.ts
// 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()`](https://developer.mozilla.org/docs/Web/API/URL/revokeObjectURL)メソッドで解放する。

```react:src/hooks.ts
// 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 を利用する](https://developer.mozilla.org/ja/docs/Web/API/File/Using_files_from_web_applications#%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88_url_%E3%82%92%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B)」参照)。

試しに、前掲フックに加えた`URL.revokeObjectURL()`メソッドの呼び出しをコメントアウトしましょう。そのうえで、画像ファイルをひとつ表示して、そのイメージのURLを別ウィンドウで開きます。つぎに、ダイアログで[キャンセル]ボタンを押し、ファイルが選択されていない状態にしてください。別ウィンドウで開いてあったURLをリロードしても画像は残ったままのはずです。

そのため、`URL.revokeObjectURL()`を呼び出して、オブジェクトURLは明示的に解放しなければなりません。改めてコメントアウトは外して、呼び出しをもとに戻せば、前の画像はリロードすると表示されないでしょう。


# ドラッグ&ドロップしたファイルのファイル型の制限
不具合はまだもうひとつ残っています。ファイルを`<input type="file">`要素にドラッグ&ドロップして選んだときの、ファイル型(種類)の制限です。要素の`accept`属性の指定が、この操作には効きません。画像ファイルでなくても、ドロップできてしまうのです。

ファイル型は[`File.type`](https://developer.mozilla.org/ja/docs/Web/API/File/type)プロパティで調べられます。そこで、フックのモジュール`src/hooks.ts`の関数`handleFiles`に加えたのがつぎの判定です。`<input type="file">`要素の[`value`](https://developer.mozilla.org/ja/docs/Web/HTML/Element/input#attr-value)の値を空文字(`''`)にすれば、「選択されていません」という状態になります。

```react:src/hooks.ts
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■ファイルと画像を扱うカスタムフック
```react:src/hooks.ts
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 };
};
```