入力項目の多いフォームをReactにリプレイスする際に工夫したこと

  • URLをコピーしました!

こんにちは、フロントエンドエンジニアのやなぎ( @apple_yagi )です。

先日、PR TIMES上で企業アカウントを作成する際に情報を入力する企業登録申請フォームをReact + Viteにリプレイスしました。

プレスリリース・ニュースリリース...

本エントリーではリプレイスするにあたり、使用したライブラリや、工夫した点などをご紹介します。

目次

企業登録申請フォームをリプレイスする難しさ

企業登録申請フォームは入力項目が30項目あり、データ量が多いフォームとなっています。このようなフォームをシングルページアプリケーションで実装する時に難しくなるのが、フロントエンドとバックエンドのバリデーションをどのように同期させるかです。すでに登録済みのメールアドレスかどうかを確認するバリデーションなどはフロントエンドだけでは完結することはできないため、フロントエンドのバリデーションがゆるくなるのが一般的だと思います。今回のリプレイスでもフロントエンドのバリデーションをバックエンドよりもゆるくする対応を行いましたが、そこでさらに問題となるのがバックエンドから返ってきたバリデーションエラーをフロントエンドの入力項目とマッピングさせる処理が複雑になりがちという問題です。今回のリプレイスではこのような問題を解決するためにこれから紹介するような工夫を行い、実装しました。

フロントエンドで使用しているライブラリ

PR TIMESではフォームを実装する際に以下のライブラリを使用しています。

バリデーションルールは基本的にZodで記述するようにし、@hookform/resolvers/zod を使用して、React Hook FormにZodで記述したバリデーションルールを渡しています。また、APIとの通信を行う際は OpenAPI から openapi-generator を使用して自動生成したクライアントを Tanstack Query でラップし、通信を行なっています。

バックエンドからのエラーレスポンスを入力項目にマッピングする

まず、APIからのエラーレスポンスは以下のようなスキーマで返ってくるように実装します。

{
  "message": "Bad Request",
  "status": 400,
  "errors": [
    {
      "code": "required",
      "message": {
        "ja": "企業名を入力してください",
        "en": "Corporate name is required"
      },
      // React Hook Formのfield名と一致する値を返す
      "field": "companyInfo.corporateName"
    }
  ]
}

このエラーレスポンスを受け取るフロントエンドのコードは以下のように実装します。

import {useMutation} from '@tanstack/react-query';

const useValidationMutation = (form: UseFormReturn<FormSchema>) => {
	return useMutation({
    mutationKey: ['validate'],
    mutationFn: async (formData: CompanyRegistrationFormData) => postValidate(formData),
    async onError(error) {
      if (!(error instanceof Response) || error.status !== 400) {
        showErrorToast(
          '予期せぬエラーが発生しました。もう一度お試しください。',
        );
        throw error;
      }

      // OpenAPIから自動生成したエラーレスポンスの型にキャストする
      // レスポンスの形は上記で記載したJSONスキーマと一致する
      const errorResponse = (await error.json()) as ValidateErrorResponse;

      for (const error of errorResponse.errors) {
        form.setError(error.field, {
          message: error.message.ja,
        });
      }

      // 最初に返ってきたエラーのfieldにfocusする
      form.setFocus(errorResponse.errors[0].field);

      newrelic.noticeError(new Error('企業登録申請ページ:バックエンドバリデーションエラー'), {
        backendErrorFields: JSON.stringify(errorResponse.errors),
      });
    },
  });
}

この実装ではAPIのエラーレスポンスにある field の値を React Hook Form の field の値と一致させることにより、大量の項目があるフォームでもエラーのマッピングを簡潔に記述することができます。また、OpenAPIを記述する際に field の値を全て enum で列挙することにより、React Hook Form 上で存在しない field が OpenAPI に記述された際に型エラーを発生させることができます。

OpenAPIにfieldの値を全て列挙
存在しないfield(test.doesNotExist)を追加
form.setErrorで型エラーが発生

ただ、この実装のデメリットとしてフロントエンド(React Hook Formのfield名の命名規則)とバックエンドが密に結合している点が挙げられます。また、ユーザーに表示するためのエラーメッセージをバックエンドからも返しているため、フロントエンドとバックエンドでエラーメッセージが二重管理となってしまいます。今回はそのようなデメリットよりもマッピングのコードを簡潔にする方がメリットが高いと判断したため、上記の実装を行いました。

バリデーションエラーをNew Relicに送信する

入力項目の多いフォームではエンジニアの認知負荷が高く、バリデーションルールを記述する際にケアレスミスが発生しやすくなると考えています。また、フロントエンドとバックエンドのバリデーションルールの差異から特定の条件で絶対に通過できないバリデーションが完成する可能性が稀にあります。弊社ではUnitテストやQAによってそれらの問題が発生しないように品質を担保していますが、万が一それらをすり抜けて本番にデプロイされてしまった際に検知できるように、New Relicにバリデーションエラーの内容を送信するようにしています。

弊社では New Relic APM 経由で New Relic のBrowserモニタリングを有効にしています。Browserモニタリングを有効にすると、グローバル空間に newrelic というオブジェクトが生えます。このオブジェクトにある noticeError という関数を実行することにより、フロントエンドから意図的に New Relic へエラーを送信することができます。

TypeScriptを使っている場合はこちらのライブラリをインストールすることで型定義を取得できます。https://www.npmjs.com/package/@types/new-relic-browser

前述したフロントエンド実装でもAPIからのエラーレスポンスを New Relic に送信しています。これにより、バックエンドで発生したバリデーションエラーを New Relic で監視できるようになります。

      newrelic.noticeError(new Error('企業登録申請ページ:バックエンドバリデーションエラー'), {
        backendErrorFields: JSON.stringify(errorResponse.errors),
      });

フロントエンドで発生したバリデーションエラーは以下のように New Relic に送信しています。

  const {handleSubmit} = useForm();
  
  const onSubmit = handleSubmit(
    async (data) => {
      await mutateAsync(data);
    },
    (error) => {
      const fieldErrorsMessage = stringifyFieldErrors(error);

      // New RelicにReact Hook Formで発生したバリデーションエラーを送信
      newrelic.noticeError(new Error('企業登録申請ページ:バリデーションエラー'), {
        fieldErrors: fieldErrorsMessage,
      });
    },
  );

React Hook Form の handleSubmit にはバリデーションエラー時のcallback関数を登録できるため、その関数に New Relic にエラーを送信する処理を記述しています。ただこの実装では1つ注意する点があり、callback関数で取得できる error の型が FieldErrors になっているため、そのまま JSON.stringify をすることができません。そのため、FieldErrors からエラーメッセージを抜き取りJSON形式の文字列に変換する処理を自前で書いています。

フロントエンドとバックエンドで発生したバリデーションエラーを監視することで、フロントエンドのバリデーションが意図せず緩くなっている箇所や、同じバリデーションエラーが何回も発生しているユーザーを発見することができ、実際に本番リリース後のバグ検知に活用することができました。

まとめ

今回は入力項目の多いフォームをリプレイスした際に工夫した点をご紹介しました。最近ではバックエンドをNode.jsにすることでフロントエンドとバックエンドのバリデーションルール・エラーメッセージを共有する構成も増えつつあると思っていますが、まだまだそうなっているサービスは少ないと思います。今回ご紹介したことが1つでも参考になれば嬉しいです。

We are hiring!

フロントエンドエンジニアはもちろん、各種ポジションで採用を行っています!

あわせて読みたい
株式会社PR TIMES
02.開発部 の求人一覧 - 株式会社PR TIMES 株式会社PR TIMESが公開している、02.開発部 の求人一覧です
  • URLをコピーしました!

この記事を書いた人

株式会社PR TIMES 開発本部 フロントエンドエンジニア

目次