In a next.js 14 app, I have this component
import { FormControl, FormField, FormItem, FormLabel, FormLabelContent, FormMessage, LabelWithTooltipProps } from "@/components/ui/form";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { cn } from "@/lib/utils";
interface FormFieldSelectProps {
control: any;
name: string;
disabled?: boolean;
className?: string;
options: { value: string; label: string }[];
emptyOption?: { value: string; label: string };
fieldProps?: any;
labelProps: LabelWithTooltipProps;
labelWidth?: string;
}
/**
* A form field select component for selecting options.
*
* @component
* @example
* <FormFieldSelect
* name="propertyType"
* control={form.control}
* disabled={!isPowerUser}
* options={propertyTypeOption.map((propertyType) => ({
* value: propertyType,
* label: capitalize(propertyType),
* }))}
* fieldProps={{
* className: 'w-full',
* }}
* labelProps={{
* label: "Property type",
* isRequired: true,
* tooltipContent: "This helps classify properties.",
* }}
* labelWidth="w-64"
* />
*/
export default function FormFieldSelect({
control,
name,
disabled = false,
options,
emptyOption,
fieldProps = {},
labelProps,
className,
labelWidth = "w-64",
}: FormFieldSelectProps) {
return (
<FormField
control={control}
name={name}
render={({ field, formState }) => (
<FormItem
className={cn("flex gap-10 w-full items-start justify-start", className)}
>
<FormLabel className={cn("flex shrink-0", labelWidth)}>
<FormLabelContent {...labelProps} />
</FormLabel>
<div className="flex flex-col gap-2 w-full">
<Select
key={`select-${name}`}
onValueChange={field.onChange}
defaultValue={field.value}
disabled={disabled}
{...fieldProps}
>
<FormControl>
<SelectTrigger
className="w-full"
error={!!formState.errors[name]}
>
<SelectValue placeholder={"Select an option"} />
</SelectTrigger>
</FormControl>
<SelectContent
key={`select-content-${name}`}
>
{emptyOption && (
<SelectItem
key={`select-empty-${name}`}
value={emptyOption.value}>{emptyOption.label}</SelectItem>
)}
{options.map((option) => (
<SelectItem key={`select-item-${option.value}`} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage className="text-danger" />
</div>
</FormItem>
)}
/>
);
}
This component uses the Select component from shadcn/ui.
I use the formField in forms like so:
{/* GENDER */}
<FormFieldSelect
className={cn(formFieldWidth)}
key="genderSelect"
name="gender"
control={form.control}
options={genderOptions.map((genderOption) => ({
value: genderOption.value,
label: capitalize(genderOption.label),
}))}
fieldProps={{
className: 'w-full',
}}
labelProps={{
label: "Gender",
isRequired: false,
}}
labelWidth={formFieldLabelWidth}
/>
Where genderOptions = UserGenders from my type library:
export const UserGenders = [
{ value: "female", label: "Female" },
{ value: "male", label: "Male" },
{ value: "other", label: "Other" },
{ value: 'N/A', label: "Prefer not to say" },
]
All works fine, until user uses the translation feature of the browser (mostly Google Trad in Chrome, as it seems that Firefox translation does not provoke the same bug). The translation feature seems to be translating ALL the client (even metadata), which seems to be leading to some error.
Anytime a user tries to manipulate the FormFieldSelect in a browser translated page, it gets this error:
Uncaught DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
The error is clearly because of reconciliation between virtual DOM and actual DOM.
I've been trying many thing with keys, making the component a "client component"
, ... but I run out of ideas to reconciliate DOMs...
Any ideas?
Issue seems to come from Google Translate adding element to the page that NextJS does not know of.
2 solutions (more a workaround) are proposed on a shadcn issue:
translate="no"
to the html tag<span><span/>
like so:<Select>
<SelectTrigger>
<SelectValue/>
</SelectTrigger>
<SelectContent>
{menuItems.map((item) => (
<SelectItem key={item} value={item}>
<span>{item}</span>
</SelectItem>
))}
</SelectContent>
</Select>
Thanks a lot to @Radim Vaculik for its help!