【Next.js】react-hook-formでFormProviderとuseFormContextを使い確認画面付きのフォームを作る

みなさんはReactでフォームを作成する場合は何を使用していますか?僕はreact-hook-formを使用しています。

やはり使用者の人数が多い分、ネットに多くの情報があるのもいいですね!

今回はそんなreact-hook-form使用した確認画面付きのフォームの作り方を紹介しようと思います!

目次

この記事を読むにあたって

まずこの記事を読むにあたって以下の知識がある方を対象としています。

  • react-hook-formの基本的な使い方が分かる
  • Next.jsを環境構築して少し触ったことがある(今回はそこまで重要ではありません)

問題無い!って方はこのまま読み進めてください。

まずはサンプルを用意したのでご確認ください!

※サンプルは送信ボタンを押してもどこにも情報は送信されませんのでご安心ください。

※以降はサンプルの内容をもとに進めて行きます。

1, 入力画面と確認画面のコンポーネントをFormProviderでラップする

// next.jsの機能
import { useRouter } from "next/router";

// components : pages
import Input from "components/pages/contact/Input"; //入力画面
import Confirm from "components/pages/contact/Confirm"; // 確認画面

//react-hook-form
import { useForm, FormProvider } from "react-hook-form";

const Contact = () => {
  // パラメータを取得
  const router = useRouter();
  const isConfirm = router.query.confirm;

  // useFormの設定&使用したい機能を呼び出す
  const methods = useForm({
    mode: "onChange",
    criteriaMode: "all"
  });

  return (
    <div className="wrapper">
      <FormProvider {...methods}>
        {!isConfirm ? (
          <>
            <Input />
          </>
        ) : (
          <>
            <Confirm />
          </>
        )}
      </FormProvider>
    </div>
  );
};

export default Contact;

FormProviderについて

FormProviderとはuseFormの機能をまとめて渡して、ラップされているコンポーネント内でuseFormContextからその機能を使うことができます。

ここでラップされているコンポーネントは以下の通りです

  • Inputコンポーネント(入力画面)
  • Confirmコンポーネント(確認画面)

このコンポーネントの中身については後に解説します。

コンポーネントを切り替えるための値を取得

// パラメータを取得
const router = useRouter();
const isConfirm = router.query.confirm;

入力画面と確認画面のコンポーネントを切り替える値を設定しています。

コンポーネントを切り替える方法はいくつかありますが、ここではNext.jsのuseRouterというhooksを使いクエリパラメータのconfirmの値の有無で切り替えます。

2, 入力画面を作成する

入力画面は1で登場したInputコンポーネントのことです。

// next.jsの機能
import { useRouter } from "next/router";

//react-hook-form
import { useFormContext, SubmitHandler } from "react-hook-form"; // SubmitHandlerは、submitイベントに関する関数の型宣言に使う
import { ErrorMessage } from "@hookform/error-message"; //エラーメッセージコンポーネント

import type { ContactType } from "types/contact";

const Input = () => {
  const router = useRouter();

  const {
    register, //inputなどに入力された値を参照するために使う
    handleSubmit,
    formState: { errors, isValid }
  } = useFormContext();

  const onSubmit: SubmitHandler<ContactType> = async (data) => {
    console.log(data);
    //ここでバリデーション用APIを叩くなど処理をする想定
    router.push(`/?confirm=1`);
  };

  return (
    <>
      <h1>入力画面</h1>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div className="form-unit">
          <p className="form-unit-title">お名前</p>
          <input
            type="text"
            className="input-text"
            placeholder="山田太郎"
            {...register("name", {
              required: "お名前は必須項目です。"
            })}
          />
          <ErrorMessage
            errors={errors}
            name="name"
            render={({ message }) =>
              message ? <p className="form-validateMessage">{message}</p> : null
            }
          />
        </div>

        <div className="form-unit">
          <p className="form-unit-title">お問い合わせ内容</p>
          <textarea
            className="input-textarea"
            placeholder="お問い合わせ内容を入力"
            {...register("content", {
              required: "お問い合わせ内容は必須項目です。"
            })}
          />
          <ErrorMessage
            errors={errors}
            name="content"
            render={({ message }) =>
              message ? <p className="form-validateMessage">{message}</p> : null
            }
          />
        </div>

        <div className="form-actionArea">
          {!isValid && (
            <>
              <p className="form-validateMessage">
                まだ全ての必須項目の入力が完了していません。
              </p>
            </>
          )}
          <div className="form-buttonWrapper">
            <button type="submit" className="form-submitButton">
              入力内容を確認する
            </button>
          </div>
        </div>
      </form>
    </>
  );
};

export default Input;

useFormContextを使い必要な機能を呼び出す

入力画面のコンポーネントではuseFormContextからFormProviderに渡された機能を呼び出すことができます。

つまりpropsで渡さなくてもuseFormの機能が使えるというわけです!素晴らしい!

あとはreact-hook-formを使える方であれば問題なくフォームを構築できるかと思います。(ほぼuseFormがuseFotmContextに置き換わっただけです)

submit時にクエリパラメータconfirmを付与

const onSubmit: SubmitHandler<ContactType> = async (data) => {
  //ここでバリデーション用APIを叩くなど処理をする想定
  router.push(`/?confirm=1`);
};

submit時にuseRouterpushを使い?confirm=1というクエリパラメータを付与された入力画面のURLにページ遷移します。

今回のサンプルではトップページ/が入力画面なので/?confirm=1となります。

すみません少しわかりにくいですよね.../contactとかの方がわかりやすかったですね。

※実際の実装ではここでバリデーション用のAPIなどを叩いて入力内容のチェックをやる想定です。そこで問題がなければuseRouterpushを使います。

3, 確認画面を作成する

確認画面は1で登場したConfirmコンポーネントのことです。

// next.jsの機能
import { useRouter } from "next/router";
import Link from "next/link";

//react-hook-form
import { useFormContext, SubmitHandler } from "react-hook-form"; // SubmitHandlerは、submitイベントに関する関数の型宣言に使う

import type { ContactType } from "types/contact";

const Confirm = () => {
  const router = useRouter();

  const {
    handleSubmit,
    getValues,
    formState: { isValid } //form内の入力の有無や送信の状態などを取得できる isValid以外にも色々ある
  } = useFormContext<ContactType>();

  const values = getValues(); // 入力データを取得

  //直アクセスの場合はリダイレクト
  //※必須項目の無いフォームは無いと思うのでisValidで判定
  if (!isValid) {
    router.push(`/`);
  }

  const onSubmit: SubmitHandler<ContactType> = async (data) => {
    //ここでメール送信などのAPIを叩くなど処理をする想定
    router.push("/complete");
  };

  return (
    <>
      <h1>入力内容確認画面</h1>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div className="form-unit">
          <p className="form-unit-title">お名前</p>
          <p>{values.name}</p>
        </div>

        <div className="form-unit">
          <p className="form-unit-title">お問い合わせ内容</p>
          <p>{values.content}</p>
        </div>

        <div className="form-actionArea">
          <div className="form-buttonWrapper">
            <button type="submit" className="form-submitButton">
              送信する
            </button>
            <Link href="/">
              <a className="form-backButton">入力内容を修正する</a>
            </Link>
          </div>
        </div>
        <p>※ただのサンプルなのでどこにも送信されません。</p>
      </form>
    </>
  );
};

export default Confirm;

getValuesで入力データを取得する

useFormContextからgetValuesを呼び出し、以下のように使用します。

const values = getValues(); // 入力データを取得

こうすることで、values.registerに設定したnemeで入力画面で入力したそれぞれのvalueを取得できます。

あとは以下のように記述することで入力内容を任意の場所に表示できます。

//例
<p>{values.name}</p> //入力画面で入力したお名前を表示

submit時に完了画面へ遷移

const onSubmit: SubmitHandler<ContactType> = async (data) => {
  //ここでメール送信などのAPIを叩くなど処理をする想定
  router.push("/complete");
};

入力画面と同様にsubmit時にuseRouterpushを使います。

今回は完了画面のURLを/completeとしているので、そこにページ遷移させてあげます。

4, 完了画面について

const Complete = () => {
  //直アクセスの場合は何かしらのフラグ管理をして、リダイレクト処理をする想定
  return (
    <div className="wrapper">
      <h1>お問い合わせありがとうございました。</h1>
      <p>
        <a href="/">トップに戻る</a>
      </p>
    </div>
  );
};

export default Complete;

完了画面は適当に作って問題ないかと思いますが、実際の実装で必要があれば送信完了フラグなどを立ててあげるといいかもしれません。

これで終わりです

これで解説は終わりとなります。
確認画面の表示までを簡単にまとめると

  1. InputコンポーネントとConfirmコンポーネントをFormProviderでラップする
  2. Inputコンポーネント内でuseFormContextを使い入力画面を実装する
  3. Confirmコンポーネント内でgetValuesを使い入力内容を表示する

このような感じです。

最後に

今回紹介した内容は超簡易版なので実際の実装ではAPIを叩いて確認ページに遷移する判定をしたり、確認画面や完了画面に直アクセスされときのリダイレクト処理だったり細かな調整が必要かと思います。

その他にも今回のサンプルはざっくりとInputコンポーネントとConfirmコンポーネントの2つしかコンポーネントはありませんし、cssもグローバルに適当に定義しているだけなので、このあたりは皆さんの得意な方法で好きに実装すると良いかと思います。