🔖

[ReactHookForm/Zod] SuperRefine した ArrayField のバリデーションエラーが表示されなくなっていた

2024/07/19に公開1

問題

ReactHookForm と Zod を使った環境にて、SuperRefine した Array のバリデーションが表示されなくなっていた。

以下、簡略化したコード。

import { z } from "zod";
import { useFieldArray, useForm } from "react-hook-form";

const someSchema = z.object({
    items: z.array(z.object({
        // 省略
    }))
})

const schema = someSchema.superRefine((value, context) => {
    if(...) {
        context.addIssue({
            code: z.ZodIssueCode.custom,
            path: ["items"],
            message: "..."
        })
    }
})

const { formState: { errors } } = useForm({
    defaultValues: { items: [] },
    resolver: zodResolver(schema),
});

const { fields, append, remove } = useFieldArray({
    control,
    name: "items",
});

console.log(errors.items?.message); // ← これが取得できなくなっている!

利用しているバージョンは以下の通り。

  • ReactHookForm: 7.45.4
  • Zod: 3.23.8

解決

解決自体はシンプル。

console.log(errors.items?.root?.message); // ← root が挟まるようになった。

コードベースの他の箇所では root なしでもエラーメッセージが取れるので、SuperRefine した Array の場合など、特殊な条件化で起こる現象かもしれない。

それぞれのライブラリの Release note などを見てみたが、原因となる箇所を見つけられず…。モヤモヤは残るが、いったんバリデーションエラーメッセージは取れるようになった。

Discussion

nap5nap5

特殊な条件化で起こる現象かもしれない。

たしかにそんな気もしなくもないですね。バージョン違いかもですね。

やってみると、以下の2つのスキーマ定義の場合でも@hookform/error-messageから提供されているErrorMessageコンポーネントからはサブスクライブできていそうな気がしました。

{
    "@hookform/error-message": "^2.0.1",
    "@hookform/resolvers": "^3.9.0",
    "react-hook-form": "^7.52.1",
    "zod": "^3.23.8"
}

その1

import { z } from "zod";
import { ExactNumber as N } from "exactnumber";
import { isEmptyString } from "@sindresorhus/is";

export const InvoiceDetailFormSchema = z.object({
  /**
   * 商品名
   */
  itemName: z.string(),
  /**
   * 単価
   */
  unitPrice: z.string(),
  /**
   * 数量
   */
  quantity: z.string(),
  /**
   * 金額
   */
  amount: z.string(),
  /**
   * 消費税
   */
  taxAmount: z.string(),
});

export type InvoiceDetailForm = z.infer<typeof InvoiceDetailFormSchema>;

export const InvoiceSummaryFormSchema = z
  .object({
    items: InvoiceDetailFormSchema.array(),
    /**
     * 小計
     */
    subTotal: z.string(),
    /**
     * 消費税小計
     */
    taxTotal: z.string(),
    /**
     * 合計
     */
    total: z.string().superRefine((val, ctx) => {
      if (isEmptyString(val)) return;

      if (!N(val).lt("1000")) {
        ctx.addIssue({
          code: "custom",
          fatal: true,
          message: "合計は1,000円以下にしてください",
        });

        return z.NEVER;
      }
    }),
  })
  .superRefine(({ items }, ctx) => {
    if (items.length > 3) {
      ctx.addIssue({
        code: "custom",
        fatal: true,
        path: ["items"],
        message: "商品は3個までになります",
      });

      return z.NEVER;
    }
  });

export type InvoiceSummaryForm = z.infer<typeof InvoiceSummaryFormSchema>;

その2

import { z } from "zod";
import { ExactNumber as N } from "exactnumber";
import { isEmptyString } from "@sindresorhus/is";

export const InvoiceDetailFormSchema = z.object({
  /**
   * 商品名
   */
  itemName: z.string(),
  /**
   * 単価
   */
  unitPrice: z.string(),
  /**
   * 数量
   */
  quantity: z.string(),
  /**
   * 金額
   */
  amount: z.string(),
  /**
   * 消費税
   */
  taxAmount: z.string(),
});

export type InvoiceDetailForm = z.infer<typeof InvoiceDetailFormSchema>;

export const InvoiceSummaryFormSchema = z.object({
  items: InvoiceDetailFormSchema.array().superRefine((val, ctx) => {
    if (val.length > 3) {
      ctx.addIssue({
        code: "custom",
        fatal: true,
        message: "商品は3個までになります",
      });

      return z.NEVER;
    }
  }),
  /**
   * 小計
   */
  subTotal: z.string(),
  /**
   * 消費税小計
   */
  taxTotal: z.string(),
  /**
   * 合計
   */
  total: z.string().superRefine((val, ctx) => {
    if (isEmptyString(val)) return;

    if (!N(val).lt("1000")) {
      ctx.addIssue({
        code: "custom",
        fatal: true,
        message: "合計は1,000円以下にしてください",
      });

      return z.NEVER;
    }
  }),
});

export type InvoiceSummaryForm = z.infer<typeof InvoiceSummaryFormSchema>;