React-Hook-FormとSSGformを組み合わせてお問い合わせフォームを実装してみる

React-Hook-FormとSSGformを組み合わせてお問い合わせフォームを実装してみる
Scroll
公開日

私が運用しているこちらのWebサイトですが、実はフロントにGatsby.js、バックエンドにWordPressという構成で作成されています。

こういった構成のWebサイトをいわゆるJAMStack“JavaScript”、”API”、”Markup”の3つの技術から組み合わせられたシステム構成)と読んだりします。

オープンソースであるWordPressをあくまでもデータベースとしてだけ使用し、セキュリティリスクを最小限に抑えつつ、フロントであるGatsby.jsやNext.jsからはAPIで記事データや画像を取得してくるというシンプルな構成で、WordPressの弱点を補えるWebサイトの構築方法として個人的にはとても気に入っています。

しかしながら、「お問い合わせフォーム」などをサイトに組み込みたい!となった場合には自前で実装する必要があるので、この辺りが少々面倒ですね。

今回はSSGformというサービスを利用させて頂きます。
SSGformは「フォームURL数無制限」「フリープランでも機能制限なし」「クレジットカード登録不要」となっているためミニマムに始めたい場合には非常に便利なサービスだと思います。

SSGformへの登録

まずは、下記からSSGformに登録します。

https://ssgform.com/

SSGformの料金プラン

登録が完了したら以下のような感じで、フォームURLの作成を進めてください。

送信先URLとして自動生成されたURLはフォームのaction属性に設定するためコピーしておきましょう。
ちなみに下記にSSGformを導入する際のコード例がサンプルとして提供されています。

SSGformコード例

ただ、今回はReact-hook-formとyupのバリデーションルールを介してフォームを送信したいため上記のようなactionにURLを指定する形ではフォームの送信は行いません。
これについては後程解説しますが、一旦はサンプルコードのままにしておきます。

フォームコンポーネントの作成

まずはフォームのコンポーネントとして下記のような感じで適当に作成します。

import * as React from "react";
import { useEffect, useState, useCallback } from "react";
import { navigate } from "gatsby";

//フォームコンポーネント
export const Form = () => {
  //ポリシーチェックボックスの状態
  const [isChecked, setIsChecked] = useState(false);

  return (
    <form className="formrun" action="https://ssgform.com/自分のURL/" method="post">
      <div className="form-inner">
        <div className="contact-form">
          <div>
            <label htmlFor="name">Name<span>*</span></label>
            <span>
              <input id="name" name="name" type="text" placeholder="山田 太郎" />
            </span>
          </div>

          <div>
            <label htmlFor="kana">Kana<span>*</span></label>
            <span>
              <input id="kana" name="kana" type="text" placeholder="ヤマダ タロウ" />
            </span>
          </div>

          <div>
            <label htmlFor="company">Company-name</label>
            <span>
              <input id="company" name="company" type="text "placeholder="会社名" />
            </span>
          </div>

          <div >
            <label htmlFor="email">メールアドレス<span>*</span></label>
            <span>
              <input id="email" name="email" type="email" placeholder="example@email.com" />
            </span>
          </div>

          <div>
            <label htmlFor="type">Type<span>*</span></label>
            <span>
              <select id="type" name="type">
                <option>以下から選択してください</option>
                <option value="公開中の実績について(削除依頼等)">公開中の実績について(削除依頼等)</option>
                <option value="その他の制作実績について(個別にお見せできます)">その他の制作実績について(個別にお見せできます)</option>
                <option value="お仕事依頼のご相談">お仕事依頼のご相談</option>
                <option value="その他ご質問等">その他ご質問等</option>
              </select>
            </span>
          </div>

          <div>
            <label htmlFor="message">Message<span>*</span></label>
            <span >
              <textarea id="textarea" cols={82} rows={16} name="textarea" ></textarea>
            </span>
          </div>

          <div>
            <span>Polisy<span>*</span></span>
            <div>
              <div>
                <div>
                 ~
                 ポリシーの内容(省略)
                 ~
                </div>
              </div>
              <label>
                <input type="checkbox" name="acceptPolicy" checked={isChecked}
                  onChange={() => {
                    setIsChecked((prev) => {
                      return !prev;
                    });
                  }}
                />
                <span>プライバシーポリシーに同意する</span>
              </label>
            </div>
          </div>

          <div>
            <button type="submit" disabled={isChecked ? false : true}>SUBMIT</button>
          </div>
        </div>
      </div>
    </form>
  );
};

特に変わった内容はありませんが、「プライバシーポリシーに同意する」にチェックを入れないと、ボタンが「disabled」で押せない状態になるようにしています。

//ポリシーチェックボックスの状態
const [isChecked, setIsChecked] = useState(false); 

//~省略~

<label>
  <input type="checkbox" name="acceptPolicy" checked={isChecked}
   onChange={() => {
    setIsChecked((prev) => {
    return !prev;
    });
   }}
  />
 <span>プライバシーポリシーに同意する</span>
</label>

//~省略~

<div>
 <button type="submit" disabled={isChecked ? false : true}>SUBMIT</button>
</div>

フォームへのバリデーションの追加

次にReact-Hook-Formとyupをインストールしてフォームの設定やバリデーション設定を行っていきます。

まずはパッケージをインストールします。

npm install react-hook-form yup @hookform/resolvers

次に、パッケージをimportしてyupのバリデーションルールとuseFormの設定をします。

import * as React from "react";
import { useEffect, useState, useCallback } from "react";
import axios from "axios";
import { useForm } from "react-hook-form";
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
import { useGoogleReCaptcha } from "react-google-recaptcha-v3";
import { navigate } from "gatsby";

//フォームのバリデーションルールの定義
const schema = yup.object().shape({
  name: yup.string().required("※入力必須の項目です"),
  kana: yup.string().required("※入力必須の項目です"),
  company: yup.string(),
  email: yup
    .string()
    .email("※正しいメールアドレスの形式でご入力ください")
    .required("※入力必須の項目です"),
  type: yup
    .string()
    .oneOf([
      "公開中の実績について(削除依頼等)",
      "その他の制作実績について(個別にお見せできます)",
      "お仕事依頼のご相談",
      "その他ご質問等",
    ],
    "※いずれかを選択してください"
    )
    .required(),
  textarea: yup.string().required("※入力必須の項目です"),
});

併せて、useFormの設定も行います。
useFormとは、主にフォームの入力値の管理やバリデーション、フォームの送信などの機能を提供します。

//フォームのバリデーションルールの定義
const schema = yup.object().shape({
//~省略~
});

//フォームコンポーネント
export const Form = () => {
  //ポリシーチェックボックスの状態
  const [isChecked, setIsChecked] = useState(false);

  //useFormの設定
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    mode: "all",
    defaultValues: {
      name: "",
      kana: "",
      company: "",
      email: "",
      type: "以下から選択してください",
      textarea: "",
  },
    resolver: yupResolver(schema),
  });

  //~省略~

}

useFormの各メソッドについては公式を参照して頂きたいですが、一応簡単に解説します。

register

フォームの各入力要素を登録するための関数で、バリデーションルールを適用できます。
下記のような場合は、emailに入力された値を受け取り、入力必須のバリデーションを設定しています。

<input
 id="email"
 name="email"
 type="email"
 placeholder="example@email.com"
 {...register("email", { required: "入力必須の項目です" })}
/>

しかしながら、今回はバリデーションルールはyup側で設定するため{ required: “入力必須の項目です” }の部分は記述しなくてOKです。
React-Hook-Form単体で使用する場合は上記のように記述するようにしましょう。

他にも下記のようなオプションもあります。
他にもいろいろあるので、公式を参照してください。

maxLength 入力される値の最大文字数
minLength入力される値の最小文字数
pattern入力の正規表現パターン
valueAsNumber入力値を数値として保持
valueAsDate入力値を日付として保持

handleSubmit

フォームがsubmitされた際に呼ばれる関数。
第一引数にはsubmit処理が成功した場合の処理を登録、第二引数には処理が失敗した際の処理を記述します。
フォームのバリデーションが成功した場合にフォームデータ(data)を受け取り、失敗した場合にはエラー(errors)を受け取ります。
handleSubmitに渡す処理については後に記述します。

formState

このオブジェクトには、フォームの状態全体に関する情報が含まれます。
{errors}はフォームのバリデーションエラーを表すオブジェクトです。
このオブジェクトには、各入力要素に対するバリデーションエラーが含まれます。
{errors.email?.message}のような形式で記述することでバリデーションエラー時のエラーメッセージを表示させることができます。

<input
 id="email"
 name="email"
 type="email"
 placeholder="example@email.com"
 {...register("email", { required: "入力必須の項目です" })}
/>
<p>{errors.email?.message}</p>

Props

次にuseFormの引数に渡しているオブジェクトのプロパティについて説明します。

mode

ユーザーがフォームを送信する前のバリデーション戦略を設定できます。
以下の設定が可能です。

onSubmit フォームが送信されたときにのみバリデーションが実行される
onBlurフィールドがフォーカスを失ったときにバリデーションがトリガーされる。
ユーザーがフィールドに入力をしてフォーカスを失ったとき、そのフィールドのバリデーションが実行されます。
つまり、フィールドを編集し終わったときにバリデーションが行われます。
onChange フォームの各フィールドが変更されるたびにバリデーションが実行される
onTouchedフィールドに触れたことがある場合にバリデーションがトリガーされる。
フィールドにフォーカスを持っていき、何かしらの操作(入力やフォーカスを失う)をした場合、そのフィールドは「触れた」とみなされ、バリデーションが実行される。
つまり、フォーム内のすべてのフィールドを実際に触れたかどうかに基づいてバリデーションが行われます。
allフォームの各フィールドが変更されるたびにバリデーションが実行される。
つまり、フォームのどの状態でもバリデーションが行わる。

defaultValues

ここを設定しないとreact-hook-formもyupも動作しないため注意です。

resolver

こちらで先ほどのyupで設定していたバリデーションルールをyupResolverの引数に渡します。
そうすることでyupで設定したバリデーションルールでフォームにバリデーションを掛けることができます。

フォーム修正

useFormとyupの設定が終わったので改めてform部分を修正していきます。

import * as React from "react";
import { useEffect, useState, useCallback } from "react";
import axios from "axios";
import { useForm, Controller } from "react-hook-form";
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
import { navigate } from "gatsby";

//フォームのバリデーション
const schema = yup.object().shape({
  name: yup.string().required("※入力必須の項目です"),
  kana: yup.string().required("※入力必須の項目です"),
  company: yup.string(),
  email: yup
    .string()
    .email("※正しいメールアドレスの形式でご入力ください")
    .required("※入力必須の項目です"),
  type: yup
    .string()
    .oneOf(
      [
        "公開中の実績について(削除依頼等)",
        "その他の制作実績について(個別にお見せできます)",
        "お仕事依頼のご相談",
        "その他ご質問等",
      ],
      "※いずれかを選択してください"
    )
    .required(),

  textarea: yup.string().required("※入力必須の項目です"),
});
//フォームコンポーネント
export const Form = () => {
  //ポリシーチェックボックスの状態
  const [isChecked, setIsChecked] = useState(false);

  //useFormの設定
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    mode: "all",
    defaultValues: {
      name: "",
      kana: "",
      company: "",
      email: "",
      type: "以下から選択してください",
      textarea: "",
    },
    resolver: yupResolver(schema),
  });

  return (
    <form className="formrun" action="https://ssgform.com/自分のURL/" method="post">
      <div className="form-inner">
        <div className="contact-form">
          <div>
            <label htmlFor="name">Name<span>*</span></label>
            <span>
              <input id="name" name="name" type="text" placeholder="山田 太郎" {...register("name")}/>
              <p className="error">{errors.name?.message}</p>
            </span>
          </div>

          <div>
            <label htmlFor="kana">Kana<span>*</span></label>
            <span>
              <input id="kana" name="kana" type="text" placeholder="ヤマダ タロウ" {...register("kana")}/>
              <p className="error">{errors.kana?.message}</p>
            </span>
          </div>

          <div>
            <label htmlFor="company">Company-name</label>
            <span>
              <input id="company" name="company" type="text "placeholder="会社名" {...register("company")}/>
            </span>
          </div>

          <div >
            <label htmlFor="email">メールアドレス<span>*</span></label>
            <span>
              <input id="email" name="email" type="email" placeholder="example@email.com" {...register("email")}/>
              <p className="error">{errors.email?.message}</p>
            </span>
          </div>

          <div>
            <label htmlFor="type">Type<span>*</span></label>
            <span>
              <select id="type" name="type" {...register("type")}>
                <option>以下から選択してください</option>
                <option value="公開中の実績について(削除依頼等)">公開中の実績について(削除依頼等)</option>
                <option value="その他の制作実績について(個別にお見せできます)">その他の制作実績について(個別にお見せできます)</option>
                <option value="お仕事依頼のご相談">お仕事依頼のご相談</option>
                <option value="その他ご質問等">その他ご質問等</option>
              </select>
              <p className="error">{errors.type?.message}</p>
            </span>
          </div>

          <div>
            <label htmlFor="message">Message<span>*</span></label>
            <span>
              <textarea id="textarea" cols={82} rows={16} name="textarea" {...register("textarea")}></textarea>
              <p className="error">{errors.textarea?.message}</p>
            </span>
          </div>

          <div>
            <span>Polisy<span>*</span></span>
            <div>
              <div>
                <div>
                 ~
                 ポリシーの内容(省略)
                 ~
                </div>
              </div>
              <label>
                <input type="checkbox" name="acceptPolicy" checked={isChecked}
                  onChange={() => {
                    setIsChecked((prev) => {
                      return !prev;
                    });
                  }}
                />
                <span>プライバシーポリシーに同意する</span>
              </label>
            </div>
          </div>

          <div>
            <button type="submit" disabled={isChecked ? false : true}>SUBMIT</button>
          </div>
        </div>
      </div>
    </form>
  );
};

formのinput要素にregisterで入力値を保存し、<p className=”error”>{errors.name?.message}</p>でバリデーションチェック時にエラーメッセージが表示されるようにしました。

  return (
    <form className="formrun" onSubmit={handleSubmit(send)}>

次に、actionとmethodで設定していたデフォルトのフォーム送信方法をuseFormのメソッドである、handleSubmitに置き換えます。

このようにする理由は、React-hook-formとyupで設定したバリデーションを適用させるためです。
actionとmethodのままだとバリデーションチェックがかからずにそのままデータが送信されてしまいます。

send関数は以下のように設定します。

  const send = (data) => {
    axios
      .post("https://ssgform.com/自分のURL", data, {
        headers: {
          "content-type": "multipart/form-data", //axiosでフォーム送信する時に必要なheader情報
          "X-Requested-With": "XMLHttpRequest", //これが無いと400レスポンスが返却される
        },
      })
      .then((response) => {
        navigate("/thanks");
      })
      .catch((error) => {
        console.error("エラー:", error);
        alert("エラー: " + error);
      });
  };

headersで「content-type」と「X-Requested-With」を設定しないと上手く機能しません。
thenメソッドでフォームの送信成功時にはthanksページへリダイレクトするようにし、エラー発生時にはアラートを表示します。

これで一通りフォームの設定は完了です。
最後に全体コードを見てみましょう。

import * as React from "react";
import { useEffect, useState, useCallback } from "react";
import axios from "axios";
import { useForm, Controller } from "react-hook-form";
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
import { navigate } from "gatsby";

//フォームのバリデーション
const schema = yup.object().shape({
  name: yup.string().required("※入力必須の項目です"),
  kana: yup.string().required("※入力必須の項目です"),
  company: yup.string(),
  email: yup
    .string()
    .email("※正しいメールアドレスの形式でご入力ください")
    .required("※入力必須の項目です"),
  type: yup
    .string()
    .oneOf(
      [
        "公開中の実績について(削除依頼等)",
        "その他の制作実績について(個別にお見せできます)",
        "お仕事依頼のご相談",
        "その他ご質問等",
      ],
      "※いずれかを選択してください"
    )
    .required(),

  textarea: yup.string().required("※入力必須の項目です"),
});
//フォームコンポーネント
export const Form = () => {
  //ポリシーチェックボックスの状態
  const [isChecked, setIsChecked] = useState(false);

  //useFormの設定
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    mode: "all",
    defaultValues: {
      name: "",
      kana: "",
      company: "",
      email: "",
      type: "以下から選択してください",
      textarea: "",
    },
    resolver: yupResolver(schema),
  });

  //フォーム送信時の処理
  const send = (data) => {
    axios
      .post("https://ssgform.com/自分のURL", data, {
        headers: {
          "content-type": "multipart/form-data", //axiosでフォーム送信する時に必要なheader情報
          "X-Requested-With": "XMLHttpRequest", //これが無いと400レスポンスが返却される
        },
      })
      .then((response) => {
        navigate("/thanks");
      })
      .catch((error) => {
        console.error("エラー:", error);
        alert("エラー: " + error);
      });
  };

  return (
    <form className="formrun" action="https://ssgform.com/自分のURL/" method="post">
      <div className="form-inner">
        <div className="contact-form">
          <div>
            <label htmlFor="name">Name<span>*</span></label>
            <span>
              <input id="name" name="name" type="text" placeholder="山田 太郎" {...register("name")}/>
              <p className="error">{errors.name?.message}</p>
            </span>
          </div>

          <div>
            <label htmlFor="kana">Kana<span>*</span></label>
            <span>
              <input id="kana" name="kana" type="text" placeholder="ヤマダ タロウ" {...register("kana")}/>
              <p className="error">{errors.kana?.message}</p>
            </span>
          </div>

          <div>
            <label htmlFor="company">Company-name</label>
            <span>
              <input id="company" name="company" type="text "placeholder="会社名" {...register("company")}/>
            </span>
          </div>

          <div >
            <label htmlFor="email">メールアドレス<span>*</span></label>
            <span>
              <input id="email" name="email" type="email" placeholder="example@email.com" {...register("email")}/>
              <p className="error">{errors.email?.message}</p>
            </span>
          </div>

          <div>
            <label htmlFor="type">Type<span>*</span></label>
            <span>
              <select id="type" name="type" {...register("type")}>
                <option>以下から選択してください</option>
                <option value="公開中の実績について(削除依頼等)">公開中の実績について(削除依頼等)</option>
                <option value="その他の制作実績について(個別にお見せできます)">その他の制作実績について(個別にお見せできます)</option>
                <option value="お仕事依頼のご相談">お仕事依頼のご相談</option>
                <option value="その他ご質問等">その他ご質問等</option>
              </select>
              <p className="error">{errors.type?.message}</p>
            </span>
          </div>

          <div>
            <label htmlFor="message">Message<span>*</span></label>
            <span>
              <textarea id="textarea" cols={82} rows={16} name="textarea" {...register("textarea")}></textarea>
              <p className="error">{errors.textarea?.message}</p>
            </span>
          </div>

          <div>
            <span>Polisy<span>*</span></span>
            <div>
              <div>
                <div>
                 ~
                 ポリシーの内容(省略)
                 ~
                </div>
              </div>
              <label>
                <input type="checkbox" name="acceptPolicy" checked={isChecked}
                  onChange={() => {
                    setIsChecked((prev) => {
                      return !prev;
                    });
                  }}
                />
                <span>プライバシーポリシーに同意する</span>
              </label>
            </div>
          </div>

          <div>
            <button type="submit" disabled={isChecked ? false : true}>SUBMIT</button>
          </div>
        </div>
      </div>
    </form>
  );
};

おまけ:スパム対策でGoogle recaptchaを導入

おまけでGoogleのreCAPTCHAv3を導入する方法です。
※GoogleのreCAPTCHA側の設定は先に済ませておいて、サイトキーをコピーして置きましょう。

そして、環境変数として.envファイルに記載しておきます。

GATSBY_SITE_KEY_THREE=********************************************

次に、必要なパッケージをインストールします。

npm install react-google-recaptcha-v3 

次に、先ほど完成させたFormコンポーネントの親コンポーネントで「GoogleReCaptchaProvider」をインポートし、Formコンポーネントをwrapします。
この時に先ほどのサイトキーをpropsとして渡します。

import { GoogleReCaptchaProvider } from "react-google-recaptcha-v3";
import { Form } from "../components/form";

//recapchaのシークレットキーを取得
const SITE_KEY = process.env.GATSBY_SITE_KEY_THREE;

export default function Contact() {
  return (
    <>
     <GoogleReCaptchaProvider reCaptchaKey={SITE_KEY}>
      <Form />
     </GoogleReCaptchaProvider>
    </>
  );
}

次にFormコンポーネント側にも設定を加えます。
react-google-recaptcha-v3から「useGoogleReCaptcha」フックをインポートします。。

//~省略~
import { yupResolver } from "@hookform/resolvers/yup";
import { useGoogleReCaptcha } from "react-google-recaptcha-v3";
import { navigate } from "gatsby";

export const Form = () => {
  //ポリシーチェックボックスの状態
  const [isChecked, setIsChecked] = useState(false);

 //~省略~

  const send = (data) => {
    axios
      .post("https://ssgform.com/自分のURL", data, {
        headers: {
          "content-type": "multipart/form-data", //axiosでフォーム送信する時に必要なheader情報
          "X-Requested-With": "XMLHttpRequest", //これが無いと400レスポンスが返却される
        },
      })
      .then((response) => {
        console.log(response);
        navigate("/thanks");
      })
      .catch((error) => {
        console.error("エラー:", error);
        alert("エラー: " + error);
      });
  };

  //Googlerecapthcaの設定
  const { executeRecaptcha } = useGoogleReCaptcha();
  const handleReCaptchaVerify = useCallback(async () => {
    if (!executeRecaptcha) {
      console.log("recaptcha の実行はまだ利用できません。");
      return;
    }
  }, [executeRecaptcha]);

  useEffect(() => {
    handleReCaptchaVerify();
  }, [handleReCaptchaVerify]);

  return (

最後にボタンクリック時にhandleReCaptchaVerify()関数が呼ばれるようにします。

<button type="submit" disabled={isChecked ? false : true} onClick={handleReCaptchaVerify} >SUBMIT</button>
©UCHIWA Creative Studio.all rights reserved.