shadcn-ui / ui

Beautifully designed components that you can copy and paste into your apps. Accessible. Customizable. Open Source.
https://ui.shadcn.com
MIT License
64.76k stars 3.69k forks source link

React Hook Form validation not working #555

Open martin-dimi opened 1 year ago

martin-dimi commented 1 year ago

Hi, I pretty much have followed the guide https://ui.shadcn.com/docs/forms/react-hook-form step by step, but unfortunately, I get no validation error messages. I've checked and the resolver method on the useForm is not getting called.

The code is pretty much 1-1 to the guide but still:

const FormSchema = z.object({
  username: z.string().min(2, {
    message: "Username must be at least 2 characters.",
  }),
})

const SomeComponent = () => {
  const form = useForm<z.infer<typeof FormSchema>>({
    resolver: async (data, context, options) => {
      // THIS IS NOT GETTING CALLED EVER => error is NOT getting populated
      console.log('formData', data)
      console.log('validation result', await zodResolver(FormSchema)(data, context, options))
      return zodResolver(FormSchema)(data, context, options)
    },
    defaultValues: {
      username: "",
    },
  })

  function onSubmit(data: z.infer<typeof FormSchema>) {
    console.log(data)
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
        <FormField
          control={form.control}
          name="username"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Username</FormLabel>
              <FormControl>
                <Input placeholder="shadcn" {...field} />
              </FormControl>
              <FormDescription>
                This is your public display name.
              </FormDescription>
              <FormMessage />
            </FormItem>
          )} />
        <Button text="Submit" type="submit" />
      </form>
    </Form>
  );
}

All of the Form components + Input are again direct copies from shadcn

martin-dimi commented 1 year ago

I was simply missing: mode: 'onChange' on the form. In the tutorial, the preview seems to be using mode: 'onTouched' but the example code does not reflect that. Worth updating? image

spattanaik01 commented 1 year ago

The documentation for Form component could be more elaborative. It is difficult for a beginner to follow through.

skonky commented 10 months ago

I was simply missing: mode: 'onChange' on the form. In the tutorial, the preview seems to be using mode: 'onTouched' but the example code does not reflect that. Worth updating?

Maybe a good idea to explicitly define the mode in the example so it's clear the prop exists

chriscodex commented 8 months ago

I was simply missing: mode: 'onChange' on the form. In the tutorial, the preview seems to be using mode: 'onTouched' but the example code does not reflect that. Worth updating? image

I have the same problem, where in the code did you add the mode?

Cliftonz commented 6 months ago

Add it to the useForm hook.

const form = useForm({
...,
mode: "onChange"
})
ProductOfAmerica commented 2 months ago

To anyone else wondering, none of the solutions above worked for me. The workaround was to remove zod validation.

previous:

  const form = z.object({
    otp: z.string().min(INPUT_NUM, `OTP must be ${INPUT_NUM} characters long`),
  });

  type ClientForm = z.infer<typeof form>;

  const multiForm = useForm<ClientForm>({
    defaultValues: {
      otp: "",
    },
    mode: "onChange",
  });

    const onSubmit = async (data: ClientForm) => {}

after:

  interface FormData {
    otp: string;
  }

  const multiForm = useForm({
    defaultValues: {
      otp: "",
    },
  });

  const onSubmit = async (data: FormData) => {}

Problem for me was that zod would kick in if I submitted once, got an error from the server, then tried to clear the errors and try again. Once I started typing the second time around, my zod error would appear asking for 6 character input. I don't want the error to display though if the user is still typing it in the second time around. The only workaround I could find was to do manual validation, and not use zod. This solution actually works as expected. Very frustrating behavior by this library.

Full final working context:

"use client";

import { Button } from "@/components/ui/button";
import { OtpStyledInput } from "@/components/ui/input-otp";
import { useForm } from "react-hook-form";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormMessage,
} from "@/components/ui/form";
import { useSearchParams } from "next/navigation";
import { verifyOTP } from "@/components/actions";
import { useEffect, useState } from "react";
import { Spinner } from "@/components/ui/spinner";

const INPUT_NUM = 6;

interface FormData {
  otp: string;
}

export default function VerifyOTPPageComponent() {
  const searchParams = useSearchParams();
  const [inputKey, setInputKey] = useState(0);
  const [show, setShow] = useState(false);
  const multiForm = useForm({
    defaultValues: {
      otp: "",
    },
  });

  const otpValue = multiForm.watch("otp");
  useEffect(() => {
    if (otpValue.length === INPUT_NUM) {
      multiForm.handleSubmit(onSubmit)();
    }
  }, [otpValue]);

  const onSubmit = async (data: FormData) => {
    setShow(true);

    if (data.otp.length !== INPUT_NUM) {
      multiForm.setError("otp", {
        message: `OTP must be ${INPUT_NUM} characters long`,
      });
      multiForm.setFocus("otp");
      setInputKey((prevKey) => prevKey + 1);
      setShow(false);
      return;
    }

    try {
      await verifyOTP(
        decodeURIComponent(searchParams.get("phone") || ""),
        data.otp,
      );
      multiForm.reset();
    } catch (error: unknown) {
      console.error("Error during OTP verification:", error);
      multiForm.reset();
      multiForm.clearErrors();
      multiForm.setError("otp", { message: (error as Error).message });
      multiForm.setFocus("otp");
      setInputKey((prevKey) => prevKey + 1);
    }

    setShow(false);
  };

  return (
    <div className="flex flex-col items-center justify-center h-screen">
      <div className="bg-white dark:bg-gray-950 shadow-lg rounded-lg p-8 w-full max-w-md">
        <h2 className="text-2xl font-bold mb-4">Verify OTP</h2>
        <p className="text-gray-500 dark:text-gray-400 mb-6">
          Enter the 6-digit one-time password sent to your phone.
        </p>
        <Form {...multiForm}>
          <form onSubmit={multiForm.handleSubmit(onSubmit)}>
            <FormField
              control={multiForm.control}
              name="otp"
              render={({ field: { ...newField }, fieldState }) => (
                <FormControl>
                  <>
                    <FormItem>
                      <OtpStyledInput
                        key={inputKey}
                        {...newField}
                        numInputs={INPUT_NUM}
                        shouldAutoFocus={true}
                      />
                    </FormItem>
                    <FormItem>
                      <div className="flex justify-end mt-6">
                        <Button type="submit" className="w-full">
                          {show ? (
                            <Spinner
                              show={show}
                              size="small"
                              className="text-primary-foreground"
                            />
                          ) : (
                            "Verify OTP"
                          )}
                        </Button>
                      </div>
                    </FormItem>
                    <FormMessage>
                      {fieldState.invalid && (
                        <div className="text-red-600 text-sm mt-2 text-center">
                          {fieldState.invalid}
                        </div>
                      )}
                    </FormMessage>
                  </>
                </FormControl>
              )}
            />
          </form>
        </Form>
      </div>
    </div>
  );
}
hsavit1 commented 3 weeks ago

@ProductOfAmerica removing the zodResolver was very helpful for tracking down a bug, thanks for the tip