You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

useFieldArray搭配Material UI Select组件失效问题排查

react-hook-form useFieldArray + Material UI Select 动态表单异常修复

问题现象

用 react-hook-form 的 useFieldArray 实现带 Material UI Select 组件的动态表单时,TextField 功能正常,但 Select 组件存在两个问题:

  • 设置的 defaultValue 在 Select 中无法显示,TextField 正常;
  • 提交表单时,无法获取 Select 的选中值和 TextField 的输入值。

Select 组件代码

import * as React from "react";
import {
  FormControl,
  FormHelperText,
  InputLabel,
  MenuItem,
  OutlinedInput,
  Select,
  SelectChangeEvent
} from "@mui/material";
import { useState } from "react";
import { useFormContext, Controller } from "react-hook-form";
import Placeholder from "./Placeholder";

export interface IOptionTypes {
  id: string;
  label: string;
  value: string;
}

interface IFormElementTypes {
  name: string;
  label: string;
  required?: boolean;
  defaultValue: string;
  options: IOptionTypes[];
  placeholder: string;
}

export default function MultiSelectField({
  name,
  label,
  required,
  defaultValue,
  options,
  placeholder
}: IFormElementTypes) {
  const {
    control,
    register,
    formState: { errors }
  } = useFormContext();

  const [selectedVal, setSelectedVal] = useState<string[]>([]);

  const handleChange = (event: SelectChangeEvent<typeof selectedVal>) => {
    const {
      target: { value }
    } = event;
    setSelectedVal(
      typeof value === "string" ? value.split(",") : value
    );
  };
  const labelText = `${label}${required ? "*" : ""}`;
  const labelId = `multi-select-${name}`;
  return (
    <Controller
      name={name}
      defaultValue={defaultValue}
      control={control}
      render={({ field }) => (
        <FormControl fullWidth>
          <InputLabel
            shrink
            sx={{ backgroundColor: "white", padding: "0px 2px" }}
            error={!!errors[name]}
            id={labelId}
          >
            {labelText}
          </InputLabel>
          <Select
            {...field}
            label={labelText}
            labelId={labelId}
            value={selectedVal}
            displayEmpty
            multiple
            {...register(`${name}` as const)}
            variant="outlined"
            fullWidth
            error={!!errors[name]}
            input={
              <OutlinedInput
                id={`multi-select-${label}`}
                label={`${label}${required ? "*" : ""}`}
              />
            }
            renderValue={
              selectedVal.length > 0
                ? undefined
                : () => <Placeholder>{placeholder}</Placeholder>
            }
            onChange={handleChange}
          >
            {options.map((option) => (
              <MenuItem key={option.id} value={option.value}>
                {option.label}
              </MenuItem>
            ))}
          </Select>
          <FormHelperText>{String(errors[name]?.message ?? "")}</FormHelperText>
        </FormControl>
      )}
    />
  );
}

调用代码

import * as React from "react";
import {
  useForm,
  useFieldArray,
  FormProvider,
  SubmitHandler
} from "react-hook-form";
import { Button, Grid, Stack, Box } from "@mui/material";
import MultiSelectField, { IOptionTypes } from "./MultiSelectField";
import TextFieldElement from "./TextFieldElement";

type FormValues = {
  items: { name: string; age: string }[];
};

const defaultValues = {
  items: [{ name: "john", age: "10" }]
};

const names: IOptionTypes[] = [
  { id: "1", label: "John", value: "john" },
  { id: "2", label: "Smith", value: "smith" },
  { id: "3", label: "Julia", value: "julia" },
  { id: "4", label: "David", value: "david" }
];

export default function App() {
  const methods = useForm<FormValues>({
    defaultValues,
    mode: "all"
  });
  const { control, formState, setError } = methods;

  const { fields, append, remove } = useFieldArray({ name: "items", control });

  const formSubmitHandler: SubmitHandler<FormValues> = async (
    data: FormValues
  ) => {
    console.log("Form values ", data);
  };

  return (
    <Grid container>
      <Grid item xs={12}>
        <Stack direction="row" spacing={2} my={2}>
          <Button
            variant="outlined"
            color="primary"
            type="submit"
            disableElevation
            fullWidth
            onClick={() => {
              append({ name: "", age: "" });
            }}
          >
            Add Field
          </Button>

          <Button
            variant="outlined"
            color="primary"
            type="submit"
            disableElevation
            fullWidth
            onClick={() => {
              remove(fields.length - 1);
            }}
          >
            Remove Field
          </Button>
        </Stack>
      </Grid>

      <FormProvider {...methods}>
        <form onSubmit={methods.handleSubmit(formSubmitHandler)}>
          <Grid item xs={12}>
            <Stack spacing={2}>
              {fields.map((field, index) => (
                <Stack direction="row" spacing={2} key={field.id}>
                  <MultiSelectField
                    name={`items.${index}.name`}
                    label="Choose Name"
                    placeholder="Name"
                    defaultValue=""
                    options={names}
                  />
                  <TextFieldElement
                    name={`items.${index}.age`}
                    label="Input Age"
                    placeholder="Age"
                  />
                </Stack>
              ))}
            </Stack>
          </Grid>
          <Grid container my={2}>
            <Grid item>
              <Button
                variant="outlined"
                color="primary"
                type="submit"
                disableElevation
                fullWidth
              >
                Save Changes
              </Button>
            </Grid>
          </Grid>
        </form>
      </FormProvider>
    </Grid>
  );
}

问题原因

  1. 本地状态与表单状态脱节:自定义的 selectedVal 本地状态没有和 react-hook-form 提供的字段状态同步,默认值无法从表单状态映射到组件,选中值也无法更新到表单状态。
  2. 重复注册字段:同时使用 Controller 和 register,导致表单状态管理冲突,Controller 已负责字段注册和状态同步,无需重复调用 register。
  3. 类型不匹配:Select 是多选模式,值类型应为数组,但传入的 defaultValue 是字符串,初始值类型也不匹配。
  4. 上下文范围错误:FormProvider 没有包裹添加/删除按钮,且表单字段部分在上下文外,导致字段无法正确关联表单状态。

修复步骤

1. 移除本地状态,同步表单状态

删除 selectedVal 本地状态,改用 Controller 提供的 field.value,修改 onChange 逻辑更新表单状态:

export default function MultiSelectField({
  name,
  label,
  required,
  defaultValue,
  options,
  placeholder
}: IFormElementTypes) {
  const {
    control,
    formState: { errors }
  } = useFormContext();

  const labelText = `${label}${required ? "*" : ""}`;
  const labelId = `multi-select-${name}`;
  
  return (
    <Controller
      name={name}
      defaultValue={defaultValue}
      control={control}
      render={({ field }) => {
        const handleChange = (event: SelectChangeEvent<string[]>) => {
          const { value } = event.target;
          const newValue = typeof value === "string" ? value.split(",") : value;
          field.onChange(newValue);
        };
        
        return (
          <FormControl fullWidth>
            <InputLabel
              shrink
              sx={{ backgroundColor: "white", padding: "0px 2px" }}
              error={!!errors[name]}
              id={labelId}
            >
              {labelText}
            </InputLabel>
            <Select
              {...field}
              label={labelText}
              labelId={labelId}
              value={field.value}
              displayEmpty
              multiple
              variant="outlined"
              fullWidth
              error={!!errors[name]}
              input={
                <OutlinedInput
                  id={`multi-select-${label}`}
                  label={labelText}
                />
              }
              renderValue={
                field.value.length > 0
                  ? undefined
                  : () => <Placeholder>{placeholder}</Placeholder>
              }
              onChange={handleChange}
            >
              {options.map((option) => (
                <MenuItem key={option.id} value={option.value}>
                  {option.label}
                </MenuItem>
              ))}
            </Select>
            <FormHelperText>{String(errors[name]?.message ?? "")}</FormHelperText>
          </FormControl>
        );
      }}
    />
  );
}

2. 修正类型定义与默认值

更新 IFormElementTypes 接口的 defaultValue 类型为数组,同时调整初始值和调用时的参数:

// 修正接口
interface IFormElementTypes {
  name: string;
  label: string;
  required?: boolean;
  defaultValue: string[];
  options: IOptionTypes[];
  placeholder: string;
}

// 修正表单初始值
const defaultValues = {
  items: [{ name: ["john"], age: "10" }]
};

// 调用时传递正确的默认值
<MultiSelectField
  name={`items.${index}.name`}
  label="Choose Name"
  placeholder="Name"
  defaultValue={field.name || []}
  options={names}
/>

3. 调整 FormProvider 范围

将 FormProvider 包裹整个表单相关内容,包括添加/删除按钮:

export default function App() {
  const methods = useForm<FormValues>({
    defaultValues,
    mode: "all"
  });
  const { control, formState, setError } = methods;

  const { fields, append, remove } = useFieldArray({ name: "items", control });

  const formSubmitHandler: SubmitHandler<FormValues> = async (
    data: FormValues
  ) => {
    console.log("Form values ", data);
  };

  return (
    <FormProvider {...methods}>
      <Grid container>
        <Grid item xs={12}>
          <Stack direction="row" spacing={2} my={2}>
            <Button
              variant="outlined"
              color="primary"
              disableElevation
              fullWidth
              onClick={() => {
                append({ name: [], age: "" });
              }}
            >
              Add Field
            </Button>

            <Button
              variant="outlined"
              color="primary"
              disableElevation
              fullWidth
              onClick={() => {
                remove(fields.length - 1);
              }}
            >
              Remove Field
            </Button>
          </Stack>
        </Grid>

        <form onSubmit={methods.handleSubmit(formSubmitHandler)}>
          <Grid item xs={12}>
            <Stack spacing={2}>
              {fields.map((field, index) => (
                <Stack direction="row" spacing={2} key={field.id}>
                  <MultiSelectField
                    name={`items.${index}.name`}
                    label="Choose Name"
                    placeholder="Name"
                    defaultValue={field.name || []}
                    options={names}
                  />
                  <TextFieldElement
                    name={`items.${index}.age`}
                    label="Input Age"
                    placeholder="Age"
                  />
                </Stack>
              ))}
            </Stack>
          </Grid>
          <Grid container my={2}>
            <Grid item>
              <Button
                variant="outlined"
                color="primary"
                type="submit"
                disableElevation
                fullWidth
              >
                Save Changes
              </Button>
            </Grid>
          </Grid>
        </form>
      </Grid>
    </FormProvider>
  );
}

效果验证

修复后:

  • Select 组件会正确显示默认选中值;
  • 提交表单时,能正常获取 Select 的选中数组和 TextField 的输入值;
  • 动态添加/删除字段功能正常。

内容的提问来源于stack exchange,提问作者iphonic

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 07:57:13