Search code examples
next.jsreact-hook-formzodradix-ui

Nextjs Shadcn form not submitting on click


I'm playing around with shadcn's react-hook-form and I've managed to navigate the docs to get the inputs and selects I need but the onSubmit function I've created isn't doing anything.

Does anyone know why?

"use client";

import * as z from "zod";
import { SubmitHandler, useForm, UseFormRegister } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Input } from "./ui/input";
import ErrorMessage from "./ErrorMessage";
import { Button } from "./ui/button";
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "./ui/Form";
import { Textarea } from "./ui/textarea";

import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import toast from "react-hot-toast";

type ReminderFormSchemaType = z.infer<typeof reminderFormSchema>;

export const reminderFormSchema = z.object({
  title: z.string().min(1, { message: "Title required!" }),
  description: z.string().max(160),
  location: z.string(),
  recurring: z.boolean(),
  recurringDigit: z.string(),
  recurringString: z.string(),
  priority: z.string(),
});

const CreateReminder = () => {
  const form = useForm<ReminderFormSchemaType>({
    resolver: zodResolver(reminderFormSchema),
  });

  function onSubmit(data: ReminderFormSchemaType) {
    toast.success("success!");
  }

  return (
    <Form {...form}>
      <form
        onSubmit={form.handleSubmit(onSubmit)}
        className="flex flex-col w-full"
      >
        <FormField
          control={form.control}
          name="title"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Title</FormLabel>
              <FormControl>
                <Input placeholder="Create a title..." {...field} />
              </FormControl>
              <FormDescription>
                {form.formState.errors.title && (
                  <ErrorMessage
                    message={form.formState.errors.title?.message}
                  />
                )}
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
        <FormField
          control={form.control}
          name="description"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Description</FormLabel>
              <FormControl>
                <Textarea
                  placeholder="Type your description here..."
                  {...field}
                />
              </FormControl>
              <FormDescription></FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
        <div className="flex flex-row justify-between items-center gap-x-3">
          <FormField
            control={form.control}
            name="recurringDigit"
            render={({ field }) => (
              <FormItem className="flex flex-row items-center gap-x-3">
                <FormLabel>Remind me every: </FormLabel>
                <Select onValueChange={field.onChange} defaultValue="1">
                  <FormControl>
                    <SelectTrigger className="w-[100px]">
                      <SelectValue placeholder="1" />
                    </SelectTrigger>
                  </FormControl>
                  <SelectContent className="bg-lightbackground w-[100px]">
                    {Array.from(String(123456789)).map((value) => (
                      <SelectItem
                        key={value}
                        value={value}
                        className="cursor-pointer transition hover:bg-white hover:text-black"
                      >
                        {value}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              </FormItem>
            )}
          />
          <div className="flex-1">
            <FormField
              control={form.control}
              name="recurringString"
              render={({ field }) => (
                <FormItem className="flex flex-row items-center flex-1 ">
                  <FormLabel></FormLabel>

                  <Select
                    onValueChange={field.onChange}
                    defaultValue="Minute(s)"
                  >
                    <FormControl>
                      <SelectTrigger className="flex-1 items-center">
                        <SelectValue placeholder="Minute(s)" />
                      </SelectTrigger>
                    </FormControl>
                    <SelectContent className="bg-lightbackground flex-1">
                      {["Day(s)", "Hour(s)", "Minute(s)"].map((value) => (
                        <SelectItem
                          key={value}
                          value={value}
                          className="cursor-pointer transition hover:bg-white hover:text-black"
                        >
                          {value}
                        </SelectItem>
                      ))}
                    </SelectContent>
                  </Select>
                </FormItem>
              )}
            />
          </div>
        </div>
        <div>
          <FormField
            control={form.control}
            name="priority"
            render={({ field }) => (
              <FormItem className="flex flex-row items-center gap-x-3">
                <FormLabel>Priority: </FormLabel>

                <Select onValueChange={field.onChange} defaultValue="High">
                  <FormControl>
                    <SelectTrigger className="flex-1">
                      <SelectValue placeholder="High" />
                    </SelectTrigger>
                  </FormControl>
                  <SelectContent className="bg-lightbackground flex-1">
                    {["Low", "Medium", "High"].map((value) => (
                      <SelectItem
                        key={value}
                        value={value}
                        className="cursor-pointer transition hover:bg-white hover:text-black"
                      >
                        {value}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>

                <FormMessage />
              </FormItem>
            )}
          />
        </div>
        <Button type="submit" className="bg-lightbackground my-3">
          Submit
        </Button>
      </form>
    </Form>
  );
};

export default CreateReminder;

I am getting an error I don't understand if that might be useful:

app-index.js:32 Warning: A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components
    at input
    at _c (webpack-internal:///(app-client)/./components/ui/input.tsx:13:11)
    at eval (webpack-internal:///(app-client)/./node_modules/@radix-ui/react-slot/dist/index.mjs:46:23)
    at eval (webpack-internal:///(app-client)/./node_modules/@radix-ui/react-slot/dist/index.mjs:20:23)
    at eval (webpack-internal:///(app-client)/./components/ui/Form.tsx:124:14)
    at div
    at eval (webpack-internal:///(app-client)/./components/ui/Form.tsx:74:11)
    at Controller (webpack-internal:///(app-client)/./node_modules/react-hook-form/dist/index.esm.mjs:544:37)
    at FormField (webpack-internal:///(app-client)/./components/ui/Form.tsx:29:14)
    at form
    at FormProvider (webpack-internal:///(app-client)/./node_modules/react-hook-form/dist/index.esm.mjs:180:13)
    at CreateReminder (webpack-internal:///(app-client)/./components/CreateReminder.tsx:53:75)
    at div
    at eval (webpack-internal:///(app-client)/./node_modules/@radix-ui/react-primitive/dist/index.mjs:44:26)
    at eval (webpack-internal:///(app-client)/./node_modules/@radix-ui/react-dismissable-layer/dist/index.mjs:44:42)
    at eval (webpack-internal:///(app-client)/./node_modules/@radix-ui/react-slot/dist/index.mjs:46:23)
    at eval (webpack-internal:///(app-client)/./node_modules/@radix-ui/react-slot/dist/index.mjs:20:23)
    at eval (webpack-internal:///(app-client)/./node_modules/@radix-ui/react-primitive/dist/index.mjs:44:26)
    at eval (webpack-internal:///(app-client)/./node_modules/@radix-ui/react-focus-scope/dist/index.mjs:32:19)
    at eval (webpack-internal:///(app-client)/./node_modules/@radix-ui/react-dialog/dist/index.mjs:274:28)
    at eval (webpack-internal:///(app-client)/./node_modules/@radix-ui/react-dialog/dist/index.mjs:206:102)
    at $921a889cee6df7e8$export$99c2b779aa4e8b8b (webpack-internal:///(app-client)/./node_modules/@radix-ui/react-presence/dist/index.mjs:28:22)
    at eval (webpack-internal:///(app-client)/./node_modules/@radix-ui/react-dialog/dist/index.mjs:191:108)
    at div
    at eval (webpack-internal:///(app-client)/./node_modules/@radix-ui/react-slot/dist/index.mjs:46:23)
    at eval (webpack-internal:///(app-client)/./node_modules/@radix-ui/react-slot/dist/index.mjs:20:23)
    at eval (webpack-internal:///(app-client)/./node_modules/@radix-ui/react-primitive/dist/index.mjs:44:26)
    at eval (webpack-internal:///(app-client)/./node_modules/@radix-ui/react-portal/dist/index.mjs:24:24)
    at $921a889cee6df7e8$export$99c2b779aa4e8b8b (webpack-internal:///(app-client)/./node_modules/@radix-ui/react-presence/dist/index.mjs:28:22)
    at Provider (webpack-internal:///(app-client)/./node_modules/@radix-ui/react-context/dist/index.mjs:47:28)
    at $5d3850c4d0b4e6c7$export$dad7c95542bacce0 (webpack-internal:///(app-client)/./node_modules/@radix-ui/react-dialog/dist/index.mjs:133:28)
    at DialogPortal (webpack-internal:///(app-client)/./components/ui/dialog.tsx:25:11)
    at _c2 (webpack-internal:///(app-client)/./components/ui/dialog.tsx:60:11)
    at Provider (webpack-internal:///(app-client)/./node_modules/@radix-ui/react-context/dist/index.mjs:47:28)
    at $5d3850c4d0b4e6c7$export$3ddf2d174ce01153 (webpack-internal:///(app-client)/./node_modules/@radix-ui/react-dialog/dist/index.mjs:77:28)
    at Modal (webpack-internal:///(app-client)/./components/Modal.tsx:15:11)
    at div
    at div
    at div
    at div
    at Sidebar (webpack-internal:///(app-client)/./components/Sidebar.tsx:15:11)
    at f (webpack-internal:///(app-client)/./node_modules/next-themes/dist/index.module.js:8:597)
    at $ (webpack-internal:///(app-client)/./node_modules/next-themes/dist/index.module.js:8:348)
    at Providers (webpack-internal:///(app-client)/./app/providers/Providers.tsx:7:11)
    at body
    at html
    at RedirectErrorBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/redirect-boundary.js:73:9)
    at RedirectBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/redirect-boundary.js:81:11)
    at NotFoundErrorBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/not-found-boundary.js:51:9)
    at NotFoundBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/not-found-boundary.js:59:11)
    at ReactDevOverlay (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/react-dev-overlay/internal/ReactDevOverlay.js:66:9)
    at HotReload (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js:276:11)
    at Router (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/app-router.js:90:11)
    at ErrorBoundaryHandler (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/error-boundary.js:80:9)
    at ErrorBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/error-boundary.js:106:11)
    at AppRouter (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/app-router.js:387:13)
    at ServerRoot (webpack-internal:///(app-client)/./node_modules/next/dist/client/app-index.js:154:11)
    at RSCComponent
    at Root (webpack-internal:///(app-client)/./node_modules/next/dist/client/app-index.js:171:11)

I'm using this form inside the Dialog component which is being conditionally rendered like so:

"use client";

import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "../components/ui/dialog";
import CreateReminder from "./CreateReminder";

import LoginForm from "./LoginForm";
import RegisterForm from "./RegisterForm";
import Subcategory from "./Subcategory";
import { Button } from "./ui/button";

import { IconType } from "react-icons";

const Modal: React.FC<{ label: string; icon: IconType }> = ({
  label,
  icon,
}) => {
  return (
    <Dialog>
      <DialogTrigger className="w-full">
        <Subcategory label={label} icon={icon} />
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle className="text-center py-2">{label}</DialogTitle>
          <DialogDescription></DialogDescription>
        </DialogHeader>
        {label === "Login" ? (
          <LoginForm />
        ) : label === "Register" ? (
          <RegisterForm />
        ) : (
          <CreateReminder />
        )}
      </DialogContent>
    </Dialog>
  );
};

export default Modal;

Solution

  • Seems like you need to pass initial values when you define the form, because they are undefined at first, and then when you type inside the input its not undefined anymore.

    When it changed from undefined to 'some value' the type will change from uncontrolled to controlled

    const form = useForm<ReminderFormSchemaType>({
      resolver: zodResolver(reminderFormSchema),
      defaultValues: {
       title: '',
       description: '',
       ...
      }
    });