Johnman.md

プログラミングのことや個人的なことを書きます。たぶん。

react-dropzoneで画像サイズのバリデーションを行う

仕事でReactを書いていて、react-dropzoneを使う機会がありました。

react-dropzone.js.org

その中で、画像サイズ(縦x横)のバリデーションを行う際に手間取ったので、やり方を残しておこうと思います。


基本的な使い方

公式ページのUsageにも書かれていますが、下記が基本的な使い方です。

import React, {useCallback} from 'react'
import {useDropzone} from 'react-dropzone'

function MyDropzone() {
  const onDrop = useCallback(acceptedFiles => {
    // Do something with the files
  }, [])
  const {getRootProps, getInputProps, isDragActive} = useDropzone({onDrop})

  return (
    <div {...getRootProps()}>
      <input {...getInputProps()} />
      {
        isDragActive ?
          <p>Drop the files here ...</p> :
          <p>Drag 'n' drop some files here, or click to select files</p>
      }
    </div>
  )
}

この useDropZone でreact-dropzoneの設定を行います。

上記の例では onDrop 関数だけ定義しているので、バリデーションなどは特に行なっていない状態です。

react-dropzoneでのバリデーション

今回やりたいことは画像の縦x横のサイズをファイルがドラッグ&ドロップされた時点でバリデーションしたいというものでした。

react-dropzoneにはvalidatorという要素が用意されています。型定義は以下です。

validator: (file: File) => FileError | Array.<FileError>

File型の引数fileを受け取り、FileErrorかその配列を返す関数をvalidatorとして使用することができます。 エラーが返った場合には、弾かれた対象のファイルをuseDropzone から返ってくる fileRejections で取得できます。

validatorでimg.onloadを使う(失敗)

画像サイズのバリデーションを行う際、img要素のonloadイベントでwidthとheightを取得するのが一般的です。

これをreact-dropzoneのvalidatorで行うと、以下のようになります。

const imageValidator = (file) => {
  const errors = [];

  const img = new Image()
  img.onload = () => {
    if (img.width !== img.height) {
      errors.push({code: 'invalid-file-ratio', message: '画像は1:1にしてください'});
    }
    if (img.width < 140) {
      errors.push({code: 'file-width-too-small', message: '最小サイズは140x140です'});
    }
  }
  img.src = URL.createObjectURL(file);
  return errors.length ? errors : null;
}

const { getRootProps, getInputProps, fileRejections } = useDropzone({
  validator: imageValidator
});
...

imageValidatorでは、img要素を作成し、onloadイベントでwidth, heightを読み込み、サイズが期待通りでなければエラーを詰めるという処理を行なっています。

しかし、このコードではバリデーションが動きません。errorsが空配列として返ってしまいます。

その理由は、画像のloadが終わる前に関数が終了し、errorsを返してしまうからです

なので、画像のloadが終わるまで(実際にバリデーションが走るまで)待機し、それが完了した時点でerrorsを返す必要があります。

これはPromiseを使うことになりますが、validatorはPromiseに対応していません。そこで使うのが、getFilesFromEvent です。

getFilesFromEventでfileにwidth, height要素を付与する(成功)

getFilesFromEvent は、ドラッグ&ドロップされたファイルに対して任意の処理を行い、その結果を後続の処理で使うための関数です。

Promiseに対応しており、DragEventまたはEventを引数にとります。

この関数内でonloadイベントの完了を待ち、fileにwidthとheightの要素を追加します。

こうすることで、validatorでは直接widthとheightを取得して判定するだけでよくなります。

const fileGetter = async (event) => {
  const files = event.dataTransfer ? event.dataTransfer.files : event.target.files;
  const promises = [];

  for (let index = 0; index < files.length; index++) {
    const file = files[index];
    const promise = new Promise((resolve, reject) => {
      const image = new Image();
      img.onload = () => {
        file.width = img.width;
        file.height = img.height;
        resolve(file);
      };
      img.src = URL.createObjectURL(file);
    });
    promises.push(promise);
  }
  // onloadイベントの完了を待つ
  return await Promise.all(promises);
}

最終的なコード

最終的なバリデーションのコードは下記です。

const fileGetter = async (event) => {
  const files = event.dataTransfer ? event.dataTransfer.files : event.target.files;
  const promises = [];

  for (let index = 0; index < files.length; index++) {
    const file = files[index];
    const promise = new Promise((resolve, reject) => {
      const image = new Image();
      img.onload = () => {
        file.width = img.width;
        file.height = img.height;
        resolve(file);
      };
      img.src = URL.createObjectURL(file);
    });
    promises.push(promise);
  }
  return await Promise.all(promises);
}

const imageValidator = (file) => {
  const errors = [];

  if (file.width !== file.height) {
    errors.push({code: 'invalid-file-ratio', message: '画像は1:1にしてください'});
  }
  if (file.width < 140) {
    errors.push({code: 'file-width-too-small', message: '最小サイズは140x140です'});
  }
  return errors.length ? errors : null;
}

const { getRootProps, getInputProps, fileRejections } = useDropzone({
  getFilesFromEvent: fileGetter,
  validator: imageValidator,
});
...

まとめ

react-dropzoneで画像サイズのバリデーションを行う方法について書きました。

イベントの処理を待たずに終了しちゃったりするのが、あまり書き慣れてない自分にとっては結構ハマりやすいなぁと思います。