👻

shadcn/ui のSelectコンポーネントでmode: "onTouched"とtrigger()を利用するとエラーが出続ける

に公開

フロントエンド専門のWeb制作会社「株式会社トゥーアール」の西畑です。

shadcn/ui のSelectコンポーネントでmode: "onTouched"trigger()を利用するとエラーが出続けるという状況に遭遇したのでその解決方法を解説します。

https://ui.shadcn.com/docs/components/select

どういう状況か

オフィシャルにあるサンプルに以下のコードを追加して強制的にエラーを表記します。

React Hook FormのuseFormmodeonTouchedを指定して

  const form = useForm<z.infer<typeof FormSchema>>({
    resolver: zodResolver(FormSchema),
    mode: "onTouched",
  });

useEffect()trigger()を発火させます。

  useEffect(() => {
    form.trigger("email");
  }, [form]);

目的としては未入力時の初期値の状態でエラーを表示するために追加しています。

具体的には以下のようなコードです。

"use client"

import Link from "next/link"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
import { z } from "zod"

import { Button } from "@/components/ui/button"
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

const FormSchema = z.object({
  email: z
    .string({
      required_error: "Please select an email to display.",
    })
    .email(),
})

export function SelectForm() {
  const form = useForm<z.infer<typeof FormSchema>>({
    resolver: zodResolver(FormSchema),
+   mode: "onTouched",
  })

  function onSubmit(data: z.infer<typeof FormSchema>) {
    toast("You submitted the following values", {
      description: (
        <pre className="mt-2 w-[320px] rounded-md bg-neutral-950 p-4">
          <code className="text-white">{JSON.stringify(data, null, 2)}</code>
        </pre>
      ),
    })
  }

+ useEffect(() => {
+   form.trigger("email");
+ }, [form]);

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6">
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              <Select onValueChange={field.onChange} defaultValue={field.value}>
                <FormControl>
                  <SelectTrigger>
                    <SelectValue placeholder="Select a verified email to display" />
                  </SelectTrigger>
                </FormControl>
                <SelectContent>
                  <SelectItem value="m@example.com">m@example.com</SelectItem>
                  <SelectItem value="m@google.com">m@google.com</SelectItem>
                  <SelectItem value="m@support.com">m@support.com</SelectItem>
                </SelectContent>
              </Select>
              <FormDescription>
                You can manage email addresses in your{" "}
                <Link href="/examples/forms">email settings</Link>.
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">Submit</Button>
      </form>
    </Form>
  )
}

本来であれば初期値がエラーで値が選択されたらエラーが消えることを期待していたのですが、値を選択してもエラーが出続けます。

原因

shadcn/uiというよりReact Hook Formの問題だったのですが、mode: "onTouched"なので一度UIに対して触れて操作を加えないとエラー判定がonChangeに変わらないのですがエラー状態でUIに対して触れてもtouchedのステータスがfalseのままでエラーが出続けるようでした。

解決策1

<Select>onValueChange()時にtrigger()を実行することで変更後の状態が反映されます。

<Select
  onValueChange={(value) => {
    field.onChange(value);
    form.trigger("email");
  }}
  defaultValue={field.value}
>

ただ、useEffect()によるエラー表示時の挙動の修正をonValueChangeで対応するのは少し方向性が違う気がします。

解決策2

今回はuseEffect()によるエラー表示時にsetValue()shouldTouchオプションでtouchedのステータスをtrueに変更することで対応しました。

  useEffect(() => {
    const emailValue = form.getValues("email");
    form.setValue("email", emailValue, { shouldTouch: true });
    form.trigger("email");
  }, [form]);

値を入れ直しているのが少し気になりますがrender時のみ限定なので許容範囲ないでしょう。

個人的にはReact Hook Formではmode: "onTouched"を好んで利用しているのですがたまに不思議な挙動に遭遇するのが難点です。

株式会社トゥーアール

Discussion