search
LoginSignup
5
Help us understand the problem. What are the problem?

posted at

updated at

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

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

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
What you can do with signing up
5
Help us understand the problem. What are the problem?