domcomponentsnext.js14virtual-dom

next.js 14 component broken when using translation feature from Chrome


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?


Solution

  • 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:

    <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!