admin管理员组

文章数量:1287598

Basically the title. Anytime I try to select a value from the MultiSelect, I run into the infinite loop.

I get that once I update the form state, it triggers a re-render and propagates that down to the child components I just can't understand why I am getting infinite loop since my field.onChange or alternatively form.setValue is inside a callback in the child.

Any ideas?

Parent

import { useCallback } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { debounce } from "lodash";
import { useForm } from "react-hook-form";
import { z } from "zod";
import {
  Form,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";

import { MultiSelect } from "./multi-select";

const options = [
  { value: "la", label: "LA" },
  { value: "ny", label: "NY" },
];

const optionSchema = z.object({
  label: z.string(),
  value: z.string(),
});

const schema = z
  .object({
    cities: z.array(optionSchema),
  })
  .required();

export type FormValues = z.infer<typeof schema>;

type Props = {
  handleSubmit: (data: Partial<FormValues>) => void;
};

export function CompanyProfileForm({ handleSubmit }: Props) {
  const form = useForm<FormValues>({
    resolver: zodResolver(schema),
    defaultValues: {
      cities: [],
    },
    values: {
      cities: [],
    },
    mode: "onChange",
  });

  const debouncedSubmitField = useCallback(
    debounce(async (fieldName: keyof FormValues) => {
      // Validate only this field
      const valid = await form.trigger(fieldName);
      if (valid) {
        const fieldValue = form.getValues(fieldName);
        console.log("fieldName :>> ", fieldName);
        console.log("fieldValue :>> ", fieldValue);
        handleSubmit({ [fieldName]: fieldValue });
      } else {
        console.error(
          `Validation failed for ${fieldName}:`,
          form.formState.errors[fieldName]
        );
      }
    }, 500),
    [form.trigger, form.getValues, form.formState.errors, handleSubmit]
  );

  return (
    <Form {...form}>
      <form className="flex flex-col gap-4">
        <FormField
          control={form.control}
          name="cities"
          render={({ field }) => (
            <FormItem className="flex flex-col w-full">
              <FormLabel className="text-zinc-600 font-normal dark:text-zinc-400">
                Cities
              </FormLabel>
              <MultiSelect
                form={form}
                field={field}
                options={options}
                formKey="cities"
              />
              <FormMessage />
            </FormItem>
          )}
        />
      </form>
    </Form>
  );
}

Child

import { useCallback, useState } from "react";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@radix-ui/react-popover";
import { isEqual } from "lodash";
import { ChevronsUpDown } from "lucide-react";

import { cn } from "../../lib/utils";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { FormControl } from "@/components/ui/form";

type Option = { label: string; value: string };

type Props = {
  field: any;
  options: Option[];
  debouncedSubmitField: (key: "cities") => void;
  formKey: "cities";
};

const SelectedOptions = ({
  selectedOptions,
}: {
  selectedOptions: Option[];
}) => {
  return selectedOptions.length
    ? selectedOptions.map((opt) => opt.label).join(", ")
    : "Select location";
};

export const MultiSelect = ({
  options,
  field,
  debouncedSubmitField,
  formKey,
}: Props) => {
  const [open, setOpen] = useState(false);
  const selectedOptions: Option[] = field.value;

  const handleSelectLocation = useCallback(
    (location: Option) => {
      let updatedOptions: Option[];

      if (selectedOptions.find((opt) => opt.value === location.value)) {
        updatedOptions = selectedOptions.filter(
          (opt) => opt.value !== location.value
        );
      } else {
        updatedOptions = [...selectedOptions, location];
      }

      if (!isEqual(selectedOptions, updatedOptions)) {
        console.log("selectedOptions :>> ", selectedOptions);
        console.log("updatedOptions :>> ", updatedOptions);
        field.onChange(updatedOptions); // <=== causes infinite loop
        // debouncedSubmitField(formKey);
      }
    },
    [debouncedSubmitField, field, formKey, selectedOptions]
  );

  const isSelected = (location: Option) =>
    !!selectedOptions.find((opt) => opt.value === location.value);

  const atLeastOneOptionSelected = selectedOptions.length > 0;

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <FormControl>
          <Button
            variant="outline"
            role="combobox"
            className={cn(
              "justify-between w-full text-md",
              !selectedOptions && "text-muted-foreground"
            )}
          >
            <SelectedOptions selectedOptions={selectedOptions} />
            <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
          </Button>
        </FormControl>
      </PopoverTrigger>
      <PopoverContent className="popover-content-width-full">
        <div className="w-full">
          {options.map((option) => {
            const isCurrentOptionSelected = isSelected(option);
            const isDisabled =
              !isCurrentOptionSelected && atLeastOneOptionSelected;
            return (
              <div
                className="flex items-center justify-between cursor-pointer p-2 hover:bg-slate-50 hover:rounded-sm text-md w-full"
                key={option.value}
                onClick={() => {
                  if (!isDisabled) {
                    handleSelectLocation(option);
                  }
                }}
              >
                <Checkbox checked={isSelected(option)} disabled={isDisabled} />
                <span>{option.label}</span>
              </div>
            );
          })}
        </div>
      </PopoverContent>
    </Popover>
  );
};

本文标签: reactjsreact hook formformsetValue or fieldonChange causes infinite loop in child componentStack Overflow