2023.11.05最終更新日:
2023.11.05
はじめに
今回は、React + TypeScriptを使って画像をアップロードする機能の実装方法について、備忘録を兼ねて解説していきたいと思います。
なお、今回は添付した画像データを送信するところまでは解説していません。 フォームでのデータ送信まで含めた実装方法を知りたい方は以下の記事をご参照ください。
【React/TypeScript】単一画像をReact Hook Formを使って送信するエンジニアとして日頃の気づきやアウトプットを投稿しているブログです。koutaroinoue-log.com
画像を添付するInputを作成する
まずは画像を添付するための<input />
要素を用意していきます。
<input />
要素で直接実装しても良いですが、可読性の観点で今回は<input />
のラッパーコンポーネントを用意しています。
tsx// InputImage/index.tsx import React, { InputHTMLAttributes, forwardRef } from "react"; export type Props = { onChange: (event: React.ChangeEvent<HTMLInputElement>) => void; id: InputHTMLAttributes<HTMLInputElement>["id"]; }; const InputImage = forwardRef<HTMLInputElement, Props>( ({ onChange, id }, ref) => { return ( <input ref={ref} id={id} type="file" accept="image/*" onChange={onChange} hidden /> ); } ); export default InputImage;
input要素のそれぞれのプロパティの解説以下の通りです。
ref
: 要素の値を参照する。hidden
: input要素を非表示にする。type
:file
を指定することで任意のファイルを割り当てることができる。accept
: どのファイル種別を受け入れるかを記述する。id
: 後述の<label />
要素のhtmlFor属性と紐づく。
また、今回はファイル添付用する際のボタンを独自に実装していくので本<input />
要素自体はhidden
で隠しておきます。
デフォルトでは以下のような見た目のボタンとなります。
画像アップロード用のフィールドを作成する
先程作成したInputImage
を使用して、画像アップロード用のフィールドUIを作成していきます。
フィールドに加えて、この後実装するアップロードのキャンセルボタンも含めて作成しています。
tsx// App.tsx import { useRef } from "react"; import "./App.css"; import InputImage from "./InputImage"; const IMAGE_ID = "imageId"; const FIELD_SIZE = 210; function App() { const fileInputRef = useRef<HTMLInputElement>(null); return ( <> <label htmlFor={IMAGE_ID} style={{ border: "white 3px dotted", width: FIELD_SIZE, height: FIELD_SIZE, display: "flex", borderRadius: 12, justifyContent: "center", alignItems: "center", overflow: "hidden", cursor: "pointer", }} > 画像をアップロード {/* ダミーインプット: 見えない */} <InputImage ref={fileInputRef} id={IMAGE_ID} onChange={() => {}} /> </label> <div style={{ height: 20 }} /> {/* キャンセルボタン */} <button onClick={() => {}}>キャンセル</button> </> ); } export default App;
ひとまず、フィールドを作成しただけですので、まだアップロードはできませんが以下のようなUIになります。
そして、「+ 画像アップロード」をクリックすると、OS標準のファイル選択のウインドウが起動するかと思います。
ファイルをアップロードできるようにする。
続いて、ファイルをアップロードできるようにしていきます。
画像アップロードを行うhandleFileChange
という関数を作成します。
処理内容としては、添付されたファイルをchangeイベントで取得し、stateとして状態管理しています。(それをコンソールに出力)
tsx// App.tsx import { useRef, useState } from "react"; import "./App.css"; import InputImage from "./InputImage"; import React from "react"; const IMAGE_ID = "imageId"; const FIELD_SIZE = 210; function App() { const fileInputRef = useRef<HTMLInputElement>(null); const [imageFile, setImageFile] = useState<File | null>(null); const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { if (e.currentTarget?.files && e.currentTarget.files[0]) { const targetFile = e.currentTarget.files[0]; setImageFile(targetFile); } }; console.log(imageFile); return ( <> <label htmlFor={IMAGE_ID} style={{ border: "white 3px dotted", width: FIELD_SIZE, height: FIELD_SIZE, display: "flex", borderRadius: 12, justifyContent: "center", alignItems: "center", overflow: "hidden", cursor: "pointer", }} > 画像をアップロード {/* ダミーインプット: 見えない */} <InputImage ref={fileInputRef} id={IMAGE_ID} onChange={handleFileChange} /> </label> <div style={{ height: 20 }} /> {/* キャンセルボタン */} <button onClick={() => {}}>キャンセル</button> </> ); } export default App;
画像をアップロードすると、inputにファイルが添付されていることがわかります。
アップロード画像をプレビューする
続いて、添付した画像をプレビューする機能を作成していきます。
基本的な構造・流れは以下の通りです。
- ファイルのアップロードされる。(先述)
- state (
imageFile
)が更新されたら、画像URLを作成する。 - 画像URLがあれば(=ファイルが添付されていたら)、画像を表示し、なければ「+ 画像をアップロード」を表示する。
tsx// App.tsx import { useRef, useState } from "react"; import "./App.css"; import InputImage from "./InputImage"; import { useGetImageUrl } from "./useGetImageUrl"; import React from "react"; const IMAGE_ID = "imageId"; const FIELD_SIZE = 210; function App() { const fileInputRef = useRef<HTMLInputElement>(null); const [imageFile, setImageFile] = useState<File | null>(null); const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { if (e.currentTarget?.files && e.currentTarget.files[0]) { const targetFile = e.currentTarget.files[0]; setImageFile(targetFile); } }; // state (imageFile)が更新されたら、画像URLを作成する。 const { imageUrl } = useGetImageUrl({ file: imageFile }); return ( <> <label htmlFor={IMAGE_ID} style={{ border: "white 3px dotted", width: FIELD_SIZE, height: FIELD_SIZE, display: "flex", borderRadius: 12, justifyContent: "center", alignItems: "center", overflow: "hidden", cursor: "pointer", }} > {imageUrl && imageFile ? ( <img src={imageUrl} alt="アップロード画像" style={{ objectFit: "cover", width: "100%", height: "100%" }} /> ) : ( "+ 画像をアップロード" )} {/* ダミーインプット: 見えない */} <InputImage ref={fileInputRef} id={IMAGE_ID} onChange={handleFileChange} /> </label> <div style={{ height: 20 }} /> {/* キャンセルボタン */} <button onClick={() => {}}>キャンセル</button> </> ); } export default App;
tsx// useGetImageUrl.ts import { useEffect, useState } from 'react'; type Args = { file: File | null; }; export const useGetImageUrl = ({ file }: Args) => { const [imageUrl, setImageUrl] = useState(''); useEffect(() => { if (!file) { return; } let reader: FileReader | null = new FileReader(); reader.onloadend = () => { // base64のimageUrlを生成する。 const base64 = reader && reader.result; if (base64 && typeof base64 === 'string') { setImageUrl(base64); } }; reader.readAsDataURL(file); return () => { reader = null; }; }, [file]); return { imageUrl, }; };
画像URLを生成するロジック部分に関しては、useGetImageUrl.ts
という名前でカスタムHookとして切り出しています。
ここまでで、画像アップロード+プレビューの実装が完成しました。
UIを確認しましょう。
画像アップロードのキャンセル機能を実装する
最後の、アップロードした画像をキャンセルする実装を行なって完成させましょう。
handleClickCancelButton
という名前の関数名を作成し、キャンセルボタンが押されたらstateとして管理しているimageFileをnullとした上で、inputからも値を削除してあげるだけでOKです。
tsximport { useRef, useState } from "react"; import "./App.css"; import InputImage from "./InputImage"; import { useGetImageUrl } from "./useGetImageUrl"; import React from "react"; const IMAGE_ID = "imageId"; const FIELD_SIZE = 210; function App() { const fileInputRef = useRef<HTMLInputElement>(null); const [imageFile, setImageFile] = useState<File | null>(null); const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { if (e.currentTarget?.files && e.currentTarget.files[0]) { const targetFile = e.currentTarget.files[0]; setImageFile(targetFile); } }; const handleClickCancelButton = () => { setImageFile(null); // <input />タグの値をリセット if (fileInputRef.current) { fileInputRef.current.value = ""; } }; const { imageUrl } = useGetImageUrl({ file: imageFile }); return ( <> <label htmlFor={IMAGE_ID} style={{ border: "white 3px dotted", width: FIELD_SIZE, height: FIELD_SIZE, display: "flex", borderRadius: 12, justifyContent: "center", alignItems: "center", overflow: "hidden", cursor: "pointer", }} > {imageUrl && imageFile ? ( <img src={imageUrl} alt="アップロード画像" style={{ objectFit: "cover", width: "100%", height: "100%" }} /> ) : ( "+ 画像をアップロード" )} {/* ダミーインプット: 見えない */} <InputImage ref={fileInputRef} id={IMAGE_ID} onChange={handleFileChange} /> </label> <div style={{ height: 20 }} /> {/* キャンセルボタン */} <button onClick={handleClickCancelButton}>キャンセル</button> </> ); } export default App;
おわりに
ファイルアップロード機能+プレビュー機能の実装は以上になります!
今回は画像をアップロードするところまでの解説でしたが、発展的な内容として、React Hook Formを使って添付した画像データを送信するところまでの実装方法についても別途解説していますので、気になった方はこちらの記事も併せてチェックしてみてください。
また、今回作成したソースコードを個人の公開リポジトリにアップしていますので、そちらもご利用ください。
GitHub - koutaro0205/react-single-image-upload: ブログ記事用の参考ソースコードブログ記事用の参考ソースコード. Contribute to koutaro0205/react-single-image-upload development by creating an accogithub.com
最後まで読んでいただきありがとうございました。
See You !!