LoginSignup
13
7

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-05-09

<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

チュートリアルシリーズ

  1. 「React + TypeScript: <input type="file">要素で選択した画像ファイルのイメージをページに表示する」(本稿)
  2. React + TypeScript: 動的に読み込んだ画像ファイルのイメージを縦横比は変えずに一定サイズに収める
  3. 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属性で選べるのは画像ファイルに制限しました。もっとも、ファイルを選択する操作ができるだけで、そのあとの処理はまだ書いていません。

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

export const FileInput: FC = () => {
	return (
		<div>
			<input type="file" accept="image/*" />
		</div>
	);
};
src/App.tsx
import { FileInput } from './FileInput';
import './styles.css';

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

選択したファイルの情報を得る

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

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オブジェクトとして収めるのが<input type="file">要素のfilesプロパティです。今回は、要素にmultiple属性を与えていません。したがって、リストの長さ(length)は0か1です。0でなかったとき、最初の要素をコンソールで確かめてみることにしましょう。

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プロパティに与えました。

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■ファイル選択と画像表示のモジュール

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>
  );
};

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

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

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()メソッドで解放する。
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 を利用する」参照)。

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

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

ドラッグ&ドロップしたファイルのファイル型の制限

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

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

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■ファイルと画像を扱うカスタムフック

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 };
};
13
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
7