Hello I am trying to make a form using react-hook-form
and shadcn combobox. There are 2 files.
- category-form.tsx
- combobox.tsx (This is used inside category-form.tsx)
- category-form.tsx
'use client';
import * as z from 'zod';
import axios from 'axios';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { Pencil } from 'lucide-react';
import { useState } from 'react';
import toast from 'react-hot-toast';
import { useRouter } from 'next/navigation';
import { Course } from '@prisma/client';
import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { Combobox } from '@/components/ui/combobox';
interface CategoryFormProps {
initialData: Course;
courseId: string;
options: { label: string; value: string }[];
}
const formSchema = z.object({
categoryId: z.string().min(1),
});
export const CategoryForm = ({ initialData, courseId, options }: CategoryFormProps) => {
const [isEditing, setIsEditing] = useState(false);
const toggleEdit = () => setIsEditing((current) => !current);
const router = useRouter();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
categoryId: initialData?.categoryId || '',
},
});
const { isSubmitting, isValid } = form.formState;
const onSubmit = async (values: z.infer<typeof formSchema>) => {
try {
await axios.patch(`/api/courses/${courseId}`, values);
toast.success('Course updated');
toggleEdit();
router.refresh();
} catch {
toast.error('Something went wrong');
}
};
const selectedOption = options.find((option) => option.value === initialData.categoryId);
return (
<div className='mt-6 rounded-md border bg-slate-100 p-4'>
<div className='flex items-center justify-between font-medium'>
Course category
<Button onClick={toggleEdit} variant='ghost'>
{isEditing ? (
<>Cancel</>
) : (
<>
<Pencil className='mr-2 h-4 w-4' />
Edit category
</>
)}
</Button>
</div>
{!isEditing && (
<p
className={cn(
'mt-2 text-sm',
!initialData.categoryId && 'italic text-slate-500'
)}
>
{selectedOption?.label || 'No category'}
</p>
)}
{isEditing && (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='mt-4 space-y-4'>
<FormField
control={form.control}
name='categoryId'
render={({ field }) => {
return (
<FormItem>
<FormControl>
<Combobox options={...options} {...field} />
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<div className='flex items-center gap-x-2'>
<Button disabled={!isValid || isSubmitting} type='submit'>
Save
</Button>
</div>
</form>
</Form>
)}
</div>
);
};
- combobox.tsx (I copied this from shadcn combox)
"use client"
import * as React from "react"
import { Check, ChevronsUpDown } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
interface ComboboxProps {
options: { label: string; value: string }[];
value?: string;
onChange: (value: string) => void;
};
export const Combobox = ({
options,
value,
onChange
}: ComboboxProps) => {
const [open, setOpen] = React.useState(false)
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between"
>
{value
? options.find((option) => option.value === value)?.label
: "Select option..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder="Search option..." />
<CommandEmpty>No option found.</CommandEmpty>
<CommandGroup>
{options.map((option) => (
<CommandItem
key={option.value}
onSelect={() => {
onChange(option.value === value ? "" : option.value)
setOpen(false)
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
value === option.value ? "opacity-100" : "opacity-0"
)}
/>
{option.label}
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
)
}
- When I used the app I got an error saying
Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()
I believe occurs, from this line <Combobox options={...options} {...field} />
inside category-form.tsx . When I spread the field, a ref is also passed to the Combobox.
To handle this I need to wrap the Combobox
around React.forwardRef
, but I am not sure which element inside the comboform.tsx to associate the ref
with. I thought I should be Button
(in comboform.tsx), so here is what I did ->
import * as React from 'react';
import { Check, ChevronsUpDown } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from '@/components/ui/command';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
interface ComboboxProps {
options: { label: string; value: string }[];
value?: string;
onChange: (value: string) => void;
}
const Combobox = React.forwardRef<HTMLButtonElement, ComboboxProps>(
({ options, value, onChange }: ComboboxProps, ref) => {
const [open, setOpen] = React.useState(false);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant='outline'
role='combobox'
aria-expanded={open}
className='w-full justify-between'
ref={ref} // THIS IS WHERE I AM ASSOCIATING MY REF
>
{value
? options.find((option) => option.value === value)?.label
: 'Select option...'}
<ChevronsUpDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
</Button>
</PopoverTrigger>
<PopoverContent className='w-full p-0'>
<Command>
<CommandInput placeholder='Search option...' />
<CommandEmpty>No option found.</CommandEmpty>
<CommandGroup>
{options.map((option) => (
<CommandItem
key={option.value}
onSelect={() => {
onChange(option.value === value ? '' : option.value);
setOpen(false);
}}
>
<Check
className={cn(
'mr-2 h-4 w-4',
value === option.value ? 'opacity-100' : 'opacity-0'
)}
/>
{option.label}
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
);
}
);
Combobox.displayName = 'Combobox';
export { Combobox };
- Please tell me if I am right, or should I have associated my
ref
with some other element, in which case PLEASE tell me in a way in which typescript won't yell at me. Thank you.