react-hook-formshadcnui

React Hook Form with shadcn: How to select multi checkbox


I'm using React Hook Form with shadcn UI components and zod for validation in my React project. I have a list of services where each checkbox represents a service, and I want to store the selected service IDs in an array field (serviceDetails.services). When a checkbox is checked, I want to add the service ID to the array; when unchecked, I want to remove it.

Here is my current code using shadcn's Checkbox component:

//constants.ts
const SERVICES = ["fitness", "sauna", "cardio"] as const;

//schema.ts
const serviceDetailsSchema = z.object({
  services: z.array(z.enum(SERVICES)),
  totalAmount: z.number().min(0, "Total amount must be non-negative"),
});
export const memberEntrySchema = z.object({
  serviceDetails: serviceDetailsSchema,
});

//types.ts
export type MemberEntryFormData = z.infer<typeof memberEntrySchema>;


//MemberEntryForm.tsx

const MemberEntryForm = () => {
  const services = [
    {
      id: 1,
      name: "Fitness",
      price: 1000,
    },
    {
      id: 2,
      name: "Sauna",
      price: 500,
    },
    {
      id: 3,
      name: "Cardio",
      price: 800,
    },
  ];

const methods = useForm<MemberEntryFormData>({
    resolver: zodResolver(memberEntrySchema),
    defaultValues: {
      //other forms default values aswell
      serviceDetails: {
        services: [],
        totalAmount: 0,
      },
    },
  });
 const onSubmit = (data: MemberEntryFormData) => {
    console.log(data);
  };
 return (
    <FormProvider {...methods}>
      <form onSubmit={methods.handleSubmit(onSubmit)} className="space-y-6">
    //other forms aswell
    <ServiceDetailsForm services={services} />
      </form>
    </FormProvider>
)


//ServiceDetailsForm.tsx

type ServiceDetailsFormProps = {
  services: {
    id: number;
    name: string;
    price: number;
  }[];
};

const ServiceDetailsForm = ({ services }: ServiceDetailsFormProps) => {
  const { control, watch } = useFormContext<MemberEntryFormData>();
  const servicesValue = watch("serviceDetails.services");

  console.log(servicesValue);

  const items = services.map((service) => ({
    id: service.name + " - " + service.price + "/month",
    label: service.name + " - " + service.price + "/month",
  }));
  return (
    <div className="space-y-2">
      <Heading3>Service Details</Heading3>
      <FormField
        control={control}
        name="serviceDetails.services"
        render={() => (
          <FormItem>
            <div className="mb-4">
              <FormLabel className="text-base">Sidebar</FormLabel>
            </div>
            {items.map((item, index) => (
              <FormField
                key={item.id}
                control={control}
                name={`serviceDetails.services`}
                render={({ field }) => {
                  return (
                    <FormItem
                      key={item.id}
                      className="flex flex-row items-start space-x-3 space-y-0"
                    >
                      <FormControl>
                        <Checkbox
                          checked={field.value?.includes(item.id)}
                          onCheckedChange={(checked) => {
                            const updatedValues = Array.isArray(field.value)
                              ? field.value
                              : [];
                            if (checked) {
                              field.onChange([...updatedValues, item.id]);
                            } else {
                              field.onChange(
                                updatedValues.filter(
                                  (value) => value !== item.id
                                )
                              );
                            }
                          }}
                        />
                      </FormControl>
                      <FormLabel className="font-normal">
                        {item.label}
                      </FormLabel>
                    </FormItem>
                  );
                }}
              />
            ))}
            <FormMessage />
          </FormItem>
        )}
      />
      //other form fields
   </div>
  );
};
    

I'm encountering an "undefined" error when submitting the form. Here's the error.

enter image description here

Please let me know what I am missing here.


Solution

  • I was able to get rid of the undefined error. The first thing I did was simplifying the id for the items variable so it matches your SERVICES constant.

    const items = services.map((service) => ({
          id: service.name.toLowerCase() as typeof SERVICES[number], // Change this line
          label: service.name + " - " + service.price + "/month",
    }));
    

    Then in your Checkbox component, I changed the checked attribute to this, so it matches the item's id correctly:

    <Checkbox
        checked={servicesValue?.includes(item.id as typeof SERVICES[number])}
    

    I'm not sure if this is what you're looking for but it does get rid of the undefined error for me.