🎯

【React】React-hook-form + react-dropzoneでドラッグ&ドロップ付きのフォームを作成する

2022/11/27に公開1

はじめに

こんにちは、itaと申します。

Webで最近流行りのUIの一つとして「ドラック&ドロップでファイルをアップロード」という仕様をよく見かけると思います。
実はReactで上記のUIを実現するためのライブラリとしてreact-dropzoneというものがあります。

一方、React Hook FormはReact用のフォームヘルパーとしてお馴染みの物となってきているでしょう。
そうなると、上記のドラック&ドロップの機能もReact Hook Formで取り扱える形にしたいという気持ちがあります。

そこで今回は、React Hook Formとreact-dropzoneの組み合わせで、ドラック&ドロップ機能付きのフォームの作成方法を書いていこうと思います。

今回のソースコードはこちらのリポジトリに公開いたしました。
以降ではこのソースコードを元に話を進めます。

目標

今回は、登録フォームのつもりで以下3つの項目を入力を可能とします。

  • 名前
  • メールアドレス
  • 画像ファイル

簡単のために3項目のみとしました。本来の登録フォームであればもっと項目は多くなるはずでしょう。
ちなみに、画像ファイルは「.png」、「.jpg」、「.jpeg」のみ許容することにします。

また、今回は項目を入力して送信すると入力内容がコンソールに出力されるようにします。
本来ならば、入力した情報をバックエンドに送ったり、またはワンクッションで確認ページに入力内容を表示したりするでしょう。

また、画像をアップロードしたら、画像のプレビューを表示する機能も実装してみます。

フォームの見た目を実装する

まずはhtml部分を書いていきます。この時点ではドラック&ドロップの部分の見た目は作りません。

RegistrationForm.tsx
import { FC } from 'react';
import './RegistrationForm.css';

const RegistrationForm: FC = () => {
  // 後に処理をここに記述

  return (
    <div className="registration-form-container">
      <h1>登録フォーム</h1>
      <form>
        <label htmlFor="name">
          名前
          <input type="text" id="name" />
        </label>
        <label htmlFor="email">
          メールアドレス
          <input type="email" id="email" />
        </label>
        <button type="submit">送信</button>
      </form>
    </div>
  );
};

シンプルなformの記述となっています。
cssの記述は今回は省略します。cssの記述やclassNameに関してはよしなに書いていけばいいでしょう。
以下のような見た目となります。

React Hook Formを使い、入力内容をコンソールに出力する

React Hook Formを導入しましょう。以下を実行します。

yarn add react-hook-form

インストールが完了したら、RegistrationForm.tsxを以下のように修正します。

RegistrationForm.tsx
import { FC } from 'react';
import { useForm, SubmitHandler } from 'react-hook-form';
import './RegistrationForm.css';

type FormData = {
  name: string;
  email: string;
};

const RegistrationForm: FC = () => {
  const { register, handleSubmit } = useForm<FormData>({
    defaultValues: {
      name: '',
      email: '',
    },
  });

  const onSubmit: SubmitHandler<FormData> = (data) => console.log(data);

  return (
    <div className="registration-form-container">
      <h1>登録フォーム</h1>
      <form onSubmit={handleSubmit(onSubmit)} action="/">
        <label htmlFor="name">
          名前
          <input type="text" id="name" {...register('name')} />
        </label>
        <label htmlFor="email">
          メールアドレス
          <input type="email" id="email" {...register('email')} />
        </label>
        <button type="submit">送信</button>
      </form>
    </div>
  );
};

export default RegistrationForm;

ポイントを説明します。
まず、フォームで取り扱うデータの型を定義しています。

type FormData = {
  name: string;
  email: string;
};

次に、useFormというHooksを用い、必要な関数等を呼び出しています。これで、FormData型の値を状態管理されるようになります。

  const { register, handleSubmit } = useForm<FormData>({
    defaultValues: {
      name: '',
      email: '',
    },
  });

registerを用いて、データの各項目を取り扱う場所を決めていきます。
例えば以下では、FormDataname属性の値は以下inputタグの値で管理されるように取り決めています。

<label htmlFor="name">
  名前
  <input type="text" id="name" {...register('name')} />
</label>

フォーム送信時にHooksで管理している値を用いるにはhandleSubmitを利用します。
下記、onSubmit関数は実際の処理を記述しています。handleSubmitの引数に定義した関数を入れたものをformタグのonSubmitpropsに入れてあげると、保持している値を呼び出して実際の処理に流すことができます。

 const onSubmit: SubmitHandler<FormData> = (data) => console.log(data);

  return (
  // 省略
      <form onSubmit={handleSubmit(onSubmit)} action="/">
        {/* 省略 */}
      </form>
  // 省略
  );

これで、名前とメールアドレスを入力して送信ボタンを押すと、コンソールに出力されるようになりました。

react-dropzoneでドラック&ドロップを実現する

react-dropzoneをインストールします。

yarn add react-dropzone

インストールが完了したら、RegistrationForm.tsxを以下のように修正します。

RegistrationForm.tsx
import { FC, useCallback, useMemo } from 'react';
import { useForm, SubmitHandler } from 'react-hook-form';
import { useDropzone } from 'react-dropzone';
import './RegistrationForm.css';

type FormData = {
  name: string;
  email: string;
  file: File | null;
};

const RegistrationForm: FC = () => {
  const { register, handleSubmit, setValue, watch } = useForm<FormData>({
    defaultValues: {
      name: '',
      email: '',
      file: null,
    },
  });

  const onDrop = useCallback((files: File[]) => {
    if (files.length > 0) {
      setValue('file', files[0]);
    }
  }, []);

  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDrop,
    accept: {
      'image/png': ['.png', '.jpg', '.jpeg'],
    },
  });

  const dropAreaBackground = isDragActive ? 'gray' : '';

  const watchFile = watch('file');

  const filePreview = useMemo(() => {
    if (!watchFile) {
      return <></>;
    }

    const url = URL.createObjectURL(watchFile);

    return <img src={url} alt="" className="file-preview" />;
  }, [watchFile]);

  const onSubmit: SubmitHandler<FormData> = (data) => console.log(data);

  return (
    <div className="registration-form-container">
      <h1>登録フォーム</h1>
      <form onSubmit={handleSubmit(onSubmit)} action="/">
        <label htmlFor="name">
          名前
          <input type="text" id="name" {...register('name')} />
        </label>
        <label htmlFor="email">
          メールアドレス
          <input type="email" id="email" {...register('email')} />
        </label>
        <p className="drop-area-title">プロフィール画像</p>
        <div {...getRootProps()} className={`drop-area ${dropAreaBackground}`}>
          <input {...getInputProps} />
          <p>
            ファイルを選択または
            <br />
            ドラッグアンドドロップ
          </p>
        </div>
        {filePreview}
        <button type="submit">送信</button>
      </form>
    </div>
  );
};

export default RegistrationForm;

ポイントを説明します。
まず、管理するオブジェクトにプロパティを追加していきます。
アップロードするファイルを取り扱うためのプロパティです。

type FormData = {
  name: string;
  email: string;
  file: File | null;
};

useFormにて、追加でsetValuewatchを呼び出しています。

const { register, handleSubmit, setValue, watch } = useForm<FormData>({
  defaultValues: {
    name: '',
    email: '',
    file: null,
  },
});

以下がファイルのドラック&ドロップを実現している記述です。react-dropzoneで提供されるuseDropzonehooksを使います。
onDrop関数は、ファイルアップロードが完了したときの処理を記述しています。setValueuseFormから呼び出した関数で、フォームで管理しているオブジェクトの指定したプロパティを変更します。ここでは、fileプロパティをアップされたファイルの一番最新のものに変更する処理を記述しています。
registerを使わずsetValueで値の変更処理をする理由は、後に対象のinputタグにuseDropzonegetInputPropsを埋め込むからです。
getRootPropsは、タグに埋め込むことで、そのタグの範囲がドラック&ドロップができる対象範囲となります。後にdivタグに埋め込みます。
isDragActiveは、boolean型の値で、ドラッグ中にtrueとなります。dropAreaBackgroundを三項演算子の形で定義していますが、これはドラッグ対象範囲のタグのclassNameにする予定です。isDragActivetrueとなることで、classNameが切り替わる仕組みです。

  const onDrop = useCallback((files: File[]) => {
    if (files.length > 0) {
      setValue('file', files[0]);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDrop,
    accept: {
      'image/png': ['.png', '.jpg', '.jpeg'],
    },
  });

  const dropAreaBackground = isDragActive ? 'gray' : '';

アップロードされた画像のプレビュー用のコンポーネントfilePreviewが以下のようになります。watchuseFormから呼び出すことのできる関数で、フォームで管理しているオブジェクトの指定したプロパティの値を監視できます。watchFileの値の変更を検知し、出力する画像を変えるという仕組みです。

  const watchFile = watch('file');

  const filePreview = useMemo(() => {
    if (!watchFile) {
      return <></>;
    }

    const url = URL.createObjectURL(watchFile);

    return <img src={url} alt="" className="file-preview" />;
  }, [watchFile]);

ドラック&ドロップと画像プレビュー部分のUIですが、前述の通りgetRootPropsgetInputPropsをそれぞれ対象のタグにpropsとして埋め込むことで、ドラック&ドロップできる範囲を指定しています。また、cssは割愛しますが、dropAreaBackground"gray"となることでドラック中に背景色が灰色に変わります。
また、ファイルプレビューのコンポーネントもその下で読み込んでいます。

<div {...getRootProps()} className={`drop-area ${dropAreaBackground}`}>
  <input {...getInputProps} />
  <p>
    ファイルを選択または
    <br />
    ドラッグアンドドロップ
  </p>
</div>
{filePreview}

これで、ドラッグ&ドロップ機能付きのフォームが完成しました!
ドラッグ中は対象範囲が灰色になり、アップロードができると画像のプレビューが表示されます。


また、送信することで、名前とメールアドレスに加え対象ファイルの情報もコンソールに表示されています。

おわりに

いかがでしたか?
より良い実装方法があれば意見等コメントいただけると大変助かります!
今回はバリデーションを実装しませんでしたが、YupZodなどのライブラリを導入することで、バリデーションも比較的容易に記述することが可能だと思います。

twitterのフォローもお願いいたします!
それでは!

参考

TypeScriptでreact-dropzoneを使ってドロップされたファイル名の表示
React + react-dropzone: ファイルをページにドラッグ&ドロップする
How to use react-dropzone with react-hook-form

Discussion