kk-web

Conformを使う?React Hook Formでいけるかもよ

2024-05-22

以前 React Hook Form を無理やり Server Action に落とし込む という記事を書いたのですが。

最近 Conform というパッケージが Server Action へ対応しており、こっちを使おうみたいな記事がちらほら見受けられます。

とはいえ React Hook Form の書きっぷりと比べると結構イマイチな書きっぷりが多く、移行するのやだなーと思っていまして。

改めて React Hook Form で Server Action に繋げる方法はないかと探したところ、公式 に思いっきり記載がありました。


現在ベータ版とのことですが、結構開発も進んでいるっぽく、それならばと思い本サイトのコンタクトフォームで組んでみたところ、あっさり動いてくれました。

React Hook Form は公式ドキュメントがかなり丁寧に書かれているので、今更ここに書くようなこともないのですが…。

せっかくなので、今回は ReCAPTCHA と Zod と Toast を組み合わせたサンプルをば。

"use client";
import { ErrorMessage } from "@hookform/error-message";
import { zodResolver } from "@hookform/resolvers/zod";
import { Box, Button, Flex, Heading, Input, Text, VStack } from "@kuma-ui/core";
import { setCookie } from "cookies-next";
import { useRef } from "react";
import ReCAPTCHA from "react-google-recaptcha";
import { Controller, Form, useForm } from "react-hook-form";
import TextareaAutosize from "react-textarea-autosize";
import { Id, toast } from "react-toastify";
import { z } from "zod";
import styles from "./style.module.scss";
import { env } from "@/env";

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

type FieldTypes = z.infer<typeof schema>;

export default function Contact(): JSX.Element {
  const {
    control,
    formState: { errors, isSubmitting },
    register,
  } = useForm<FieldTypes>({
    defaultValues: {
      email: "",
      message: "",
      name: "",
      subject: "",
    },
    progressive: true,
    resolver: zodResolver(schema),
  });
  const ref = useRef<ReCAPTCHA>(null);
  const toastId = useRef<Id>(null);

  return (
    <>
      <Box height="0px" overflow="hidden" style={{ opacity: 0 }} width="0px">
        <Heading as="h2">CONTACT</Heading>
      </Box>
      <Flex
        alignItems="center"
        height="100%"
        justify="center"
        pb={32}
        pt={12}
        px={12}
      >
        <Form
          action="/email"
          className={styles.form}
          control={control}
          onError={(): void => {
            if (!toastId.current) {
              return;
            }

            toast.update(toastId.current, {
              autoClose: 5000,
              isLoading: false,
              render: "送信に失敗しました",
              type: "error",
            });
          }}
          onSubmit={async (): Promise<void> => {
            if (!ref.current) {
              return;
            }

            const token = await ref.current.executeAsync();

            if (typeof token !== "string") {
              return;
            }

            setCookie("token", token);

            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
            // @ts-ignore
            toastId.current = toast("送信しています…", {
              autoClose: false,
              isLoading: true,
            });
          }}
          onSuccess={(): void => {
            if (!toastId.current) {
              return;
            }

            toast.update(toastId.current, {
              autoClose: 5000,
              isLoading: false,
              render: "メッセージを送信しました",
              type: "success",
            });
          }}
        >
          <ReCAPTCHA
            ref={ref}
            sitekey={env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY}
            size="invisible"
            theme="dark"
          />
          <VStack gap={32}>
            <VStack gap={24}>
              <VStack gap={4}>
                <Box as="label" fontSize="1.0rem" htmlFor="name">
                  Name
                  <Box as="abbr" pl={4}>
                    *
                  </Box>
                </Box>
                <Controller
                  control={control}
                  name="name"
                  render={({ field }): JSX.Element => (
                    <Input
                      {...field}
                      autoFocus={true}
                      bg="colors.lightWhite"
                      color="colors.black"
                      fontFamily="arial"
                      id="name"
                      px={6}
                      py={4}
                    />
                  )}
                />
                <ErrorMessage
                  errors={errors}
                  name="name"
                  render={({ message }): JSX.Element => (
                    <Text
                      color="colors.brandRed"
                      fontFamily="arial"
                      fontSize="1.2rem"
                    >
                      {message}
                    </Text>
                  )}
                />
              </VStack>
              <VStack gap={4}>
                <Box as="label" fontSize="1.0rem" htmlFor="email">
                  Email
                  <Box as="abbr" pl={4}>
                    *
                  </Box>
                </Box>
                <Controller
                  control={control}
                  name="email"
                  render={({ field }): JSX.Element => (
                    <Input
                      {...field}
                      bg="colors.lightWhite"
                      color="colors.black"
                      fontFamily="arial"
                      id="email"
                      px={6}
                      py={4}
                      type="email"
                    />
                  )}
                />
                <ErrorMessage
                  errors={errors}
                  name="email"
                  render={({ message }): JSX.Element => (
                    <Text
                      color="colors.brandRed"
                      fontFamily="arial"
                      fontSize="1.2rem"
                    >
                      {message}
                    </Text>
                  )}
                />
              </VStack>
              <VStack gap={4}>
                <Box as="label" fontSize="1.0rem" htmlFor="subject">
                  Subject
                  <Box as="abbr" pl={4}>
                    *
                  </Box>
                </Box>
                <Controller
                  control={control}
                  name="subject"
                  render={({ field }): JSX.Element => (
                    <Input
                      {...field}
                      bg="colors.lightWhite"
                      color="colors.black"
                      fontFamily="arial"
                      id="subject"
                      px={6}
                      py={4}
                    />
                  )}
                />
                <ErrorMessage
                  errors={errors}
                  name="subject"
                  render={({ message }): JSX.Element => (
                    <Text
                      color="colors.brandRed"
                      fontFamily="arial"
                      fontSize="1.2rem"
                    >
                      {message}
                    </Text>
                  )}
                />
              </VStack>
              <VStack gap={4}>
                <Box as="label" fontSize="1.0rem" htmlFor="message">
                  Message
                  <Box as="abbr" pl={4}>
                    *
                  </Box>
                </Box>
                <TextareaAutosize
                  {...register("message")}
                  className={styles.textareaAutosize}
                  id="message"
                  minRows={6}
                />
                <ErrorMessage
                  errors={errors}
                  name="message"
                  render={({ message }): JSX.Element => (
                    <Text
                      color="colors.brandRed"
                      fontFamily="arial"
                      fontSize="1.2rem"
                    >
                      {message}
                    </Text>
                  )}
                />
              </VStack>
            </VStack>
            <Flex justify="center">
              <Button
                bg="colors.brandBlue"
                color="colors.lightWhite"
                fontFamily="arial"
                opacity={isSubmitting ? 0.5 : 1}
                px={24}
                py={8}
                transition="250ms"
              >
                送信する
              </Button>
            </Flex>
          </VStack>
        </Form>
      </Flex>
    </>
  );
}

サーバー側は formData から values を取得するよう修正するだけなので省略します。

気になる方はリポジトリーを確認していただけると。


そんな感じです、正式リリースが今から楽しみです。

© 2018 kk-web