kk-web

Next.js × React Hook Form × Zod

2024-02-13

Next.js の Server Actions と React Hook Form と Client Validation をかませる方法ってないのかなーと思っていたのですが。

ぼちぼち 良い感じの実装感 を見つけたので、共有をば。


/src/app/contact/page.tsx

import Contact, { ContactProps } from "@/components/Contact";
import transporter from "@/lib/transporter";

export default function Page(): JSX.Element {
  const sendEmail: ContactProps["sendEmail"] = async ({
    email,
    name,
    text,
  }) => {
    "use server";

    await transporter.sendMail({
      replyTo: `"${name}" <${email}>`,
      text,
      to: process.env.NODEMAILER_AUTH_USER,
    });
  };

  return <Contact sendEmail={sendEmail} />;
}

/src/components/Contact/index.tsx

"use client";
import { ErrorMessage } from "@hookform/error-message";
import { zodResolver } from "@hookform/resolvers/zod";
import i18next from "i18next";
import { ReactNode } from "react";
import { useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
import TextareaAutosize from "react-textarea-autosize";
import z from "zod";
import { zodI18nMap } from "zod-i18n-map";
import translation from "zod-i18n-map/locales/ja/zod.json";
import styles from "./style.module.scss";

void i18next.init({
  lng: "ja",
  resources: {
    ja: { zod: translation },
  },
});
z.setErrorMap(zodI18nMap);

const schema = z.object({
  email: z.string().min(1).email(),
  name: z.string().min(1),
  text: z.string().min(1),
});

type FieldTypes = z.infer<typeof schema>;

export type ContactProps = {
  sendEmail: (data: FieldTypes) => Promise<void>;
};

export default function Contact({ sendEmail }: ContactProps): JSX.Element {
  const {
    formState: { errors, isSubmitting },
    handleSubmit,
    register,
  } = useForm<FieldTypes>({
    defaultValues: {
      email: "",
      name: "",
      text: "",
    },
    resolver: zodResolver(schema),
    shouldUnregister: false,
  });
  const action = handleSubmit(async (data) => {
    await toast.promise(sendEmail(data), {
      error: "メッセージの送信に失敗しました",
      loading: "送信しています…",
      success: "メッセージを送信しました",
    });
  });

  return (
    <div className={styles.wrapper}>
      <div className={styles.inner}>
        <div className={styles.h2Wrapper}>
          <h2 className={styles.h2}>お問い合わせ</h2>
        </div>
        <div className={styles.formWrapper}>
          <form
            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
            // @ts-ignore
            // eslint-disable-next-line @typescript-eslint/no-misused-promises
            action={action}
          >
            <div className={styles.formInner}>
              <div className={styles.field}>
                <label className={styles.label} htmlFor="name">
                  お名前 / 企業名<abbr>*</abbr>
                </label>
                <input
                  {...register("name")}
                  className={styles.input}
                  id="name"
                />
                <ErrorMessage
                  errors={errors}
                  name="name"
                  render={({ message }): ReactNode => (
                    <p className={styles.errorMessage}>{message}</p>
                  )}
                />
              </div>
              ...
              <div className={styles.formFooter}>
                <button className={styles.button} type="submit">
                  送信
                </button>
              </div>
            </div>
          </form>
        </div>
      </div>
    </div>
  );
}

挙動感はまったく問題ないんですが、少しだけ問題がありまして。

export default function Contact({ sendEmail }: ContactProps): JSX.Element {

ここで以下の Warning を吐いていまして。

Props must be serializable for components in the "use client" entry file, "sendEmail" is invalid. ts(71007)

これのスムーズな解決法がわかっておらず、いったんスルーしています。

どなたかの参考になれば幸いです。

© 2018 kk-web