「react-images-uploading」を使って画像のアップロード機能を実装する。目標はドラッグドロップで画像アップロードできるところまで。
使用するライブラリ
インストール
npm install --save react-images-uploading
使ってみる
サンプルページ見てみたけど、とりあえず最小限でできそうな感じでやってみる。
'use client' import React from 'react' import ReactImagesUploading, { ImageListType } from 'react-images-uploading' const ImageUploader = () => { const [images, setImages] = React.useState([]) function onChange(value: ImageListType): void { console.log('%o', { value }) } return ( <ReactImagesUploading value={images} onChange={onChange}> {({ onImageUpload }) => { return ( <button type="button" onClick={onImageUpload}> ここをクリックすると画像選択画面にいくはず </button> ) }} </ReactImagesUploading> ) } export default ImageUploader
実物はこんな感じ
こいつをクリックすると画像選択へ
Propsの型
childrenの指定の仕方が独特。こんなふうにもできるんだとちょっと勉強になった。
型は以下のようになっている。
export interface ImageUploadingPropsType { value: ImageListType; onChange: (value: ImageListType, addUpdatedIndex?: Array<number>) => void; children?: (props: ExportInterface) => React.ReactNode; multiple?: boolean; maxNumber?: number; acceptType?: Array<string>; maxFileSize?: number; resolutionWidth?: number; resolutionHeight?: number; resolutionType?: ResolutionType; onError?: (errors: ErrorsType, files?: ImageListType) => void; dataURLKey?: string; inputProps?: React.HTMLProps<HTMLInputElement>; allowNonImageType?: boolean; }
childrenは普通は、React.ReactNodeだけを返せばいいけど、このコンポーネントはchildren?: (props: ExportInterface) => React.ReactNode を受け取るようになっている。
つまりchildrenとしてReact.ReactNodeを返す関数 を入れろと言っている。それでその関数は、ExportInterface 型を引数に取る、ということ。
そんで、ExportInterface がどんな型かというと
export interface ExportInterface { imageList: ImageListType; onImageUpload: () => void; onImageRemoveAll: () => void; errors: ErrorsType; onImageUpdate: (index: number) => void; onImageRemove: (index: number) => void; isDragging: boolean; dragProps: { onDrop: (e: any) => void; onDragEnter: (e: any) => void; onDragLeave: (e: any) => void; onDragOver: (e: any) => void; onDragStart: (e: any) => void; }; }
さっきの例では、「onImageUpload: () => void」だけ使用した。
この関数はおそらく、処理が走ると、画像選択の画面に行く等関数だと思う。
なので、この関数をonClickなどに仕込むとクリック時に画像選択の画面に行ける。
const ImageUploader = () => { ...省略... return ( <ReactImagesUploading value={images} onChange={onChange}> {({ onImageUpload }) => { return ( // 引数に受け取ったonImageUploadをonClickに設定する <button type="button" onClick={onImageUpload}> ここをクリックすると画像選択画面にいくはず </button> ) }} </ReactImagesUploading> ) }
onChange
これがいつ発火されるか調べる。
これは多分画像を選択したら発火する。はず。。
試してみる。
現状はこんな感じ
const ImageUploader = () => { const [images, setImages] = React.useState([]) // こいつは画像選択したら発火されるはず function onChange(value: ImageListType): void { console.log('%o', { value }) } return ( <ReactImagesUploading value={images} onChange={onChange}> {({ onImageUpload }) => { return ( <button type="button" onClick={onImageUpload} className="rounded-full bg-slate-100 border-slate-500 border px-4 py-[2px] " > ここをクリックすると画像選択画面にいくはず </button> ) }} </ReactImagesUploading> ) } export default ImageUploader
さきほどのようにボタンをクリックして画像選択した後のconsole.logの様子。onChangeが発火されているっぽい
選択した画像を保存してみる
次のステップとして、選択した画像をストレージに保存してみたい。
今回はfirebaseが提供しているfire cloud store に選択した画像を保存してみる。
先ほどのonChangeには選択された画像が入っているはずだから、そこから画像を抜いて登録する方針でいく。
先ほどのonChangeの部分のみ変更した
function onChange(value: ImageListType): void { console.log('%o', { value }) const formData = new FormData() formData.append('image', value[0].file!) uploadImage(formData) }
onChangeの引数にはImageListTypeが来る。これがどんな型かというと
export declare type ImageListType = Array<ImageType>; export interface ImageType { dataURL?: string; file?: File; [key: string]: any; }
ImageTypeの配列でImageTypeのfileに実際のファイルがある。のでそれを引き抜いてアップロード関数に渡す。とりあえず今は動作検証だけしたいので、画像のチェックはせずにvalue[0].file! としてファイルを抜き出している。
わざわざFormDataに画像のデータを詰め直しているのは、アップロード関数がNext.jsのServer Actionという機能を使用しており、その仕様のため。(FormDataなどの一部のクラスを除いて基本的にはメソッドを持たないオブジェクトしか引数に渡せない)
アップロード関数の実装は本件とは趣旨がずれるので内容は省略するが、引数に受け取ったファイルを使って、firestorageへ画像をアップロードするだけである。
一応アップロードする関数のインターフェースだけ示すとこんな感じ
// FormData内にFileがある(実際はバリデーションが必要) uploadImage(formData: FormData): Promise<void>
今はアップロードするだけなので、返り値はvoidだが、反映した画像を表示したいとなったらストレージから返される公開URLを返すことになりそう。
実際に画像を選択したら保存されるか試してみる。
こんな感じでちゃんとFirestorage(この例では実際のFirestorageではなくエミュレータを使用した)に保存されていたのでよさそう。
アップロードした画像を表示する
画像が登録できたら次は保存した画像を表示したい。
これは保存した際に発行される公開URLをもらってから、stateを経由して渡してあげればよさそう。
なので方針としては、先ほどのonChange内で画像を保存したときに公開URLをもらえるようにして、処理の最後にstateに追加する。
その後際レンダリングが走り、state内の追加した公開URLを参照できるようになるはずなので、それをimgのsrcに設定すればOKなはず。
const ImageUploader = () => { const [images, setImages] = React.useState([]) const [uploadedImageURL, setUploadedImageURL] = React.useState<string | null>( null, ) // ★追加1 : 公開後の画像のURLを保存するState function onChange(value: ImageListType): void { const formData = new FormData() formData.append('image', value[0].file!) // ★追加2:アップロード時に公開URLを返すようにして、そのURLをstateに保存 uploadImage(formData).then((result) => { setUploadedImageURL(result) }) } return ( <> <ReactImagesUploading value={images} onChange={onChange}> {({ onImageUpload }) => { return ( <button type="button" onClick={onImageUpload} className="rounded-full bg-slate-100 border-slate-500 border px-4 py-[2px] " > ここをクリックすると画像選択画面にいくはず </button> ) }} </ReactImagesUploading> // ★追加3:公開URLが発行されたら画像を表示する {uploadedImageURL && ( <UploadedImage url={uploadedImageURL} className="mt-4" /> )} </> ) } // ★追加4:追加した画像を表示するコンポーネント const UploadedImage = (props: { url: string; className?: string }) => { return ( <div className={props.className ?? ''}> <div>アップロード後の画像c</div> <img src={props.url} /> </div> ) } export default ImageUploader
アップロード関数も先ほどは返り値がvoidだったが、公開URLを返すようにしたのでインターフェースが以下のように変更になった。
type UploadedPublicImageURL = string export async function uploadImage( formData: FormData, ): Promise<UploadedPublicImageURL>
画像選択後に保存した画像が表示された:)
ドラッグで画像を保存できるようにする
次は画像ドラッグで保存できるようにする。といっても簡単で、先ほどのonImageUploadを設定したコンポーネントにdragProps を渡すだけ。
<ReactImagesUploading value={images} onChange={onChange}> {({ onImageUpload, dragProps }) => { return ( <button type="button" onClick={onImageUpload} className="rounded-full bg-slate-100 border-slate-500 border px-4 py-[2px] " {...dragProps} > ここをクリックすると画像選択画面にいくはず </button> ) }} </ReactImagesUploading>
ドラッグ中かどうかを判定するにはisDrappingを使用する。ドラッグ中はtrueを返し、そうでない時はfalseを返してくれる。