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)
これのスムーズな解決法がわかっておらず、いったんスルーしています。
どなたかの参考になれば幸いです。